Node.js Intermediate

Node.js Deployment and Production: Scaling Your Applications

CodingerWeb
CodingerWeb
25 views 95 min read

Preparing for Production

Deploying Node.js applications to production requires careful consideration of performance, security, monitoring, and scalability. This lesson covers best practices for production deployment.

Environment Configuration

Use environment variables to manage configuration across different environments:

# Install dotenv for development
npm install dotenv
// config/config.js
require("dotenv").config();

const config = {
    development: {
        port: process.env.PORT || 3000,
        database: {
            host: process.env.DB_HOST || "localhost",
            user: process.env.DB_USER || "root",
            password: process.env.DB_PASSWORD || "",
            name: process.env.DB_NAME || "myapp_dev"
        },
        jwt: {
            secret: process.env.JWT_SECRET || "dev-secret",
            expiresIn: process.env.JWT_EXPIRES_IN || "7d"
        },
        redis: {
            host: process.env.REDIS_HOST || "localhost",
            port: process.env.REDIS_PORT || 6379
        }
    },
    production: {
        port: process.env.PORT || 8080,
        database: {
            host: process.env.DB_HOST,
            user: process.env.DB_USER,
            password: process.env.DB_PASSWORD,
            name: process.env.DB_NAME,
            ssl: true
        },
        jwt: {
            secret: process.env.JWT_SECRET,
            expiresIn: process.env.JWT_EXPIRES_IN || "1d"
        },
        redis: {
            host: process.env.REDIS_HOST,
            port: process.env.REDIS_PORT,
            password: process.env.REDIS_PASSWORD
        }
    }
};

const environment = process.env.NODE_ENV || "development";
module.exports = config[environment];
# .env (for development)
NODE_ENV=development
PORT=3000
DB_HOST=localhost
DB_USER=root
DB_PASSWORD=password
DB_NAME=myapp_dev
JWT_SECRET=your-super-secret-jwt-key
REDIS_HOST=localhost
REDIS_PORT=6379

Production Security

# Install security packages
npm install helmet cors express-rate-limit express-mongo-sanitize xss-clean hpp
// middleware/security.js
const helmet = require("helmet");
const cors = require("cors");
const rateLimit = require("express-rate-limit");
const mongoSanitize = require("express-mongo-sanitize");
const xss = require("xss-clean");
const hpp = require("hpp");

const setupSecurity = (app) => {
    // Set security HTTP headers
    app.use(helmet());
    
    // Enable CORS
    app.use(cors({
        origin: process.env.ALLOWED_ORIGINS?.split(",") || "*",
        credentials: true
    }));
    
    // Rate limiting
    const limiter = rateLimit({
        max: 100, // limit each IP to 100 requests per windowMs
        windowMs: 15 * 60 * 1000, // 15 minutes
        message: "Too many requests from this IP, please try again later."
    });
    app.use("/api", limiter);
    
    // Stricter rate limiting for auth endpoints
    const authLimiter = rateLimit({
        max: 5,
        windowMs: 15 * 60 * 1000,
        message: "Too many authentication attempts, please try again later."
    });
    app.use("/api/auth", authLimiter);
    
    // Data sanitization against NoSQL query injection
    app.use(mongoSanitize());
    
    // Data sanitization against XSS
    app.use(xss());
    
    // Prevent parameter pollution
    app.use(hpp({
        whitelist: ["sort", "fields", "page", "limit"]
    }));
};

module.exports = setupSecurity;

Logging and Monitoring

# Install logging packages
npm install winston morgan
// utils/logger.js
const winston = require("winston");

const logger = winston.createLogger({
    level: process.env.LOG_LEVEL || "info",
    format: winston.format.combine(
        winston.format.timestamp(),
        winston.format.errors({ stack: true }),
        winston.format.json()
    ),
    defaultMeta: { service: "myapp" },
    transports: [
        // Write all logs with level `error` and below to `error.log`
        new winston.transports.File({ 
            filename: "logs/error.log", 
            level: "error" 
        }),
        // Write all logs with level `info` and below to `combined.log`
        new winston.transports.File({ 
            filename: "logs/combined.log" 
        })
    ]
});

// If we're not in production, log to the console as well
if (process.env.NODE_ENV !== "production") {
    logger.add(new winston.transports.Console({
        format: winston.format.combine(
            winston.format.colorize(),
            winston.format.simple()
        )
    }));
}

module.exports = logger;
// middleware/logging.js
const morgan = require("morgan");
const logger = require("../utils/logger");

// Custom morgan token for response time
morgan.token("response-time", (req, res) => {
    return res.getHeader("X-Response-Time");
});

// Create a write stream for morgan
const stream = {
    write: (message) => {
        logger.info(message.trim());
    }
};

const setupLogging = (app) => {
    // HTTP request logging
    if (process.env.NODE_ENV === "production") {
        app.use(morgan("combined", { stream }));
    } else {
        app.use(morgan("dev"));
    }
    
    // Response time middleware
    app.use((req, res, next) => {
        const start = Date.now();
        res.on("finish", () => {
            const duration = Date.now() - start;
            res.setHeader("X-Response-Time", `${duration}ms`);
            
            logger.info("Request processed", {
                method: req.method,
                url: req.url,
                statusCode: res.statusCode,
                responseTime: `${duration}ms`,
                userAgent: req.get("User-Agent"),
                ip: req.ip
            });
        });
        next();
    });
};

module.exports = setupLogging;

Process Management with PM2

# Install PM2 globally
npm install -g pm2
// ecosystem.config.js
module.exports = {
    apps: [{
        name: "myapp",
        script: "./app.js",
        instances: "max", // Use all available CPU cores
        exec_mode: "cluster",
        env: {
            NODE_ENV: "development",
            PORT: 3000
        },
        env_production: {
            NODE_ENV: "production",
            PORT: 8080
        },
        error_file: "./logs/err.log",
        out_file: "./logs/out.log",
        log_file: "./logs/combined.log",
        time: true,
        max_memory_restart: "1G",
        node_args: "--max-old-space-size=1024"
    }]
};
# PM2 commands
pm2 start ecosystem.config.js --env production
pm2 status
pm2 logs
pm2 monit
pm2 restart myapp
pm2 stop myapp
pm2 delete myapp

# Save PM2 configuration
pm2 save
pm2 startup

Docker Deployment

# Dockerfile
FROM node:18-alpine

# Create app directory
WORKDIR /usr/src/app

# Copy package files
COPY package*.json ./

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

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

# Copy app source
COPY --chown=nodeuser:nodejs . .

# Create logs directory
RUN mkdir -p logs && chown nodeuser:nodejs logs

# Switch to non-root user
USER nodeuser

# Expose port
EXPOSE 8080

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

# Start the application
CMD ["node", "app.js"]
# docker-compose.yml
version: "3.8"

services:
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - NODE_ENV=production
      - DB_HOST=db
      - REDIS_HOST=redis
    depends_on:
      - db
      - redis
    restart: unless-stopped
    volumes:
      - ./logs:/usr/src/app/logs
    
  db:
    image: mysql:8.0
    environment:
      - MYSQL_ROOT_PASSWORD=rootpassword
      - MYSQL_DATABASE=myapp
      - MYSQL_USER=appuser
      - MYSQL_PASSWORD=apppassword
    volumes:
      - db_data:/var/lib/mysql
    restart: unless-stopped
    
  redis:
    image: redis:7-alpine
    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:
  db_data:

Performance Optimization

// middleware/compression.js
const compression = require("compression");

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

module.exports = setupCompression;
// middleware/caching.js
const redis = require("redis");
const client = redis.createClient({
    host: process.env.REDIS_HOST,
    port: process.env.REDIS_PORT,
    password: process.env.REDIS_PASSWORD
});

const cache = (duration = 300) => {
    return async (req, res, next) => {
        if (req.method !== "GET") {
            return next();
        }
        
        const key = `cache:${req.originalUrl}`;
        
        try {
            const cached = await client.get(key);
            
            if (cached) {
                return res.json(JSON.parse(cached));
            }
            
            // Store original res.json
            const originalJson = res.json;
            
            res.json = function(data) {
                // Cache the response
                client.setex(key, duration, JSON.stringify(data));
                
                // Call original res.json
                originalJson.call(this, data);
            };
            
            next();
        } catch (err) {
            console.error("Cache error:", err);
            next();
        }
    };
};

module.exports = { cache };

Health Checks and Monitoring

// healthcheck.js
const http = require("http");

const options = {
    hostname: "localhost",
    port: process.env.PORT || 8080,
    path: "/health",
    method: "GET",
    timeout: 2000
};

const req = http.request(options, (res) => {
    if (res.statusCode === 200) {
        process.exit(0);
    } else {
        process.exit(1);
    }
});

req.on("error", () => {
    process.exit(1);
});

req.on("timeout", () => {
    req.destroy();
    process.exit(1);
});

req.end();
// routes/health.js
const express = require("express");
const router = express.Router();

router.get("/health", async (req, res) => {
    const healthcheck = {
        uptime: process.uptime(),
        message: "OK",
        timestamp: Date.now(),
        checks: {
            database: "OK",
            redis: "OK",
            memory: {
                used: process.memoryUsage().heapUsed / 1024 / 1024,
                total: process.memoryUsage().heapTotal / 1024 / 1024
            }
        }
    };
    
    try {
        // Add database health check
        // await db.ping();
        
        // Add Redis health check
        // await redis.ping();
        
        res.status(200).json(healthcheck);
    } catch (err) {
        healthcheck.message = "ERROR";
        healthcheck.checks.database = "ERROR";
        res.status(503).json(healthcheck);
    }
});

module.exports = router;

Deployment Scripts

# package.json scripts
{
  "scripts": {
    "start": "node app.js",
    "dev": "nodemon app.js",
    "test": "jest",
    "test:watch": "jest --watch",
    "build": "npm ci --only=production",
    "deploy": "npm run build && pm2 restart ecosystem.config.js --env production",
    "logs": "pm2 logs",
    "monitor": "pm2 monit"
  }
}
#!/bin/bash
# deploy.sh

echo "Starting deployment..."

# Pull latest code
git pull origin main

# Install dependencies
npm ci --only=production

# Run tests
npm test

# Build application (if needed)
npm run build

# Restart application with PM2
pm2 restart ecosystem.config.js --env production

# Check application health
sleep 5
curl -f http://localhost:8080/health || exit 1

echo "Deployment completed successfully!"

Exercise

Set up a complete production deployment for a Node.js application that includes:

  1. Environment configuration management
  2. Security middleware implementation
  3. Logging and monitoring setup
  4. Docker containerization
  5. PM2 process management
  6. Health checks and graceful shutdown

Conclusion

You've now learned the fundamentals of Node.js development, from basic concepts to production deployment. Continue practicing by building real-world applications and exploring advanced topics like microservices, GraphQL, and serverless deployment.