Table of Contents
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:
- Environment configuration management
- Security middleware implementation
- Logging and monitoring setup
- Docker containerization
- PM2 process management
- 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.