Node.js Deployment: Comprehensive Theory & Practice Guide

From fundamentals to PM2, Nginx, CI/CD, and zero-downtime production operations.

PM2NginxCI/CD

Table of Contents

  1. Theory: Deployment Fundamentals
  2. Theory: Deployment Architectures
  3. Basic: Application Preparation
  4. Basic: Process Management with PM2
  5. Advanced: Reverse Proxy with Nginx
  6. Advanced: CI/CD Pipeline
  7. Advanced: Zero-Downtime & Health Checks
  8. Best Practices & Checklists
  9. Interview Q&A + MCQ
  10. 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
└─────────────────────────────────────────────────────────────────┘
ModelDescriptionBest ForExample
VPSFull OS controlCustom setupsAWS EC2, DigitalOcean
PaaSManaged infrastructureFast deploymentHeroku, Railway, Render
Container-basedDocker orchestrationMicroservicesKubernetes, ECS
ServerlessFunctions as a serviceEvent-driven workloadsAWS 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      │
└─────────────────────────────────────────────────────────────────┘
ComponentPurposePortIn Production
Node.js AppBusiness logic, APIsInternal 3000Managed by PM2
NginxReverse proxy, static files, TLS80/443System service
PM2Process lifecycle and restartN/ADaemon
Load BalancerTraffic distribution80/443ELB/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
ModelWhen to UseSetup ComplexityMaintenance
VPS (Manual)Need full controlHighHigh
VPS + CI/CDFrequent team deploysMediumMedium
PaaSFast launch with less opsLowLow
Docker + OrchestrationMicroservices, scaleHighMedium
ServerlessSporadic/event trafficLowVery 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:

AProcess management for Node apps
BDatabase migrations
CTLS certificate issuance
DFrontend bundling
Explanation: PM2 manages process lifecycle, clustering, logs, and restarts.
2

Nginx is commonly used as:

AReverse proxy
BNode package manager
CJavaScript runtime
DSQL driver
Explanation: Nginx fronts traffic and forwards it to internal app servers.
3

Best place for secrets:

AEnvironment variables/secrets manager
BHardcoded in source files
CCommitted `.env` file
DNginx comments
Explanation: Secrets should never be committed into repo history.
4

`pm2 reload` is preferred for:

AZero-downtime process refresh
BDatabase backup
CTLS renewal
DDisk cleanup
Explanation: Reload rotates processes without fully dropping traffic.
5

Readiness probe should return 503 when:

AApp cannot safely accept traffic
BApp just started regardless
CStatic files are cached
DCI passes
Explanation: Readiness gates production traffic routing.
6

In production, Node app port should usually be:

AInternal/private behind Nginx
BPublic 80 directly
COnly 443 without proxy
DNo port needed
Explanation: Nginx usually terminates TLS and forwards to internal app port.
7

Main benefit of CI/CD pipeline:

AAutomated, repeatable deployments
BRemoves need for testing
CEliminates monitoring
DOnly reduces npm install time
Explanation: CI/CD improves consistency and safety of releases.
8

`NODE_ENV=production` should be:

ASet in production runtime
BNever set
CSet to test
DOnly in package.json comments
Explanation: Frameworks and libs use this for production optimizations.
9

A post-deploy verification should include:

AHealth endpoint + process status checks
BOnly git log
COnly DNS flush
DOnly editor restart
Explanation: Verify service health, proxy status, and logs immediately.
10

Stateless deployment helps with:

AHorizontal scaling and failover
BIncreasing synchronous CPU loops
CReducing need for environment vars
DRemoving process manager
Explanation: Shared external state enables any instance to handle requests.