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.
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.
Installing PM2 globally
PM2 should be installed globally so you can use it from any directory:
# 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
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:
One process runs on one core. Best for small apps or when you want lightweight overhead.
exec_mode: "fork", instances: 1,
Multiple processes share the same port. Node.js cluster module handles load balancing. Best for production scaling.
exec_mode: "cluster", instances: "max", // or: 4, 8, etc
Full ecosystem.config.js with all options
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
Number of processes to spawn. Use 'max' for all cores (recommended for production). Or: 1, 2, 4, 8, etc.
If a process exceeds this memory threshold, PM2 automatically restarts it. E.g., '500M' prevents memory leaks from crashing your app.
Array of directories to watch for changes. On file change, the process restarts. Useful for development; disable in production.
Environment variables passed to the app. Can also use env_staging, env_production, etc. Access via process.env in your app.
Milliseconds to wait for graceful shutdown before force-killing. Default 1600ms. Set higher if your app needs more cleanup time.
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
# 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
# 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
# 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
# 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
# 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:
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:
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
# 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
# Install Certbot and Nginx plugin sudo apt update sudo apt install -y certbot python3-certbot-nginx # Verify installation certbot --version
Get SSL certificate
# 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
# 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:
Typical deployment flow
# 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:
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
# 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
Rotate log when it reaches this size (10M, 100M, etc). Default: 10M
Keep this many old rotated log files. Default: 30
Compress rotated logs with gzip. Saves disk space. Default: false
Date format for rotated files. Default: YYYY-MM-DD_HH-mm-ss
Check for rotation every N milliseconds. Default: 300000 (5 min)
Manual log management
# 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:
# 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
✅ 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.
✅ 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 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.
✅ 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';'
✅ 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.
✅ 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.
✅ 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.
✅ 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
On subsequent deploys
Ongoing management
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