DeployWise
HomeGuidesPM2 + Nginx Production Setup
PM2NginxNode.jsProductionCluster ModeSSL

PM2 + Nginx Production Setup: Complete Guide

Master the production stack for Node.js apps. Learn ecosystem.config.js configuration, cluster mode for multi-core scaling, zero-downtime reloads, SSL termination with Nginx, and advanced log management.

15 min read
Updated 2026

What PM2 does and why you need it

PM2 is a production process manager for Node.js applications. It runs your app as a managed process, automatically restarts it on crashes, handles graceful reloads, manages logs, and scales across CPU cores.

Without PM2: if your app crashes, it's down until you manually restart it. With PM2: your app auto-recovers in milliseconds, and you can redeploy without dropping connections.

Auto-restart on crash — recovers in milliseconds without manual intervention
Cluster mode — spawn multiple instances across all CPU cores for better throughput
Zero-downtime reloads — update code and restart without dropping user connections
Memory limits — automatically restart if app exceeds threshold to prevent memory leaks
Log aggregation — capture stdout/stderr with timestamps and rotation
Startup scripts — auto-start on server reboot using pm2 startup
Environment management — load different .env configs per app
File watching — auto-restart on source code changes (dev mode)

Installing PM2 globally

PM2 should be installed globally so you can use it from any directory:

bash
# Install PM2 globally
npm install -g pm2

# Update PM2
npm install -g pm2@latest

# Verify installation
pm2 --version

PM2 is now available in your PATH. You can use pm2 commands from any terminal session.

ecosystem.config.js deep dive

The ecosystem.config.js file is where you configure PM2 settings for your app. This single file controls process behavior, clustering, memory limits, environment variables, and more.

Basic ecosystem.config.js

javascript
module.exports = {
  apps: [
    {
      name: "my-app",
      script: "./dist/index.js",
      instances: "max",
      exec_mode: "cluster",
      env: {
        NODE_ENV: "production",
        PORT: 3000,
      },
    },
  ],
};

Fork mode vs Cluster mode

PM2 has two execution modes:

Fork mode (single process)

One process runs on one core. Best for small apps or when you want lightweight overhead.

javascript
exec_mode: "fork",
instances: 1,
Cluster mode (multi-process)

Multiple processes share the same port. Node.js cluster module handles load balancing. Best for production scaling.

javascript
exec_mode: "cluster",
instances: "max",  // or: 4, 8, etc

Full ecosystem.config.js with all options

javascript
module.exports = {
  apps: [
    {
      // App name and script
      name: "my-app",
      script: "./dist/index.js",
      cwd: "/var/www/my-app",

      // Execution mode and instances
      exec_mode: "cluster",           // or "fork"
      instances: "max",               // spawn on all cores (or: 2, 4, 8)
      instance_var: "INSTANCE_ID",    // env var with instance number

      // Memory and restart limits
      max_memory_restart: "500M",     // restart if exceeds 500MB
      max_restarts: 10,               // max restarts in 60 seconds
      min_uptime: "10s",              // min uptime before counting crash

      // File watching (auto-restart on code change)
      watch: ["src"],                 // auto-restart on changes
      ignore_watch: [
        "node_modules",
        "logs",
        ".git",
      ],
      watch_delay: 1000,              // debounce in ms

      // Logs
      output: "/var/log/pm2/my-app-out.log",
      error: "/var/log/pm2/my-app-err.log",
      log_date_format: "YYYY-MM-DD HH:mm:ss Z",

      // Environment variables
      env: {
        NODE_ENV: "production",
        PORT: 3000,
        DATABASE_URL: "postgres://...",
      },
      env_staging: {
        NODE_ENV: "staging",
        PORT: 3001,
      },

      // Graceful shutdown
      kill_timeout: 5000,             // ms to wait before SIGKILL
      wait_ready: true,               // wait for "ready" message from app

      // Auto-start on reboot
      autorestart: true,              // auto-restart on server reboot
      autostart: true,                // start on pm2 daemon launch

      // Additional options
      merge_logs: false,              // merge logs from all instances
      time: true,                     // timestamp in logs
    },
  ],

  // Deploy settings (optional, for pm2 deploy)
  deploy: {
    production: {
      user: "ubuntu",
      host: "your-server.com",
      ref: "origin/main",
      repo: "https://github.com/you/my-app.git",
      path: "/var/www/my-app",
      "post-deploy": "npm ci && npm run build && pm2 reload ecosystem.config.js --env production",
    },
  },
};

Key configuration options explained

instances

Number of processes to spawn. Use 'max' for all cores (recommended for production). Or: 1, 2, 4, 8, etc.

max_memory_restart

If a process exceeds this memory threshold, PM2 automatically restarts it. E.g., '500M' prevents memory leaks from crashing your app.

watch

Array of directories to watch for changes. On file change, the process restarts. Useful for development; disable in production.

env

Environment variables passed to the app. Can also use env_staging, env_production, etc. Access via process.env in your app.

kill_timeout

Milliseconds to wait for graceful shutdown before force-killing. Default 1600ms. Set higher if your app needs more cleanup time.

wait_ready

If true, app must emit process.send('ready') before PM2 marks it as healthy. Useful for preventing traffic during startup.

PM2 commands cheat sheet

Common PM2 operations for managing your production apps:

Starting and stopping

bash
# Start from ecosystem.config.js
pm2 start ecosystem.config.js

# Start specific app from config
pm2 start ecosystem.config.js --only my-app

# Start with specific environment
pm2 start ecosystem.config.js --env production

# Stop a process
pm2 stop my-app

# Stop all processes
pm2 stop all

# Restart a process (hard restart)
pm2 restart my-app

# Reload (zero-downtime restart)
pm2 reload my-app

# Delete a process
pm2 delete my-app

# Delete all processes
pm2 delete all

Monitoring and logs

bash
# List all processes
pm2 list

# Show status of one app
pm2 info my-app

# Show live logs
pm2 logs my-app

# Show logs from all apps
pm2 logs

# Show last 100 lines
pm2 logs my-app --lines 100

# Monitor CPU/memory in real-time
pm2 monit

# Clear all logs
pm2 flush

Saving state and startup

bash
# Save current process list
pm2 save

# Resurrect saved process list
pm2 resurrect

# Generate startup script
pm2 startup systemd -u ubuntu --hp /home/ubuntu

# Delete startup script
pm2 unstartup systemd

# Reset PM2 (clears all processes and history)
pm2 reset

Cluster and scaling

bash
# Reload with new instance count
pm2 reload my-app -i 4

# Increase instances
pm2 scale my-app +2

# Set to exact count
pm2 scale my-app 8

# Show which CPU each instance is on
pm2 show my-app

Debugging

bash
# Kill and restart (useful if hung)
pm2 kill && pm2 start ecosystem.config.js

# Show last error
pm2 logs my-app --err

# Validate ecosystem.config.js
pm2 validate

Nginx reverse proxy basics

Nginx sits in front of your Node.js app. It listens on port 80 (HTTP) and 443 (HTTPS), handles SSL termination, proxies requests to your PM2-managed app on localhost:3000, and provides compression and caching.

Why Nginx? Node.js is a great application server, but it's not optimized for:

Handling thousands of idle connections (Nginx does this efficiently)
Serving static files at scale (Nginx is faster)
SSL/TLS termination (Nginx does this, so your app doesn't need to)
Load balancing between multiple instances
Compression (gzip)

Request flow: Client → Nginx (443) → PM2 → Node.js (3000)

Full Nginx config for Node.js apps

Save this as /etc/nginx/sites-available/my-app:

nginx
upstream my_app {
    # Reference your PM2 cluster instances
    server localhost:3000;
    # Optional: if you manually manage multiple ports:
    # server localhost:3001;
    # server localhost:3002;
    keepalive 64;
}

server {
    listen 80;
    listen [::]:80;
    server_name yourdomain.com www.yourdomain.com;

    # Redirect HTTP to HTTPS
    location / {
        return 301 https://$server_name$request_uri;
    }

    # Allow Let's Encrypt validation
    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name yourdomain.com www.yourdomain.com;

    # SSL certificates (from Certbot)
    ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;

    # SSL configuration
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;

    # Security headers
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Frame-Options "DENY" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;

    # Logging
    access_log /var/log/nginx/my-app-access.log;
    error_log /var/log/nginx/my-app-error.log;

    # Gzip compression
    gzip on;
    gzip_types text/plain text/css text/javascript application/javascript application/json;
    gzip_min_length 1024;

    # Proxy settings
    location / {
        proxy_pass http://my_app;
        proxy_http_version 1.1;

        # WebSocket support
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';

        # Headers
        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_set_header X-Forwarded-Host $server_name;

        # Timeouts
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;

        # Buffering
        proxy_buffering on;
        proxy_buffer_size 4k;
        proxy_buffers 8 4k;
        proxy_busy_buffers_size 8k;

        # Don't cache
        proxy_cache_bypass $http_upgrade;
    }

    # Static files (if your app serves from /public)
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
        proxy_pass http://my_app;
        expires 30d;
        add_header Cache-Control "public, immutable";
    }
}

Enable the site

bash
# Create symlink to sites-enabled
sudo ln -s /etc/nginx/sites-available/my-app /etc/nginx/sites-enabled/

# Test configuration
sudo nginx -t

# Reload Nginx
sudo systemctl reload nginx

SSL with Certbot

Free SSL certificates from Let's Encrypt. Certbot automates installation and renewal.

Install Certbot

bash
# Install Certbot and Nginx plugin
sudo apt update
sudo apt install -y certbot python3-certbot-nginx

# Verify installation
certbot --version

Get SSL certificate

bash
# Interactive - Certbot modifies Nginx config automatically
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com

# Or manual - you edit Nginx config yourself
sudo certbot certonly --manual -d yourdomain.com -d www.yourdomain.com

Auto-renewal

bash
# Certbot installs a systemd timer automatically
sudo systemctl enable certbot.timer

# Check renewal status
sudo certbot renew --dry-run

# Manual renewal
sudo certbot renew

Certbot checks and renews certificates 30 days before expiry. Your SSL is always fresh.

Zero-downtime reloads

One of PM2's superpowers is reloading without dropping connections. This is crucial for deployments.

How it works

When you run pm2 reload, PM2:

1.Spawns a new process (instance 1) with updated code
2.Waits for it to become healthy
3.Gracefully shuts down the old process (instance 0)
4.Removes old process, keeps new one
5.Repeats for all instances (in cluster mode)

Typical deployment flow

bash
# Pull latest code
git pull origin main

# Install/update dependencies
npm ci

# Build
npm run build

# Reload with zero downtime
pm2 reload ecosystem.config.js

# Save state
pm2 save

Graceful shutdown in your app

For smooth reloads, handle the SIGTERM signal in your app to gracefully close connections:

javascript
const http = require('http');

const server = http.createServer((req, res) => {
  res.writeHead(200);
  res.end('Hello');
});

server.listen(3000, () => {
  console.log('Server listening on 3000');

  // Tell PM2 we're ready
  if (process.send) {
    process.send('ready');
  }
});

// Handle graceful shutdown
process.on('SIGTERM', async () => {
  console.log('SIGTERM received, closing gracefully...');

  server.close(() => {
    console.log('Server closed');
    process.exit(0);
  });

  // Force exit after 5 seconds
  setTimeout(() => {
    console.error('Forced exit');
    process.exit(1);
  }, 5000);
});

With this setup, PM2 waits for all connections to close before terminating the old process.

PM2 log management (pm2-logrotate)

PM2 logs can grow large over time. Use pm2-logrotate to automatically rotate and compress old logs.

Install pm2-logrotate

bash
# Install module
pm2 install pm2-logrotate

# Configure rotation
pm2 set pm2-logrotate:max_size 10M
pm2 set pm2-logrotate:retain 30
pm2 set pm2-logrotate:compress true

# Verify config
pm2 conf pm2-logrotate

Log rotation options

max_size

Rotate log when it reaches this size (10M, 100M, etc). Default: 10M

retain

Keep this many old rotated log files. Default: 30

compress

Compress rotated logs with gzip. Saves disk space. Default: false

dateFormat

Date format for rotated files. Default: YYYY-MM-DD_HH-mm-ss

rotateInterval

Check for rotation every N milliseconds. Default: 300000 (5 min)

Manual log management

bash
# View live logs
pm2 logs my-app

# View error logs only
pm2 logs my-app --err

# View from last N lines
pm2 logs my-app --lines 100

# Flush all logs
pm2 flush

# Clear specific app logs
pm2 logs my-app --clear

# Show log files location
pm2 info my-app | grep -i "log"

# Manually rotate logs
pm2 logrotate

Monitoring with pm2 monit

Real-time CPU and memory monitoring directly in your terminal:

bash
# Start real-time monitor
pm2 monit

# This shows live graphs of:
# - CPU usage per instance
# - Memory usage per instance
# - Process status (online/errored/stopped)
# - Restart count
# - Uptime

# Press 'q' to exit

For persistent monitoring and alerting, integrate with third-party services like PM2+ (pm2.keymetrics.io), DataDog, New Relic, or Prometheus.

Common PM2 + Nginx issues

502 Bad Gateway from Nginx

Your Node.js process isn't running or isn't listening on the expected port. Run 'pm2 status' to check if the app is online. Check 'pm2 logs app-name' for errors. Verify proxy_pass port matches your app's PORT env variable.

PM2 process shows 'errored' status

The app crashed on startup. Run 'pm2 logs my-app' to see the error. Common causes: missing dependencies, missing env variables, or syntax errors. Fix the issue, run 'pm2 delete my-app', then 'pm2 start ecosystem.config.js'.

App restarts repeatedly (respawn loop)

App crashes immediately after starting. Check 'pm2 logs' for errors. Increase 'kill_timeout' in ecosystem.config.js if the app needs more time to shut down. Verify max_restarts isn't set too low.

WebSocket connections drop after reload

Your Nginx config needs WebSocket headers. Ensure these are in your Nginx location block: 'proxy_set_header Upgrade $http_upgrade;' and 'proxy_set_header Connection 'upgrade';'

Nginx config test passes but site doesn't load

Run 'sudo systemctl reload nginx' after enabling the site. Check 'sudo systemctl status nginx' for errors. View 'sudo tail -f /var/log/nginx/error.log' for detailed errors.

SSL certificate not renewing automatically

Check 'sudo systemctl status certbot.timer' to verify auto-renewal is enabled. Run 'sudo certbot renew --dry-run' to test renewal. Check '/var/log/letsencrypt/letsencrypt.log' for renewal errors.

High memory usage, app keeps restarting

Set 'max_memory_restart' in ecosystem.config.js (e.g., '500M'). This prevents memory leaks from consuming all RAM. Monitor with 'pm2 monit'. Check your app code for memory leaks.

Timeout errors when accessing the site

Increase 'proxy_read_timeout', 'proxy_connect_timeout', and 'proxy_send_timeout' in your Nginx config. Default is often too short for slow endpoints. Set to 60s or higher.

How DeployWise manages PM2 + Nginx automatically

Every step above — ecosystem.config.js generation, PM2 setup, Nginx configuration, SSL certificates, log rotation — is automated by DeployWise. Here's what happens when you click Deploy:

Initial deployment

1.Provision server and install dependencies (Node.js, PM2, Nginx, Certbot)
2.Generate ecosystem.config.js with your app settings (auto-detected instances, memory limits)
3.Clone your repository and build your app
4.Start app with PM2 (auto-saves and enables startup script)
5.Generate Nginx reverse proxy config optimized for your app
6.Obtain free SSL certificate from Let's Encrypt
7.Enable pm2-logrotate for automatic log management
8.Verify health: check if app is online and responding to requests

On subsequent deploys

1.Pull latest code from your GitHub repository
2.Install new/changed dependencies (npm ci)
3.Run your build command (npm run build)
4.Reload PM2 with zero downtime (zero dropped connections)
5.Update Nginx config if domain or settings changed
6.Verify app is healthy and responding

Ongoing management

Auto-restarts your app if it crashes
Rotates and compresses logs to save disk space
Renews SSL certificates 30 days before expiry (automatic)
Monitors app health and uptime
Scales cluster instances based on demand (optional)

You never interact with PM2 or Nginx commands — DeployWise handles all of it through the dashboard. But now you understand what's happening under the hood.

Stop configuring PM2 and Nginx manually

DeployWise automates the entire stack — from ecosystem.config.js to SSL renewal. One click deploys your Node.js app with production-grade process management and reverse proxy.

Try DeployWise Free