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: overlayCloud 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: LoadBalancerCI/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