Running node app.js in a terminal is not production deployment. If the terminal closes, the process dies. If the app crashes, it stays dead. If the server reboots, it doesn't restart. PM2 solves all of this.
PM2 Installation and Basic Use
npm install -g pm2
# Start your application
pm2 start app.js --name "my-app"
# With environment mode
pm2 start app.js --name "my-app" --env production
# Start on system boot
pm2 startup
pm2 save # Save current process list to resurrect on reboot
Ecosystem Configuration File
For production, use an ecosystem file instead of command-line flags:
// ecosystem.config.js
module.exports = {
apps: [{
name: 'my-api',
script: 'dist/server.js', // or build output
instances: 'max', // Use all CPU cores
exec_mode: 'cluster', // Fork for single-threaded, cluster for HTTP
env_production: {
NODE_ENV: 'production',
PORT: 3000
},
// Logging
log_file: '/var/log/myapp/combined.log',
error_file: '/var/log/myapp/error.log',
// Auto-restart on memory leak
max_memory_restart: '500M',
// Restart if file watches change (useful for simple deployments)
watch: false // Keep false in production
}]
};
pm2 start ecosystem.config.js --env production
Nginx as Reverse Proxy
server {
listen 443 ssl;
server_name api.yourdomain.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade'; # For WebSocket support
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_cache_bypass $http_upgrade;
}
}
Deployment Update Script
#!/bin/bash
# deploy.sh
set -e
cd /var/www/my-app
git pull origin main
npm ci --production
npm run build
pm2 reload my-api --update-env # Graceful zero-downtime reload
echo "Deployment complete"
Monitoring
pm2 monit # Real-time dashboard (CPU, memory, log)
pm2 logs my-api --lines 50 # Recent logs
pm2 status # Process overview
Deploy your Node.js API on Space-Node VPS
Why PM2 in 2026
PM2 has been the de-facto Node.js process manager since 2015 and is still the simplest way to run Node in production. Alternatives (systemd, Docker, Kubernetes) are valid but heavier.
What PM2 gives you:
- Auto-restart on crash.
- Cluster mode (multiple workers per CPU).
- Log rotation.
- One-line zero-downtime reload.
- Built-in metrics endpoint.
- Startup script generation for systemd / launchd.
What it doesn't give you:
- Container isolation (use Docker for that).
- Multi-host orchestration (use k8s).
- Network policy / service mesh.
Install
npm install -g pm2
For production, install on a non-root user:
adduser --system --group --home /home/app app
sudo -u app npm install --prefix /home/app/.npm-global -g pm2
Ecosystem file (preferred over CLI flags)
ecosystem.config.cjs:
module.exports = {
apps: [{
name: 'web',
script: './dist/index.js',
instances: 'max',
exec_mode: 'cluster',
max_memory_restart: '512M',
env_production: {
NODE_ENV: 'production',
PORT: 3000,
},
error_file: './logs/web-err.log',
out_file: './logs/web-out.log',
merge_logs: true,
time: true,
}],
};
instances: 'max' runs one worker per CPU core. exec_mode: 'cluster' enables Node's built-in cluster module for shared-port load balancing.
Start, monitor, persist
pm2 start ecosystem.config.cjs --env production
pm2 status
pm2 logs web --lines 100
pm2 monit # ncurses dashboard
pm2 save # persist current process list
pm2 startup systemd # generate systemd boot script (run as root)
pm2 save + pm2 startup is the combination that makes processes survive reboot. Skipping either means PM2 forgets your apps after the next restart.
Zero-downtime deploys
# during deploy
git pull
npm ci --omit=dev
npm run build
pm2 reload web # workers restart one-by-one, no dropped requests
reload (not restart) is what makes it zero-downtime in cluster mode. Each worker gets a SIGINT, finishes in-flight requests, then a new worker takes its place.
Log rotation
pm2 install pm2-logrotate
pm2 set pm2-logrotate:max_size 50M
pm2 set pm2-logrotate:retain 7
pm2 set pm2-logrotate:compress true
Without rotation, out.log grows to GB and starves the disk. We've debugged "server slow" issues that turned out to be a 40 GB log file filling /var.
Common breakage
| Symptom | Cause | Fix |
|---|---|---|
| App died, didn't restart | crash loop > 15 in 60s = give up | fix the crash, then pm2 start |
| "max_memory_restart" loops | memory leak on hot path | profile with --inspect, fix leak |
| Port already in use | old PM2 instance from prior user | pm2 list as the right user |
| pm2 restart drops requests | using restart not reload | use reload in cluster mode |
| OOM after rotation runs | log files held by deleted FD | restart pm2-logrotate; fix node process if it grew unbounded |
When to switch off PM2
- You're moving to containers anyway: drop PM2, let Kubernetes / Docker handle restart and scaling.
- You only run one process and don't need cluster mode: a plain systemd unit is simpler.
- You need better metrics: PM2 Plus is paid; Prometheus + node-exporter is free.
For a single VPS running 1-5 Node services, PM2 is still the right answer in 2026.
