Selaa lähdekoodia

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 8 kuukautta sitten
vanhempi
sitoutus
fe44f0b9b7

+ 4 - 2
Dockerfile

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

+ 12 - 9
config/config.json

@@ -1,26 +1,29 @@
 {
   "development": {
     "username": "postgres",
-    "password": "mqldev@123",
+    "password": "postgres",
     "database": "financial_data",
-    "host": "127.0.0.1",
+    "host": "db",
     "port": 5432,
-    "dialect": "postgres"
+    "dialect": "postgres",
+    "use_env_variable": false
   },
   "test": {
     "username": "postgres",
-    "password": "mqldev@123",
+    "password": "postgres",
     "database": "financial_data",
-    "host": "127.0.0.1",
+    "host": "db",
     "port": 5432,
-    "dialect": "postgres"
+    "dialect": "postgres",
+    "use_env_variable": false
   },
   "production": {
     "username": "postgres",
-    "password": "mqldev@123",
+    "password": "postgres",
     "database": "financial_data",
-    "host": "127.0.0.1",
+    "host": "db",
     "port": 5432,
-    "dialect": "postgres"
+    "dialect": "postgres",
+    "use_env_variable": false
   }
 }

+ 3 - 3
docker-compose.yml

@@ -33,7 +33,7 @@ services:
     container_name: market-data-api
     environment:
       - NODE_ENV=${NODE_ENV:-development}
-      - PORT=${PORT:-3000}
+      - PORT=${PORT:-3001}
       - DB_HOST=db
       - DB_PORT=5432
       - DB_NAME=${DB_NAME:-financial_data}
@@ -50,7 +50,7 @@ services:
       - ./models:/app/models
       - ./logs:/app/logs
     ports:
-      - "${PORT:-3000}:3000"
+      - "${PORT:-3001}:3001"
     depends_on:
       db:
         condition: service_healthy
@@ -59,7 +59,7 @@ services:
     restart: unless-stopped
     # Override entrypoint for development to use nodemon with live reload
     entrypoint: /bin/sh
-    command: -c "/usr/local/bin/wait-for-db.sh && npx sequelize-cli db:migrate && npm install -g nodemon && nodemon src/server.js"
+    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"
 
   # Nginx Reverse Proxy
   nginx:

+ 17 - 0
docker/entrypoint.sh

@@ -30,6 +30,23 @@ else
     fi
 fi
 
+# Sync models first to create tables (if they don't exist)
+echo "Syncing database models..."
+node -e "
+const { sequelize } = require('./src/models');
+sequelize.sync({ alter: true, force: false })
+  .then(() => {
+    console.log('Database models synced successfully.');
+    process.exit(0);
+  })
+  .catch((error) => {
+    console.error('Error syncing models:', error);
+    process.exit(1);
+  });
+" || {
+    echo "WARNING: Model sync failed, continuing with migrations..."
+}
+
 # Run database migrations
 echo "Running database migrations..."
 npx sequelize-cli db:migrate || {

+ 13 - 0
docker/sync-models.js

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

+ 31 - 6
migrations/20251016210526-add_unique_constraint_candles.js

@@ -3,14 +3,39 @@
 /** @type {import('sequelize-cli').Migration} */
 module.exports = {
   async up (queryInterface, Sequelize) {
-    await queryInterface.addConstraint('candles_1h', {
-      fields: ['symbol_id', 'open_time'],
-      type: 'unique',
-      name: 'unique_symbol_open_time'
-    });
+    // Check if table exists before adding constraint
+    const tableExists = await queryInterface.sequelize.query(
+      "SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'candles_1h');",
+      { type: Sequelize.QueryTypes.SELECT }
+    );
+    
+    if (tableExists && tableExists[0] && tableExists[0].exists) {
+      // Check if constraint already exists
+      const constraintExists = await queryInterface.sequelize.query(
+        "SELECT EXISTS (SELECT FROM pg_constraint WHERE conname = 'unique_symbol_open_time');",
+        { type: Sequelize.QueryTypes.SELECT }
+      );
+      
+      if (!constraintExists || !constraintExists[0] || !constraintExists[0].exists) {
+        await queryInterface.addConstraint('candles_1h', {
+          fields: ['symbol_id', 'open_time'],
+          type: 'unique',
+          name: 'unique_symbol_open_time'
+        });
+      }
+    } else {
+      console.log('Table candles_1h does not exist, skipping constraint addition. Tables will be created by model sync.');
+    }
   },
 
   async down (queryInterface, Sequelize) {
-    await queryInterface.removeConstraint('candles_1h', 'unique_symbol_open_time');
+    const tableExists = await queryInterface.sequelize.query(
+      "SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'candles_1h');",
+      { type: Sequelize.QueryTypes.SELECT }
+    );
+    
+    if (tableExists && tableExists[0] && tableExists[0].exists) {
+      await queryInterface.removeConstraint('candles_1h', 'unique_symbol_open_time');
+    }
   }
 };

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

@@ -3,14 +3,31 @@
 /** @type {import('sequelize-cli').Migration} */
 module.exports = {
   async up (queryInterface, Sequelize) {
-    // Drop the existing CHECK constraint if it exists and add a new one with 'index'
-    await queryInterface.sequelize.query("ALTER TABLE symbols DROP CONSTRAINT IF EXISTS symbols_instrument_type_check;");
-    await queryInterface.sequelize.query("ALTER TABLE symbols ADD CONSTRAINT symbols_instrument_type_check CHECK (instrument_type IN ('crypto', 'stock', 'forex', 'commodity', 'index'));");
+    // Check if table exists
+    const tableExists = await queryInterface.sequelize.query(
+      "SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'symbols');",
+      { type: Sequelize.QueryTypes.SELECT }
+    );
+    
+    if (tableExists && tableExists[0] && tableExists[0].exists) {
+      // Drop the existing CHECK constraint if it exists and add a new one with 'index'
+      await queryInterface.sequelize.query("ALTER TABLE symbols DROP CONSTRAINT IF EXISTS symbols_instrument_type_check;");
+      await queryInterface.sequelize.query("ALTER TABLE symbols ADD CONSTRAINT symbols_instrument_type_check CHECK (instrument_type IN ('crypto', 'stock', 'forex', 'commodity', 'index'));");
+    } else {
+      console.log('Table symbols does not exist, skipping constraint update. Tables will be created by model sync.');
+    }
   },
 
   async down (queryInterface, Sequelize) {
-    // Revert the CHECK constraint without 'index'
-    await queryInterface.sequelize.query("ALTER TABLE symbols DROP CONSTRAINT symbols_instrument_type_check;");
-    await queryInterface.sequelize.query("ALTER TABLE symbols ADD CONSTRAINT symbols_instrument_type_check CHECK (instrument_type IN ('crypto', 'stock', 'forex', 'commodity'));");
+    const tableExists = await queryInterface.sequelize.query(
+      "SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'symbols');",
+      { type: Sequelize.QueryTypes.SELECT }
+    );
+    
+    if (tableExists && tableExists[0] && tableExists[0].exists) {
+      // Revert the CHECK constraint without 'index'
+      await queryInterface.sequelize.query("ALTER TABLE symbols DROP CONSTRAINT IF EXISTS symbols_instrument_type_check;");
+      await queryInterface.sequelize.query("ALTER TABLE symbols ADD CONSTRAINT symbols_instrument_type_check CHECK (instrument_type IN ('crypto', 'stock', 'forex', 'commodity'));");
+    }
   }
 };

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

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

+ 2 - 2
nginx/nginx.conf

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