Node.js Deployment: Comprehensive Theory & Practice Guide
From fundamentals to PM2, Nginx, CI/CD, and zero-downtime production operations.
PM2NginxCI/CD
Table of Contents
- Theory: Deployment Fundamentals
- Theory: Deployment Architectures
- Basic: Application Preparation
- Basic: Process Management with PM2
- Advanced: Reverse Proxy with Nginx
- Advanced: CI/CD Pipeline
- Advanced: Zero-Downtime & Health Checks
- Best Practices & Checklists
- Interview Q&A + MCQ
- Contextual Learning Links
1. Theory: Deployment Fundamentals
Deployment is moving your Node.js app from local development to a production environment where users can access it reliably and securely.
┌─────────────────────────────────────────────────────────────────┐
│ Deployment Lifecycle │
├─────────────────────────────────────────────────────────────────┤
│ 1. Preparation ──► 2. Hosting ──► 3. Environment ──► 4. Deploy ──► 5. Monitor
│ • package.json • VPS/Cloud • Node.js • Git/GitHub • Logs
│ • .gitignore • PaaS • PM2 • Reverse Proxy • Metrics
│ • env vars • Container • Nginx • SSL/TLS • Alerts
└─────────────────────────────────────────────────────────────────┘
| Model | Description | Best For | Example |
|---|---|---|---|
| VPS | Full OS control | Custom setups | AWS EC2, DigitalOcean |
| PaaS | Managed infrastructure | Fast deployment | Heroku, Railway, Render |
| Container-based | Docker orchestration | Microservices | Kubernetes, ECS |
| Serverless | Functions as a service | Event-driven workloads | AWS Lambda, Vercel |
const deploymentPrinciples = {
environmentParity: {
description: 'Keep dev/staging/prod as similar as possible',
implementation: 'Same Node version, OS family, dependencies'
},
configSegregation: {
description: 'Separate code from config',
implementation: 'Use environment variables'
},
statelessness: {
description: 'Do not store user state in app memory',
implementation: 'Sessions in Redis, files in object storage'
},
idempotency: {
description: 'Same deployment process, same result',
implementation: 'Immutable artifacts and declarative infra'
}
};
2. Theory: Deployment Architectures
┌─────────────────────────────────────────────────────────────────┐
│ Traditional Architecture │
├─────────────────────────────────────────────────────────────────┤
│ User ──► Nginx (80/443) ──► Node.js (3000) ──► PostgreSQL │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Modern Scalable Architecture │
├─────────────────────────────────────────────────────────────────┤
│ User ─► CDN ─► LB ─┬─► Node 1 │
│ ├─► Node 2 │
│ └─► Node 3 │
│ Redis cache/session + Primary DB + Replicas │
└─────────────────────────────────────────────────────────────────┘
| Component | Purpose | Port | In Production |
|---|---|---|---|
| Node.js App | Business logic, APIs | Internal 3000 | Managed by PM2 |
| Nginx | Reverse proxy, static files, TLS | 80/443 | System service |
| PM2 | Process lifecycle and restart | N/A | Daemon |
| Load Balancer | Traffic distribution | 80/443 | ELB/HAProxy |
3. Basic: Application Preparation
{
"name": "myapp-api",
"version": "1.0.0",
"main": "dist/server.js",
"scripts": {
"start": "node dist/server.js",
"build": "tsc",
"dev": "nodemon src/server.ts",
"prestart": "npm run build",
"postinstall": "npm run build"
},
"engines": { "node": ">=18.0.0", "npm": ">=9.0.0" }
}
// config.js
const config = {
port: parseInt(process.env.PORT || '3000', 10),
nodeEnv: process.env.NODE_ENV || 'development',
database: {
url: process.env.DATABASE_URL,
poolSize: parseInt(process.env.DB_POOL_SIZE || '10', 10),
ssl: process.env.NODE_ENV === 'production'
},
redis: { url: process.env.REDIS_URL || 'redis://localhost:6379' },
jwtSecret: process.env.JWT_SECRET,
corsOrigins: process.env.CORS_ORIGINS?.split(',') || []
};
if (config.nodeEnv === 'production') {
const required = ['DATABASE_URL', 'JWT_SECRET'];
const missing = required.filter(key => !process.env[key]);
if (missing.length) throw new Error(`Missing required env vars: ${missing.join(', ')}`);
}
module.exports = config;
# .gitignore
node_modules/
.env
.env.*
!.env.example
dist/
build/
logs/
*.log
coverage/
.pm2/
# .env.example
NODE_ENV=development
PORT=3000
DATABASE_URL=postgresql://localhost:5432/mydb
DB_POOL_SIZE=10
REDIS_URL=redis://localhost:6379
JWT_SECRET=change_this_in_production
CORS_ORIGINS=http://localhost:3001
ENABLE_CACHING=false
ENABLE_METRICS=false
4. Basic: Process Management with PM2
# Install PM2 globally
sudo npm install -g pm2
# Start application
pm2 start app.js --name myapp
pm2 start ecosystem.config.js
pm2 startup
pm2 save
pm2 list
pm2 logs
pm2 reload myapp
// ecosystem.config.js
module.exports = {
apps: [{
name: 'myapp-api',
script: './dist/server.js',
instances: 4,
exec_mode: 'cluster',
env: { NODE_ENV: 'development', PORT: 3000 },
env_production: { NODE_ENV: 'production', PORT: 3000 },
watch: false,
max_memory_restart: '1G',
error_file: './logs/err.log',
out_file: './logs/out.log',
log_file: './logs/combined.log',
time: true,
kill_timeout: 5000,
min_uptime: '10s',
max_restarts: 10,
merge_logs: true
}]
};
# PM2 commands reference
pm2 start app.js -i max
pm2 reload myapp
pm2 show myapp
pm2 reset myapp
pm2 jlist
pm2 kill
pm2 install pm2-logrotate
5. Advanced: Reverse Proxy with Nginx
Nginx provides TLS termination, static file serving, buffering, and proxying traffic to internal Node.js ports.
# /etc/nginx/sites-available/myapp
server {
listen 80;
server_name api.example.com;
client_max_body_size 10M;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 60s;
}
location /health {
proxy_pass http://localhost:3000/health;
access_log off;
}
}
# Enable Nginx site
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx
# HTTPS with Let's Encrypt
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d api.example.com -d www.api.example.com
sudo certbot renew --dry-run
upstream node_backend {
least_conn;
server 127.0.0.1:3001;
server 127.0.0.1:3002;
server 127.0.0.1:3003;
keepalive 32;
}
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://node_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
6. Advanced: CI/CD Pipeline
# .github/workflows/deploy.yml (condensed)
name: Deploy to Production
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'npm' }
- run: npm ci
- run: npm test
- run: npm run lint
- run: npm run type-check
deploy:
needs: test
runs-on: ubuntu-latest
steps:
- name: Deploy via SSH
run: |
scp deploy.tar.gz $SSH_USER@$SSH_HOST:/tmp/
ssh $SSH_USER@$SSH_HOST "cd /var/www/myapp && tar -xzf /tmp/deploy.tar.gz && npm ci --only=production && pm2 reload ecosystem.config.js --update-env"
# Required GitHub secrets
SSH_HOST=your-server-ip-or-domain
SSH_USER=ubuntu
SSH_PRIVATE_KEY=$(cat ~/.ssh/deploy_key)
APP_URL=https://api.example.com
7. Advanced: Zero-Downtime & Health Checks
// server.js - graceful shutdown
const express = require('express');
const app = express();
let activeConnections = 0;
const server = app.listen(3000);
server.on('connection', (socket) => {
activeConnections++;
socket.on('close', () => activeConnections--);
});
app.get('/health', (req, res) => res.json({ status: 'healthy', pid: process.pid }));
app.get('/ready', (req, res) => {
const isReady = server.listening && activeConnections <= 100;
res.status(isReady ? 200 : 503).json({ status: isReady ? 'ready' : 'not ready', activeConnections });
});
async function gracefulShutdown(signal) {
server.close();
const force = setTimeout(() => process.exit(1), 10000);
const wait = () => activeConnections === 0 ? (clearTimeout(force), process.exit(0)) : setTimeout(wait, 1000);
wait();
}
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
// health.js - probe patterns
router.get('/health/live', (req, res) => res.status(200).json({ status: 'alive' }));
router.get('/health/ready', async (req, res) => {
const checks = {
database: await checkDatabase(),
redis: await checkRedis(),
memory: checkMemoryUsage()
};
const isReady = Object.values(checks).every(c => c.healthy);
res.status(isReady ? 200 : 503).json({ status: isReady ? 'ready' : 'not ready', checks });
});
8. Best Practices & Checklists
const preDeploymentChecklist = {
code: ['✓ Tests passing', '✓ No lint errors', '✓ TypeScript build passes'],
config: ['✓ NODE_ENV=production', '✓ Secrets in env vars', '✓ CORS configured'],
security: ['✓ Strong JWT secret', '✓ HTTPS enabled', '✓ Input validation'],
performance: ['✓ Compression enabled', '✓ Redis caching', '✓ Cluster mode'],
monitoring: ['✓ /health endpoint', '✓ Structured logs', '✓ Metrics endpoint']
};
# Deployment commands quick reference
sudo apt update && sudo apt upgrade -y
sudo apt install -y nodejs nginx
sudo npm install -g pm2
cd /var/www/myapp
git pull origin main
npm ci --only=production
npm run build
pm2 reload ecosystem.config.js --update-env
sudo nginx -t
sudo systemctl reload nginx
sudo certbot --nginx -d api.example.com
| Model | When to Use | Setup Complexity | Maintenance |
|---|---|---|---|
| VPS (Manual) | Need full control | High | High |
| VPS + CI/CD | Frequent team deploys | Medium | Medium |
| PaaS | Fast launch with less ops | Low | Low |
| Docker + Orchestration | Microservices, scale | High | Medium |
| Serverless | Sporadic/event traffic | Low | Very Low |
# Post-deployment verification
pm2 status
pm2 logs --lines 50
curl -I https://api.example.com/health
sudo nginx -t
sudo systemctl status nginx
openssl s_client -connect api.example.com:443 -servername api.example.com
htop
df -h
free -h
10 Interview Questions + 10 MCQs
1What is deployment in Node.js context?easy
Answer: Moving an app from development to production infrastructure where users can access it.
2Why use PM2 in production?easy
Answer: Process supervision, restart policies, clustering, logs, and startup persistence.
3What role does Nginx play for Node.js?easy
Answer: Reverse proxy, TLS termination, buffering, and static asset handling.
4What enables zero-downtime deployment?medium
Answer: Graceful shutdown, health checks, and reload strategies (e.g., `pm2 reload`).
5Difference between liveness and readiness probes?medium
Answer: Liveness checks process health; readiness checks traffic-serving capability.
6Why keep app stateless in deployment?medium
Answer: Enables horizontal scaling and safe failover across instances.
7What should be in `.env.example`?easy
Answer: Required variable names and safe defaults, never real secrets.
8Why run deployment via CI/CD?easy
Answer: Repeatability, traceability, automated checks, and reduced manual error.
9When choose PaaS over VPS?medium
Answer: When speed and lower ops overhead matter more than fine-grained control.
10Why verify deployment post-release?hard
Answer: To catch runtime/config regressions quickly and rollback before customer impact grows.
10 Deployment MCQs
1
PM2 primarily provides:
Explanation: PM2 manages process lifecycle, clustering, logs, and restarts.
2
Nginx is commonly used as:
Explanation: Nginx fronts traffic and forwards it to internal app servers.
3
Best place for secrets:
Explanation: Secrets should never be committed into repo history.
4
`pm2 reload` is preferred for:
Explanation: Reload rotates processes without fully dropping traffic.
5
Readiness probe should return 503 when:
Explanation: Readiness gates production traffic routing.
6
In production, Node app port should usually be:
Explanation: Nginx usually terminates TLS and forwards to internal app port.
7
Main benefit of CI/CD pipeline:
Explanation: CI/CD improves consistency and safety of releases.
8
`NODE_ENV=production` should be:
Explanation: Frameworks and libs use this for production optimizations.
9
A post-deploy verification should include:
Explanation: Verify service health, proxy status, and logs immediately.
10
Stateless deployment helps with:
Explanation: Shared external state enables any instance to handle requests.