Bläddra i källkod

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 månader sedan
förälder
incheckning
fe44f0b9b7

+ 4 - 2
Dockerfile

@@ -8,7 +8,8 @@ WORKDIR /app
8
 COPY package*.json ./
8
 COPY package*.json ./
9
 
9
 
10
 # Install all dependencies (including dev dependencies for build tools)
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
 # Copy application code and scripts (needed for development)
14
 # Copy application code and scripts (needed for development)
14
 COPY . .
15
 COPY . .
@@ -39,7 +40,8 @@ WORKDIR /app
39
 COPY package*.json ./
40
 COPY package*.json ./
40
 
41
 
41
 # Install only production dependencies
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
 # Copy application code
46
 # Copy application code
45
 COPY . .
47
 COPY . .

+ 12 - 9
config/config.json

@@ -1,26 +1,29 @@
1
 {
1
 {
2
   "development": {
2
   "development": {
3
     "username": "postgres",
3
     "username": "postgres",
4
-    "password": "mqldev@123",
4
+    "password": "postgres",
5
     "database": "financial_data",
5
     "database": "financial_data",
6
-    "host": "127.0.0.1",
6
+    "host": "db",
7
     "port": 5432,
7
     "port": 5432,
8
-    "dialect": "postgres"
8
+    "dialect": "postgres",
9
+    "use_env_variable": false
9
   },
10
   },
10
   "test": {
11
   "test": {
11
     "username": "postgres",
12
     "username": "postgres",
12
-    "password": "mqldev@123",
13
+    "password": "postgres",
13
     "database": "financial_data",
14
     "database": "financial_data",
14
-    "host": "127.0.0.1",
15
+    "host": "db",
15
     "port": 5432,
16
     "port": 5432,
16
-    "dialect": "postgres"
17
+    "dialect": "postgres",
18
+    "use_env_variable": false
17
   },
19
   },
18
   "production": {
20
   "production": {
19
     "username": "postgres",
21
     "username": "postgres",
20
-    "password": "mqldev@123",
22
+    "password": "postgres",
21
     "database": "financial_data",
23
     "database": "financial_data",
22
-    "host": "127.0.0.1",
24
+    "host": "db",
23
     "port": 5432,
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
     container_name: market-data-api
33
     container_name: market-data-api
34
     environment:
34
     environment:
35
       - NODE_ENV=${NODE_ENV:-development}
35
       - NODE_ENV=${NODE_ENV:-development}
36
-      - PORT=${PORT:-3000}
36
+      - PORT=${PORT:-3001}
37
       - DB_HOST=db
37
       - DB_HOST=db
38
       - DB_PORT=5432
38
       - DB_PORT=5432
39
       - DB_NAME=${DB_NAME:-financial_data}
39
       - DB_NAME=${DB_NAME:-financial_data}
@@ -50,7 +50,7 @@ services:
50
       - ./models:/app/models
50
       - ./models:/app/models
51
       - ./logs:/app/logs
51
       - ./logs:/app/logs
52
     ports:
52
     ports:
53
-      - "${PORT:-3000}:3000"
53
+      - "${PORT:-3001}:3001"
54
     depends_on:
54
     depends_on:
55
       db:
55
       db:
56
         condition: service_healthy
56
         condition: service_healthy
@@ -59,7 +59,7 @@ services:
59
     restart: unless-stopped
59
     restart: unless-stopped
60
     # Override entrypoint for development to use nodemon with live reload
60
     # Override entrypoint for development to use nodemon with live reload
61
     entrypoint: /bin/sh
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
   # Nginx Reverse Proxy
64
   # Nginx Reverse Proxy
65
   nginx:
65
   nginx:

+ 17 - 0
docker/entrypoint.sh

@@ -30,6 +30,23 @@ else
30
     fi
30
     fi
31
 fi
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
 # Run database migrations
50
 # Run database migrations
34
 echo "Running database migrations..."
51
 echo "Running database migrations..."
35
 npx sequelize-cli db:migrate || {
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
 /** @type {import('sequelize-cli').Migration} */
3
 /** @type {import('sequelize-cli').Migration} */
4
 module.exports = {
4
 module.exports = {
5
   async up (queryInterface, Sequelize) {
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
   async down (queryInterface, Sequelize) {
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
 /** @type {import('sequelize-cli').Migration} */
3
 /** @type {import('sequelize-cli').Migration} */
4
 module.exports = {
4
 module.exports = {
5
   async up (queryInterface, Sequelize) {
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
   async down (queryInterface, Sequelize) {
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
 /** @type {import('sequelize-cli').Migration} */
3
 /** @type {import('sequelize-cli').Migration} */
4
 module.exports = {
4
 module.exports = {
5
   async up (queryInterface, Sequelize) {
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
     // Rename table from candles_1h to candles
17
     // Rename table from candles_1h to candles
7
     await queryInterface.renameTable('candles_1h', 'candles');
18
     await queryInterface.renameTable('candles_1h', 'candles');
8
 
19
 

+ 2 - 2
nginx/nginx.conf

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