Skip to main content

Node.js production deployment with PM2 and nginx

TL;DR

Complete guide to configuring PM2 process management and nginx reverse proxy for production Node.js applications.

Key facts

Topology
Node.js + PM2 + nginx

TL;DR

PM2 and nginx form the standard production pair for Node.js on Linux. PM2 manages your Node.js processes with clustering, auto-restarts, and log management, while nginx handles TLS termination, static file serving, and reverse proxying.

PM2 ecosystem configuration

Create ecosystem.config.js in your project root:

module.exports = {
  apps: [{
    name: 'api',
    script: 'dist/server.js',
    instances: 'max',
    exec_mode: 'cluster',
    max_memory_restart: '1G',
    node_args: '--max-old-space-size=1536',
    env_production: {
      NODE_ENV: 'production',
      PORT: 3000,
    },
    error_file: '/var/log/pm2/api-error.log',
    out_file: '/var/log/pm2/api-out.log',
    merge_logs: true,
    log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
  }],
};

Start with:

pm2 start ecosystem.config.js --env production

PM2 persistence

Ensure processes survive a server reboot:

pm2 startup systemd
pm2 save

PM2 log management

Install the log rotation module to prevent disk fill:

pm2 install pm2-logrotate
pm2 set pm2-logrotate:max_size 50M
pm2 set pm2-logrotate:retain 7
pm2 set pm2-logrotate:compress true

nginx reverse proxy

upstream node_app {
    server 127.0.0.1:3000;
    keepalive 64;
}

server {
    listen 443 ssl http2;
    server_name example.com;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    gzip on;
    gzip_types text/plain application/json application/javascript text/css;

    location / {
        proxy_pass http://node_app;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        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;
        proxy_connect_timeout 10s;
    }

    location /static/ {
        alias /var/www/myapp/public/;
        expires 30d;
        add_header Cache-Control "public, immutable";
    }
}

Test and reload:

sudo nginx -t && sudo systemctl reload nginx

Health checks

Add a health endpoint in your app:

app.get('/health', (req, res) => {
  res.json({ status: 'ok', uptime: process.uptime() });
});

Monitor it with PM2's built-in monitoring or configure an external uptime check.

With Reflex

Reflex integrates with both PM2 and nginx to provide unified process monitoring, zero-downtime deployment orchestration, and automated incident response. Instead of SSH-ing in to run pm2 reload and checking nginx logs manually, Reflex handles the full lifecycle with audit trails and rollback capability. See How it works.