Skip to content

Deployment

Overview

Deploying Node.js applications to production requires careful consideration of environment configuration, process management, monitoring, and scaling strategies. This chapter covers deployment best practices, containerization, cloud platforms, and production optimization.

Production Environment Setup

Environment Configuration

javascript
// config/production.js
module.exports = {
  // Server configuration
  server: {
    port: process.env.PORT || 8080,
    host: process.env.HOST || '0.0.0.0',
    env: 'production'
  },

  // Database configuration
  database: {
    url: process.env.DATABASE_URL,
    poolSize: parseInt(process.env.DB_POOL_SIZE) || 20,
    ssl: process.env.DB_SSL === 'true',
    options: {
      useNewUrlParser: true,
      useUnifiedTopology: true,
      maxPoolSize: 20,
      serverSelectionTimeoutMS: 5000,
      socketTimeoutMS: 45000,
    }
  },

  // Redis configuration
  redis: {
    url: process.env.REDIS_URL,
    maxRetriesPerRequest: 3,
    retryDelayOnFailover: 100,
    enableOfflineQueue: false
  },

  // Security configuration
  security: {
    jwtSecret: process.env.JWT_SECRET,
    bcryptRounds: 12,
    rateLimitWindowMs: 15 * 60 * 1000,
    rateLimitMax: 100,
    corsOrigin: process.env.CORS_ORIGIN?.split(',') || false
  },

  // Logging configuration
  logging: {
    level: process.env.LOG_LEVEL || 'info',
    format: 'json',
    transports: {
      file: {
        enabled: true,
        filename: '/var/log/app/app.log',
        maxsize: 10485760, // 10MB
        maxFiles: 5
      },
      console: {
        enabled: process.env.CONSOLE_LOGGING === 'true'
      }
    }
  },

  // Monitoring configuration
  monitoring: {
    enabled: true,
    metricsPort: process.env.METRICS_PORT || 9090,
    healthCheckPath: '/health',
    readinessCheckPath: '/ready'
  }
};

Process Management with PM2

javascript
// ecosystem.config.js
module.exports = {
  apps: [{
    name: 'node-app',
    script: './src/server.js',
    instances: 'max', // Use all CPU cores
    exec_mode: 'cluster',
    
    // Environment variables
    env: {
      NODE_ENV: 'production',
      PORT: 8080
    },
    
    // Logging
    log_file: '/var/log/pm2/app.log',
    out_file: '/var/log/pm2/app-out.log',
    error_file: '/var/log/pm2/app-error.log',
    log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
    
    // Process management
    max_memory_restart: '1G',
    restart_delay: 4000,
    max_restarts: 10,
    min_uptime: '10s',
    
    // Monitoring
    pmx: true,
    
    // Advanced features
    watch: false,
    ignore_watch: ['node_modules', 'logs'],
    
    // Graceful shutdown
    kill_timeout: 5000,
    listen_timeout: 3000,
    
    // Auto restart on file changes (development only)
    watch_options: {
      followSymlinks: false
    }
  }],

  deploy: {
    production: {
      user: 'deploy',
      host: ['server1.example.com', 'server2.example.com'],
      ref: 'origin/main',
      repo: 'git@github.com:username/repo.git',
      path: '/var/www/production',
      'pre-deploy-local': '',
      'post-deploy': 'npm install && pm2 reload ecosystem.config.js --env production',
      'pre-setup': ''
    }
  }
};

Health Checks and Monitoring

javascript
// src/health.js
const express = require('express');
const mongoose = require('mongoose');
const redis = require('redis');

class HealthChecker {
  constructor(app) {
    this.app = app;
    this.setupHealthRoutes();
  }

  setupHealthRoutes() {
    // Basic health check
    this.app.get('/health', (req, res) => {
      res.status(200).json({
        status: 'healthy',
        timestamp: new Date().toISOString(),
        uptime: process.uptime(),
        version: process.env.npm_package_version || '1.0.0'
      });
    });

    // Detailed readiness check
    this.app.get('/ready', async (req, res) => {
      const checks = await this.performReadinessChecks();
      const isReady = checks.every(check => check.status === 'healthy');
      
      res.status(isReady ? 200 : 503).json({
        status: isReady ? 'ready' : 'not ready',
        checks,
        timestamp: new Date().toISOString()
      });
    });

    // Liveness probe
    this.app.get('/live', (req, res) => {
      res.status(200).json({
        status: 'alive',
        pid: process.pid,
        memory: process.memoryUsage(),
        timestamp: new Date().toISOString()
      });
    });
  }

  async performReadinessChecks() {
    const checks = [];

    // Database connectivity
    try {
      await mongoose.connection.db.admin().ping();
      checks.push({
        name: 'database',
        status: 'healthy',
        responseTime: Date.now()
      });
    } catch (error) {
      checks.push({
        name: 'database',
        status: 'unhealthy',
        error: error.message
      });
    }

    // Redis connectivity
    try {
      const redisClient = redis.createClient(process.env.REDIS_URL);
      await redisClient.ping();
      await redisClient.quit();
      checks.push({
        name: 'redis',
        status: 'healthy'
      });
    } catch (error) {
      checks.push({
        name: 'redis',
        status: 'unhealthy',
        error: error.message
      });
    }

    // Memory usage check
    const memUsage = process.memoryUsage();
    const memoryHealthy = memUsage.heapUsed < (1024 * 1024 * 1024); // 1GB threshold
    
    checks.push({
      name: 'memory',
      status: memoryHealthy ? 'healthy' : 'warning',
      heapUsed: memUsage.heapUsed,
      heapTotal: memUsage.heapTotal
    });

    return checks;
  }
}

module.exports = HealthChecker;

Containerization with Docker

Dockerfile

dockerfile
# Multi-stage build for production optimization
FROM node:18-alpine AS builder

# Set working directory
WORKDIR /app

# Copy package files
COPY package*.json ./

# Install dependencies
RUN npm ci --only=production && npm cache clean --force

# Copy source code
COPY . .

# Build application (if needed)
RUN npm run build || true

# Production stage
FROM node:18-alpine AS production

# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001

# Set working directory
WORKDIR /app

# Copy built application from builder stage
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nodejs:nodejs /app/package*.json ./
COPY --from=builder --chown=nodejs:nodejs /app/src ./src

# Create logs directory
RUN mkdir -p /var/log/app && chown nodejs:nodejs /var/log/app

# Switch to non-root user
USER nodejs

# Expose port
EXPOSE 8080

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD node healthcheck.js

# Start application
CMD ["node", "src/server.js"]

Docker Compose for Development

yaml
# docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "3000:8080"
    environment:
      - NODE_ENV=development
      - DATABASE_URL=mongodb://mongo:27017/myapp
      - REDIS_URL=redis://redis:6379
    depends_on:
      - mongo
      - redis
    volumes:
      - ./src:/app/src
      - ./logs:/var/log/app
    restart: unless-stopped

  mongo:
    image: mongo:5
    ports:
      - "27017:27017"
    environment:
      - MONGO_INITDB_ROOT_USERNAME=admin
      - MONGO_INITDB_ROOT_PASSWORD=password
    volumes:
      - mongo_data:/data/db
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    restart: unless-stopped

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      - ./ssl:/etc/nginx/ssl
    depends_on:
      - app
    restart: unless-stopped

volumes:
  mongo_data:
  redis_data:

Production Docker Compose

yaml
# docker-compose.prod.yml
version: '3.8'

services:
  app:
    image: myapp:latest
    deploy:
      replicas: 3
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 3
      resources:
        limits:
          cpus: '1'
          memory: 1G
        reservations:
          cpus: '0.5'
          memory: 512M
    environment:
      - NODE_ENV=production
      - DATABASE_URL=${DATABASE_URL}
      - REDIS_URL=${REDIS_URL}
      - JWT_SECRET=${JWT_SECRET}
    networks:
      - app-network
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.prod.conf:/etc/nginx/nginx.conf
      - ./ssl:/etc/nginx/ssl
      - ./static:/var/www/static
    networks:
      - app-network
    depends_on:
      - app

networks:
  app-network:
    driver: overlay

Cloud Platform Deployment

AWS Deployment with Elastic Beanstalk

json
// .ebextensions/01-node-settings.config
{
  "option_settings": [
    {
      "namespace": "aws:elasticbeanstalk:container:nodejs",
      "option_name": "NodeCommand",
      "value": "npm start"
    },
    {
      "namespace": "aws:elasticbeanstalk:container:nodejs",
      "option_name": "NodeVersion",
      "value": "18.17.0"
    },
    {
      "namespace": "aws:elasticbeanstalk:application:environment",
      "option_name": "NODE_ENV",
      "value": "production"
    }
  ]
}

Kubernetes Deployment

yaml
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: node-app
  labels:
    app: node-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: node-app
  template:
    metadata:
      labels:
        app: node-app
    spec:
      containers:
      - name: node-app
        image: myapp:latest
        ports:
        - containerPort: 8080
        env:
        - name: NODE_ENV
          value: "production"
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: app-secrets
              key: database-url
        - name: REDIS_URL
          valueFrom:
            secretKeyRef:
              name: app-secrets
              key: redis-url
        resources:
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "500m"
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /ready
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 5

---
apiVersion: v1
kind: Service
metadata:
  name: node-app-service
spec:
  selector:
    app: node-app
  ports:
  - protocol: TCP
    port: 80
    targetPort: 8080
  type: LoadBalancer

CI/CD Pipeline

GitHub Actions Workflow

yaml
# .github/workflows/deploy.yml
name: Deploy to Production

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    
    services:
      mongodb:
        image: mongo:5
        ports:
          - 27017:27017
      redis:
        image: redis:7
        ports:
          - 6379:6379

    steps:
    - uses: actions/checkout@v3
    
    - name: Setup Node.js
      uses: actions/setup-node@v3
      with:
        node-version: '18'
        cache: 'npm'
    
    - name: Install dependencies
      run: npm ci
    
    - name: Run linting
      run: npm run lint
    
    - name: Run tests
      run: npm test
      env:
        NODE_ENV: test
        DATABASE_URL: mongodb://localhost:27017/test
        REDIS_URL: redis://localhost:6379
    
    - name: Run security audit
      run: npm audit --audit-level high

  build:
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v2
    
    - name: Login to Container Registry
      uses: docker/login-action@v2
      with:
        registry: ghcr.io
        username: ${{ github.actor }}
        password: ${{ secrets.GITHUB_TOKEN }}
    
    - name: Build and push Docker image
      uses: docker/build-push-action@v4
      with:
        context: .
        push: true
        tags: |
          ghcr.io/${{ github.repository }}:latest
          ghcr.io/${{ github.repository }}:${{ github.sha }}
        cache-from: type=gha
        cache-to: type=gha,mode=max

  deploy:
    needs: build
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Deploy to production
      run: |
        echo "Deploying to production..."
        # Add your deployment commands here
        # e.g., kubectl apply, helm upgrade, etc.

Performance Optimization

Production Optimizations

javascript
// src/optimizations.js
const compression = require('compression');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');

function applyProductionOptimizations(app) {
  // Security headers
  app.use(helmet({
    contentSecurityPolicy: {
      directives: {
        defaultSrc: ["'self'"],
        styleSrc: ["'self'", "'unsafe-inline'"],
        scriptSrc: ["'self'"],
        imgSrc: ["'self'", "data:", "https:"]
      }
    },
    hsts: {
      maxAge: 31536000,
      includeSubDomains: true,
      preload: true
    }
  }));

  // Compression
  app.use(compression({
    filter: (req, res) => {
      if (req.headers['x-no-compression']) {
        return false;
      }
      return compression.filter(req, res);
    },
    level: 6,
    threshold: 1024
  }));

  // Rate limiting
  const limiter = rateLimit({
    windowMs: 15 * 60 * 1000, // 15 minutes
    max: 100, // limit each IP to 100 requests per windowMs
    message: {
      error: 'Too many requests from this IP',
      retryAfter: Math.ceil(15 * 60 * 1000 / 1000)
    },
    standardHeaders: true,
    legacyHeaders: false
  });
  
  app.use('/api/', limiter);

  // Static file caching
  app.use('/static', express.static('public', {
    maxAge: '1y',
    etag: true,
    lastModified: true
  }));

  // Request timeout
  app.use((req, res, next) => {
    req.setTimeout(30000, () => {
      res.status(408).json({ error: 'Request timeout' });
    });
    next();
  });

  return app;
}

module.exports = { applyProductionOptimizations };

Monitoring and Logging

Application Metrics

javascript
// src/metrics.js
const prometheus = require('prom-client');

class MetricsCollector {
  constructor() {
    // Create a Registry
    this.register = new prometheus.Registry();
    
    // Add default metrics
    prometheus.collectDefaultMetrics({ register: this.register });
    
    // Custom metrics
    this.httpRequestDuration = new prometheus.Histogram({
      name: 'http_request_duration_seconds',
      help: 'Duration of HTTP requests in seconds',
      labelNames: ['method', 'route', 'status_code'],
      buckets: [0.1, 0.5, 1, 2, 5]
    });
    
    this.httpRequestTotal = new prometheus.Counter({
      name: 'http_requests_total',
      help: 'Total number of HTTP requests',
      labelNames: ['method', 'route', 'status_code']
    });
    
    this.activeConnections = new prometheus.Gauge({
      name: 'active_connections',
      help: 'Number of active connections'
    });
    
    // Register custom metrics
    this.register.registerMetric(this.httpRequestDuration);
    this.register.registerMetric(this.httpRequestTotal);
    this.register.registerMetric(this.activeConnections);
  }
  
  middleware() {
    return (req, res, next) => {
      const start = Date.now();
      
      res.on('finish', () => {
        const duration = (Date.now() - start) / 1000;
        const route = req.route?.path || req.path;
        
        this.httpRequestDuration
          .labels(req.method, route, res.statusCode)
          .observe(duration);
          
        this.httpRequestTotal
          .labels(req.method, route, res.statusCode)
          .inc();
      });
      
      next();
    };
  }
  
  getMetrics() {
    return this.register.metrics();
  }
}

module.exports = MetricsCollector;

Next Steps

In the final chapter, we'll explore learning resources and advanced topics to continue your Node.js journey.

Key Takeaways

  • Production configuration requires environment-specific settings
  • Process managers like PM2 provide clustering and monitoring
  • Containerization ensures consistent deployment environments
  • Health checks enable proper load balancer integration
  • CI/CD pipelines automate testing and deployment
  • Monitoring and metrics help identify performance issues
  • Security headers and rate limiting protect against attacks
  • Proper logging facilitates debugging and auditing

Content is for learning and research only.