Browse Source

fix: Fix Docker deployment issues and configure for local development

Fix multiple issues encountered during local Docker deployment:

Docker Configuration:
- Fix Dockerfile to handle package-lock.json properly (use npm install if missing)
- Update .dockerignore to include package-lock.json in builds
- Add docker/sync-models.js script to sync Sequelize models before migrations
- Update docker-compose.yml to sync models before running migrations
- Configure API to use port 3001 (to avoid conflict with port 3000)
- Update docker-compose.yml to read PORT from .env file

Database Configuration:
- Update config/config.json for Docker environment (host: db, password: postgres)
- Make migrations resilient by checking table existence before operations
- Update all three migrations to skip if tables don't exist (tables created by model sync)

Nginx Configuration:
- Fix nginx.conf log format (body_size_sent -> body_bytes_sent)
- Update nginx upstream to proxy to api:3001 instead of api:3000

Entrypoint Script:
- Add model sync step to entrypoint.sh before migrations

These changes ensure:
- Containers start successfully without migration errors
- Database tables are created via model sync before migrations run
- Migrations are idempotent and can run on fresh databases
- Port configuration is consistent across all services
- Local development works with port 3001 to avoid conflicts
Hussain Afzal 3 months ago
parent
commit
fe44f0b9b7

+ 4 - 2
Dockerfile

@@ -8,7 +8,8 @@ WORKDIR /app
8 8
 COPY package*.json ./
9 9
 
10 10
 # Install all dependencies (including dev dependencies for build tools)
11
-RUN npm ci
11
+# Use npm install if package-lock.json doesn't exist, otherwise use npm ci
12
+RUN if [ -f package-lock.json ]; then npm ci; else npm install; fi
12 13
 
13 14
 # Copy application code and scripts (needed for development)
14 15
 COPY . .
@@ -39,7 +40,8 @@ WORKDIR /app
39 40
 COPY package*.json ./
40 41
 
41 42
 # Install only production dependencies
42
-RUN npm ci --only=production && npm cache clean --force
43
+# Use npm install if package-lock.json doesn't exist, otherwise use npm ci
44
+RUN if [ -f package-lock.json ]; then npm ci --only=production; else npm install --only=production; fi && npm cache clean --force
43 45
 
44 46
 # Copy application code
45 47
 COPY . .

+ 12 - 9
config/config.json

@@ -1,26 +1,29 @@
1 1
 {
2 2
   "development": {
3 3
     "username": "postgres",
4
-    "password": "mqldev@123",
4
+    "password": "postgres",
5 5
     "database": "financial_data",
6
-    "host": "127.0.0.1",
6
+    "host": "db",
7 7
     "port": 5432,
8
-    "dialect": "postgres"
8
+    "dialect": "postgres",
9
+    "use_env_variable": false
9 10
   },
10 11
   "test": {
11 12
     "username": "postgres",
12
-    "password": "mqldev@123",
13
+    "password": "postgres",
13 14
     "database": "financial_data",
14
-    "host": "127.0.0.1",
15
+    "host": "db",
15 16
     "port": 5432,
16
-    "dialect": "postgres"
17
+    "dialect": "postgres",
18
+    "use_env_variable": false
17 19
   },
18 20
   "production": {
19 21
     "username": "postgres",
20
-    "password": "mqldev@123",
22
+    "password": "postgres",
21 23
     "database": "financial_data",
22
-    "host": "127.0.0.1",
24
+    "host": "db",
23 25
     "port": 5432,
24
-    "dialect": "postgres"
26
+    "dialect": "postgres",
27
+    "use_env_variable": false
25 28
   }
26 29
 }

+ 3 - 3
docker-compose.yml

@@ -33,7 +33,7 @@ services:
33 33
     container_name: market-data-api
34 34
     environment:
35 35
       - NODE_ENV=${NODE_ENV:-development}
36
-      - PORT=${PORT:-3000}
36
+      - PORT=${PORT:-3001}
37 37
       - DB_HOST=db
38 38
       - DB_PORT=5432
39 39
       - DB_NAME=${DB_NAME:-financial_data}
@@ -50,7 +50,7 @@ services:
50 50
       - ./models:/app/models
51 51
       - ./logs:/app/logs
52 52
     ports:
53
-      - "${PORT:-3000}:3000"
53
+      - "${PORT:-3001}:3001"
54 54
     depends_on:
55 55
       db:
56 56
         condition: service_healthy
@@ -59,7 +59,7 @@ services:
59 59
     restart: unless-stopped
60 60
     # Override entrypoint for development to use nodemon with live reload
61 61
     entrypoint: /bin/sh
62
-    command: -c "/usr/local/bin/wait-for-db.sh && npx sequelize-cli db:migrate && npm install -g nodemon && nodemon src/server.js"
62
+    command: -c "/usr/local/bin/wait-for-db.sh && node docker/sync-models.js && npx sequelize-cli db:migrate && npm install -g nodemon && nodemon src/server.js"
63 63
 
64 64
   # Nginx Reverse Proxy
65 65
   nginx:

+ 17 - 0
docker/entrypoint.sh

@@ -30,6 +30,23 @@ else
30 30
     fi
31 31
 fi
32 32
 
33
+# Sync models first to create tables (if they don't exist)
34
+echo "Syncing database models..."
35
+node -e "
36
+const { sequelize } = require('./src/models');
37
+sequelize.sync({ alter: true, force: false })
38
+  .then(() => {
39
+    console.log('Database models synced successfully.');
40
+    process.exit(0);
41
+  })
42
+  .catch((error) => {
43
+    console.error('Error syncing models:', error);
44
+    process.exit(1);
45
+  });
46
+" || {
47
+    echo "WARNING: Model sync failed, continuing with migrations..."
48
+}
49
+
33 50
 # Run database migrations
34 51
 echo "Running database migrations..."
35 52
 npx sequelize-cli db:migrate || {

+ 13 - 0
docker/sync-models.js

@@ -0,0 +1,13 @@
1
+// Script to sync Sequelize models before running migrations
2
+const { sequelize } = require('../src/models');
3
+
4
+sequelize.sync({ alter: true, force: false })
5
+  .then(() => {
6
+    console.log('Database models synced successfully.');
7
+    process.exit(0);
8
+  })
9
+  .catch((error) => {
10
+    console.error('Error syncing models:', error);
11
+    process.exit(1);
12
+  });
13
+

+ 31 - 6
migrations/20251016210526-add_unique_constraint_candles.js

@@ -3,14 +3,39 @@
3 3
 /** @type {import('sequelize-cli').Migration} */
4 4
 module.exports = {
5 5
   async up (queryInterface, Sequelize) {
6
-    await queryInterface.addConstraint('candles_1h', {
7
-      fields: ['symbol_id', 'open_time'],
8
-      type: 'unique',
9
-      name: 'unique_symbol_open_time'
10
-    });
6
+    // Check if table exists before adding constraint
7
+    const tableExists = await queryInterface.sequelize.query(
8
+      "SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'candles_1h');",
9
+      { type: Sequelize.QueryTypes.SELECT }
10
+    );
11
+    
12
+    if (tableExists && tableExists[0] && tableExists[0].exists) {
13
+      // Check if constraint already exists
14
+      const constraintExists = await queryInterface.sequelize.query(
15
+        "SELECT EXISTS (SELECT FROM pg_constraint WHERE conname = 'unique_symbol_open_time');",
16
+        { type: Sequelize.QueryTypes.SELECT }
17
+      );
18
+      
19
+      if (!constraintExists || !constraintExists[0] || !constraintExists[0].exists) {
20
+        await queryInterface.addConstraint('candles_1h', {
21
+          fields: ['symbol_id', 'open_time'],
22
+          type: 'unique',
23
+          name: 'unique_symbol_open_time'
24
+        });
25
+      }
26
+    } else {
27
+      console.log('Table candles_1h does not exist, skipping constraint addition. Tables will be created by model sync.');
28
+    }
11 29
   },
12 30
 
13 31
   async down (queryInterface, Sequelize) {
14
-    await queryInterface.removeConstraint('candles_1h', 'unique_symbol_open_time');
32
+    const tableExists = await queryInterface.sequelize.query(
33
+      "SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'candles_1h');",
34
+      { type: Sequelize.QueryTypes.SELECT }
35
+    );
36
+    
37
+    if (tableExists && tableExists[0] && tableExists[0].exists) {
38
+      await queryInterface.removeConstraint('candles_1h', 'unique_symbol_open_time');
39
+    }
15 40
   }
16 41
 };

+ 23 - 6
migrations/20251027075914-add-index-to-instrument-type.js

@@ -3,14 +3,31 @@
3 3
 /** @type {import('sequelize-cli').Migration} */
4 4
 module.exports = {
5 5
   async up (queryInterface, Sequelize) {
6
-    // Drop the existing CHECK constraint if it exists and add a new one with 'index'
7
-    await queryInterface.sequelize.query("ALTER TABLE symbols DROP CONSTRAINT IF EXISTS symbols_instrument_type_check;");
8
-    await queryInterface.sequelize.query("ALTER TABLE symbols ADD CONSTRAINT symbols_instrument_type_check CHECK (instrument_type IN ('crypto', 'stock', 'forex', 'commodity', 'index'));");
6
+    // Check if table exists
7
+    const tableExists = await queryInterface.sequelize.query(
8
+      "SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'symbols');",
9
+      { type: Sequelize.QueryTypes.SELECT }
10
+    );
11
+    
12
+    if (tableExists && tableExists[0] && tableExists[0].exists) {
13
+      // Drop the existing CHECK constraint if it exists and add a new one with 'index'
14
+      await queryInterface.sequelize.query("ALTER TABLE symbols DROP CONSTRAINT IF EXISTS symbols_instrument_type_check;");
15
+      await queryInterface.sequelize.query("ALTER TABLE symbols ADD CONSTRAINT symbols_instrument_type_check CHECK (instrument_type IN ('crypto', 'stock', 'forex', 'commodity', 'index'));");
16
+    } else {
17
+      console.log('Table symbols does not exist, skipping constraint update. Tables will be created by model sync.');
18
+    }
9 19
   },
10 20
 
11 21
   async down (queryInterface, Sequelize) {
12
-    // Revert the CHECK constraint without 'index'
13
-    await queryInterface.sequelize.query("ALTER TABLE symbols DROP CONSTRAINT symbols_instrument_type_check;");
14
-    await queryInterface.sequelize.query("ALTER TABLE symbols ADD CONSTRAINT symbols_instrument_type_check CHECK (instrument_type IN ('crypto', 'stock', 'forex', 'commodity'));");
22
+    const tableExists = await queryInterface.sequelize.query(
23
+      "SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'symbols');",
24
+      { type: Sequelize.QueryTypes.SELECT }
25
+    );
26
+    
27
+    if (tableExists && tableExists[0] && tableExists[0].exists) {
28
+      // Revert the CHECK constraint without 'index'
29
+      await queryInterface.sequelize.query("ALTER TABLE symbols DROP CONSTRAINT IF EXISTS symbols_instrument_type_check;");
30
+      await queryInterface.sequelize.query("ALTER TABLE symbols ADD CONSTRAINT symbols_instrument_type_check CHECK (instrument_type IN ('crypto', 'stock', 'forex', 'commodity'));");
31
+    }
15 32
   }
16 33
 };

+ 11 - 0
migrations/20251112102032-add-timeframe-to-candles.js

@@ -3,6 +3,17 @@
3 3
 /** @type {import('sequelize-cli').Migration} */
4 4
 module.exports = {
5 5
   async up (queryInterface, Sequelize) {
6
+    // Check if candles_1h table exists
7
+    const tableExists = await queryInterface.sequelize.query(
8
+      "SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'candles_1h');",
9
+      { type: Sequelize.QueryTypes.SELECT }
10
+    );
11
+    
12
+    if (!tableExists || !tableExists[0] || !tableExists[0].exists) {
13
+      console.log('Table candles_1h does not exist, skipping migration. Tables will be created by model sync with timeframe support.');
14
+      return;
15
+    }
16
+    
6 17
     // Rename table from candles_1h to candles
7 18
     await queryInterface.renameTable('candles_1h', 'candles');
8 19
 

+ 2 - 2
nginx/nginx.conf

@@ -15,7 +15,7 @@ http {
15 15
     default_type application/octet-stream;
16 16
 
17 17
     log_format main '$remote_addr - $remote_user [$time_local] "$request" '
18
-                    '$status $body_size_sent "$http_referer" '
18
+                    '$status $body_bytes_sent "$http_referer" '
19 19
                     '"$http_user_agent" "$http_x_forwarded_for"';
20 20
 
21 21
     access_log /var/log/nginx/access.log main;
@@ -36,7 +36,7 @@ http {
36 36
 
37 37
     # Upstream API server
38 38
     upstream api {
39
-        server api:3000;
39
+        server api:3001;
40 40
     }
41 41
 
42 42
     server {