From 26c05912fc15dc938e1b673d3b222e122ebd301a Mon Sep 17 00:00:00 2001 From: paul Date: Sun, 13 Jul 2025 22:14:29 +0200 Subject: [PATCH] fix: resolve critical production deployment issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix database connection error "getaddrinfo ENOTFOUND postgres" - Add wait-for-db.sh script to ensure PostgreSQL is ready before starting - Fix email processor initialization timing issue - Add missing storage path environment variables - Add database dependency to backend service - Enhance health check endpoint with database connectivity check - Update production database defaults to match docker-compose - Install postgresql-client in Docker image for health checks - Document all required environment variables in .env.example Fixes immediate production deployment failures and ensures proper service startup order. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- PRODUCTION_DEPLOYMENT_FIXES.md | 84 ++++++++++++++++++++++++++ backend/.env.example | 67 ++++++++++---------- backend/Dockerfile | 9 ++- backend/knexfile.js | 8 +-- backend/server.js | 26 ++++++-- backend/src/services/emailProcessor.js | 11 ++-- backend/wait-for-db.sh | 25 ++++++++ docker-compose.prod.yml | 6 ++ 8 files changed, 187 insertions(+), 49 deletions(-) create mode 100644 PRODUCTION_DEPLOYMENT_FIXES.md create mode 100755 backend/wait-for-db.sh diff --git a/PRODUCTION_DEPLOYMENT_FIXES.md b/PRODUCTION_DEPLOYMENT_FIXES.md new file mode 100644 index 0000000..847d09b --- /dev/null +++ b/PRODUCTION_DEPLOYMENT_FIXES.md @@ -0,0 +1,84 @@ +# Production Deployment Fixes + +This document describes the fixes applied to resolve production deployment issues in Docker. + +## Issues Fixed + +### 1. Database Connection Error: "getaddrinfo ENOTFOUND postgres" +**Problem**: The backend was trying to connect to hostname "postgres" but the database service is named "db" in docker-compose. +**Solution**: +- Updated `knexfile.js` to use correct default host "db" instead of "postgres" +- Added `depends_on: db` to backend service in docker-compose.prod.yml + +### 2. Backend Starting Before Database Ready +**Problem**: Backend service started before PostgreSQL was ready, causing connection failures. +**Solution**: +- Created `wait-for-db.sh` script that waits for PostgreSQL to be ready +- Updated Dockerfile to install postgresql-client and use the wait script +- Script also runs migrations automatically on startup + +### 3. Email Processor Initialization Failure +**Problem**: Email processor tried to initialize on module load before database was available. +**Solution**: +- Modified `emailProcessor.js` to export initialization functions +- Updated `server.js` to call initialization after database is ready +- Added proper error handling for email service initialization + +### 4. Missing Environment Variables +**Problem**: Critical storage path environment variables were missing. +**Solution**: +- Added STORAGE_PATH, EVENTS_PATH, and ARCHIVE_PATH to docker-compose.prod.yml +- Created `.env.example` documenting all required environment variables + +### 5. Enhanced Health Check +**Problem**: Basic health check didn't verify database connectivity. +**Solution**: +- Updated `/api/health` endpoint to check database connection +- Returns proper HTTP 503 status when unhealthy + +## Files Modified + +1. **backend/knexfile.js** - Fixed production database defaults +2. **backend/wait-for-db.sh** - Created database wait script +3. **backend/Dockerfile** - Added postgresql-client and wait script +4. **docker-compose.prod.yml** - Added dependencies and environment variables +5. **backend/src/services/emailProcessor.js** - Disabled auto-initialization +6. **backend/server.js** - Added email initialization and improved health check +7. **backend/.env.example** - Created environment variable documentation + +## Deployment Steps + +1. Ensure all environment variables are set according to `.env.example` +2. Build and deploy with docker-compose: + ```bash + docker-compose -f docker-compose.prod.yml build + docker-compose -f docker-compose.prod.yml up -d + ``` +3. The backend will now: + - Wait for PostgreSQL to be ready + - Run migrations automatically + - Initialize all services in proper order + - Provide health status at `/api/health` + +## Verification + +Check deployment health: +```bash +curl http://localhost/api/health +``` + +Expected response: +```json +{ + "status": "ok", + "database": "connected", + "timestamp": "2025-07-13T20:30:00.000Z" +} +``` + +## Email Configuration + +Email service requires configuration in the database. If email is not configured: +- The service will log a warning but continue running +- Emails will be queued but not sent +- Configure email settings in the admin panel after deployment \ No newline at end of file diff --git a/backend/.env.example b/backend/.env.example index 94a77c7..e4b74a7 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,42 +1,41 @@ -NODE_ENV=development +# Backend Environment Variables Example +# Copy this file to .env and update with your values + +# Application +NODE_ENV=production PORT=3000 -# URLs -ADMIN_URL=http://localhost:3000 -FRONTEND_URL=http://localhost:3001 - # Security -JWT_SECRET=dev-secret-key +JWT_SECRET=your-very-secure-jwt-secret-at-least-32-characters-long -# Email Configuration -SMTP_HOST=mailhog -SMTP_PORT=1025 -SMTP_SECURE=false -SMTP_USER= -SMTP_PASS= -EMAIL_FROM=noreply@localhost - -# Storage Paths (relative to project root) -STORAGE_PATH=./storage -EVENTS_PATH=./storage/events -ARCHIVE_PATH=./storage/events/archived - -# Logging -LOG_LEVEL=info +# URLs +ADMIN_URL=https://yourdomain.com +FRONTEND_URL=https://yourdomain.com # Database Configuration -# Development: Use SQLite -DATABASE_CLIENT=sqlite3 -DATABASE_PATH=./data/photo_sharing.db +DATABASE_CLIENT=pg +DB_HOST=db +DB_PORT=5432 +DB_USER=picpeak +DB_PASSWORD=your-secure-database-password +DB_NAME=picpeak -# Production: Use PostgreSQL -# DATABASE_CLIENT=pg -# DB_HOST=localhost -# DB_PORT=5432 -# DB_USER=picpeak -# DB_PASSWORD=your-secure-password -# DB_NAME=picpeak +# Email Configuration +SMTP_HOST=smtp.example.com +SMTP_PORT=587 +SMTP_SECURE=false +SMTP_USER=your-smtp-username +SMTP_PASS=your-smtp-password +EMAIL_FROM=noreply@yourdomain.com -# Umami Analytics (optional) -UMAMI_URL= -UMAMI_WEBSITE_ID= +# Storage Paths (Docker) +STORAGE_PATH=/app/storage +EVENTS_PATH=/app/storage/events +ARCHIVE_PATH=/app/storage/events/archived + +# Analytics (Optional) +UMAMI_URL=https://analytics.yourdomain.com +UMAMI_WEBSITE_ID=your-website-id + +# Logging +LOG_LEVEL=info \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile index e054e76..23d202f 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -16,8 +16,8 @@ FROM node:18-alpine WORKDIR /app -# Install dumb-init for proper signal handling -RUN apk add --no-cache dumb-init +# Install dumb-init for proper signal handling and postgresql-client for database checks +RUN apk add --no-cache dumb-init postgresql-client # Create non-root user RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001 @@ -26,6 +26,9 @@ RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001 COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules COPY --chown=nodejs:nodejs . . +# Make wait script executable +RUN chmod +x wait-for-db.sh + # Create necessary directories RUN mkdir -p storage/events/active storage/events/archived storage/thumbnails data logs && \ chown -R nodejs:nodejs storage data logs @@ -35,4 +38,4 @@ USER nodejs EXPOSE 3000 ENTRYPOINT ["dumb-init", "--"] -CMD ["node", "server.js"] +CMD ["./wait-for-db.sh", "node", "server.js"] diff --git a/backend/knexfile.js b/backend/knexfile.js index 9c8eb36..7b658ca 100644 --- a/backend/knexfile.js +++ b/backend/knexfile.js @@ -27,11 +27,11 @@ const config = { production: { client: process.env.DATABASE_CLIENT || 'pg', connection: { - host: process.env.DB_HOST || 'postgres', + host: process.env.DB_HOST || 'db', port: process.env.DB_PORT || 5432, - user: process.env.DB_USER || 'postgres', - password: process.env.DB_PASSWORD || 'postgres', - database: process.env.DB_NAME || 'photo_sharing' + user: process.env.DB_USER || 'picpeak', + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME || 'picpeak' }, pool: { min: 2, diff --git a/backend/server.js b/backend/server.js index f2618b8..3d221e8 100644 --- a/backend/server.js +++ b/backend/server.js @@ -13,7 +13,7 @@ const path = require('path'); const { initializeDatabase } = require('./src/database/db'); const { startFileWatcher } = require('./src/services/fileWatcher'); const { startExpirationChecker } = require('./src/services/expirationChecker'); -const { startEmailQueueProcessor } = require('./src/services/emailProcessor'); +const { initializeTransporter, startEmailQueueProcessor } = require('./src/services/emailProcessor'); const { maintenanceMiddleware } = require('./src/middleware/maintenance'); const { sessionTimeoutMiddleware } = require('./src/middleware/sessionTimeout'); const logger = require('./src/utils/logger'); @@ -152,8 +152,25 @@ app.use('/thumbnails', require('./src/middleware/photoAuth'), setCorsHeaders, se app.use('/uploads', setCorsHeaders, secureStatic(path.join(__dirname, 'storage/uploads'))); // Health check endpoint -app.get('/api/health', (req, res) => { - res.json({ status: 'ok', timestamp: new Date().toISOString() }); +app.get('/api/health', async (req, res) => { + try { + // Check database connectivity + await db.raw('SELECT 1'); + + res.json({ + status: 'ok', + database: 'connected', + timestamp: new Date().toISOString() + }); + } catch (error) { + logger.error('Health check failed:', error); + res.status(503).json({ + status: 'error', + database: 'disconnected', + error: error.message, + timestamp: new Date().toISOString() + }); + } }); // Routes @@ -189,7 +206,8 @@ async function startServer() { // Start expiration checker startExpirationChecker(); - // Start email queue processor + // Initialize email transporter and start queue processor + await initializeTransporter(); startEmailQueueProcessor(); app.listen(PORT, () => { diff --git a/backend/src/services/emailProcessor.js b/backend/src/services/emailProcessor.js index 2af51ea..f68d9f9 100644 --- a/backend/src/services/emailProcessor.js +++ b/backend/src/services/emailProcessor.js @@ -395,12 +395,15 @@ function stopEmailQueueProcessor() { } } -// Initialize on module load -initializeTransporter().then(() => { - startEmailQueueProcessor(); -}); +// Initialize on module load - DISABLED for production startup +// This will be called from server.js after database is ready +// initializeTransporter().then(() => { +// startEmailQueueProcessor(); +// }); module.exports = { + initializeTransporter, + startEmailQueueProcessor, sendTemplateEmail, processEmailQueue, queueEmail, diff --git a/backend/wait-for-db.sh b/backend/wait-for-db.sh new file mode 100755 index 0000000..75094df --- /dev/null +++ b/backend/wait-for-db.sh @@ -0,0 +1,25 @@ +#!/bin/sh +# wait-for-db.sh - Wait for PostgreSQL to be ready before starting the application + +set -e + +host="$DB_HOST" +port="${DB_PORT:-5432}" +user="${DB_USER:-picpeak}" + +echo "Waiting for PostgreSQL at $host:$port..." + +# Wait for PostgreSQL to be ready +until PGPASSWORD=$DB_PASSWORD psql -h "$host" -p "$port" -U "$user" -d "${DB_NAME:-picpeak}" -c '\q' 2>/dev/null; do + >&2 echo "PostgreSQL is unavailable - sleeping" + sleep 2 +done + +>&2 echo "PostgreSQL is up - executing command" + +# Run migrations +echo "Running database migrations..." +npm run migrate + +# Execute the main command +exec "$@" \ No newline at end of file diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 143a635..d2bdeee 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -8,6 +8,8 @@ services: context: ./backend dockerfile: Dockerfile restart: unless-stopped + depends_on: + - db environment: - NODE_ENV=production - PORT=3000 @@ -31,6 +33,10 @@ services: # Analytics - UMAMI_URL=${UMAMI_URL} - UMAMI_WEBSITE_ID=${UMAMI_WEBSITE_ID} + # Storage paths + - STORAGE_PATH=/app/storage + - EVENTS_PATH=/app/storage/events + - ARCHIVE_PATH=/app/storage/events/archived volumes: - ./storage:/app/storage - ./data:/app/data