How do I deploy a production REST API with Node.js and PM2 on a VPS?
PM2 is a production process manager for Node.js that provides crash recovery, cluster mode for multi-core utilization, zero-downtime reloads, log management, and startup scripts. Without PM2, a crashed Node.js process means API downtime until manual restart.
DETAILED EXPLANATION:
Problems PM2 solves:
1. Crash recovery: PM2 auto-restarts crashed processes in under 1 second
2. Multi-core utilization: Node.js is single-threaded; cluster mode spawns one process per CPU core
3. Zero-downtime deployment: pm2 reload does graceful restart without dropping connections
4. Startup persistence: API automatically starts on server reboot
5. Log management: Centralized logs with rotation
PM2 vs alternatives:
- systemd: Lower level, no Node.js-specific features
- Forever: Older, less maintained
- Docker: Heavier, better for containerized deployments
- PM2: Best for direct VPS deployment without containers
WHEN TO USE:
- Any Node.js/Express/Fastify/Next.js API in production
- Background workers and queue processors
- On Connect Quest VPS with 2+ GB RAM for Node.js applications
STEP-BY-STEP - Complete production setup:
1. Install Node.js (LTS):
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
apt install -y nodejs
node --version
npm --version
2. Install PM2:
npm install -g pm2
3. Sample Express API (server.js):
const express = require('express');
const app = express();
app.use(express.json());
app.get('/health', (req, res) => {
res.json({ status: 'ok', pid: process.pid, uptime: process.uptime() });
});
app.get('/api/products', async (req, res) => {
// Your business logic
res.json({ products: [] });
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log('API on port ' + PORT + ', PID: ' + process.pid));
// Graceful shutdown (critical for PM2 cluster mode)
process.on('SIGTERM', () => {
console.log('SIGTERM received, shutting down gracefully');
process.exit(0);
});
4. PM2 ecosystem file (ecosystem.config.js):
module.exports = {
apps: [{
name: 'my-api',
script: './server.js',
instances: 'max', // One per CPU core
exec_mode: 'cluster',
watch: false,
max_memory_restart: '500M',
env: {
NODE_ENV: 'production',
PORT: 3000,
DB_HOST: '127.0.0.1',
},
error_file: '/var/log/pm2/api-error.log',
out_file: '/var/log/pm2/api-out.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
cron_restart: '0 2 * * *', // Restart daily at 2am (prevent memory leak)
}]
};
5. Start and configure startup:
cd /var/www/api && npm install
pm2 start ecosystem.config.js
pm2 save
pm2 startup systemd
# Copy and run the output command (registers PM2 with systemd)
6. Nginx reverse proxy (/etc/nginx/sites-available/api):
server {
listen 443 ssl http2;
server_name api.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/api.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.yourdomain.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
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;
}
}
nginx -t && systemctl reload nginx
REAL EXAMPLES:
PM2 status output:
App name id mode pid status cpu memory uptime
my-api 0 cluster 12301 online 0% 48.2mb 5d
my-api 1 cluster 12302 online 0% 47.8mb 5d
my-api 2 cluster 12303 online 0% 49.1mb 5d
my-api 3 cluster 12304 online 0% 48.5mb 5d
Four processes on 4-core CPU = 4x throughput vs single process
Zero-downtime deploy workflow:
git pull origin main
npm install --production
pm2 reload ecosystem.config.js
Reload output: Reloading app [my-api] with id [0,1,2,3]
Zero connections dropped during reload
FLOW:
HTTPS request -> Nginx (SSL) -> proxy_pass 127.0.0.1:3000
-> PM2 load balances across 4 worker processes (round-robin)
-> Worker processes request -> response sent back through Nginx -> client
Process crash scenario:
Worker 2 crashes (uncaught exception)
PM2 detects crash within 100ms
PM2 restarts worker 2 automatically within 1 second
Requests during restart handled by workers 0, 1, 3 (no downtime)
KEY POINTS:
- pm2 reload = zero-downtime graceful restart (use this for deployments)
- pm2 restart = hard restart with brief downtime (use only when debugging)
- max_memory_restart prevents slow Node.js memory leaks from accumulating
- Connect Quest 4-core VPS with 8GB RAM recommended for production Node.js APIs
COMMON MISTAKES:
- Running node server.js directly (no crash recovery, no multi-core, no restart on reboot)
- Not running pm2 startup (API down after every server reboot)
- Using pm2 restart instead of pm2 reload during deployments (causes brief 502s)
QUICK FIX:
API returns 502 after deployment: Check pm2 logs my-api --err --lines 20
Usually: syntax error in code. Rollback: git revert HEAD && pm2 reload ecosystem.config.js
DIFFICULTY: Intermediate
RELATED: VPS Hosting, Nginx, Node.js, Cloud Hosting, Connect Quest VPS
DETAILED EXPLANATION:
Problems PM2 solves:
1. Crash recovery: PM2 auto-restarts crashed processes in under 1 second
2. Multi-core utilization: Node.js is single-threaded; cluster mode spawns one process per CPU core
3. Zero-downtime deployment: pm2 reload does graceful restart without dropping connections
4. Startup persistence: API automatically starts on server reboot
5. Log management: Centralized logs with rotation
PM2 vs alternatives:
- systemd: Lower level, no Node.js-specific features
- Forever: Older, less maintained
- Docker: Heavier, better for containerized deployments
- PM2: Best for direct VPS deployment without containers
WHEN TO USE:
- Any Node.js/Express/Fastify/Next.js API in production
- Background workers and queue processors
- On Connect Quest VPS with 2+ GB RAM for Node.js applications
STEP-BY-STEP - Complete production setup:
1. Install Node.js (LTS):
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
apt install -y nodejs
node --version
npm --version
2. Install PM2:
npm install -g pm2
3. Sample Express API (server.js):
const express = require('express');
const app = express();
app.use(express.json());
app.get('/health', (req, res) => {
res.json({ status: 'ok', pid: process.pid, uptime: process.uptime() });
});
app.get('/api/products', async (req, res) => {
// Your business logic
res.json({ products: [] });
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log('API on port ' + PORT + ', PID: ' + process.pid));
// Graceful shutdown (critical for PM2 cluster mode)
process.on('SIGTERM', () => {
console.log('SIGTERM received, shutting down gracefully');
process.exit(0);
});
4. PM2 ecosystem file (ecosystem.config.js):
module.exports = {
apps: [{
name: 'my-api',
script: './server.js',
instances: 'max', // One per CPU core
exec_mode: 'cluster',
watch: false,
max_memory_restart: '500M',
env: {
NODE_ENV: 'production',
PORT: 3000,
DB_HOST: '127.0.0.1',
},
error_file: '/var/log/pm2/api-error.log',
out_file: '/var/log/pm2/api-out.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
cron_restart: '0 2 * * *', // Restart daily at 2am (prevent memory leak)
}]
};
5. Start and configure startup:
cd /var/www/api && npm install
pm2 start ecosystem.config.js
pm2 save
pm2 startup systemd
# Copy and run the output command (registers PM2 with systemd)
6. Nginx reverse proxy (/etc/nginx/sites-available/api):
server {
listen 443 ssl http2;
server_name api.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/api.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.yourdomain.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
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;
}
}
nginx -t && systemctl reload nginx
REAL EXAMPLES:
PM2 status output:
App name id mode pid status cpu memory uptime
my-api 0 cluster 12301 online 0% 48.2mb 5d
my-api 1 cluster 12302 online 0% 47.8mb 5d
my-api 2 cluster 12303 online 0% 49.1mb 5d
my-api 3 cluster 12304 online 0% 48.5mb 5d
Four processes on 4-core CPU = 4x throughput vs single process
Zero-downtime deploy workflow:
git pull origin main
npm install --production
pm2 reload ecosystem.config.js
Reload output: Reloading app [my-api] with id [0,1,2,3]
Zero connections dropped during reload
FLOW:
HTTPS request -> Nginx (SSL) -> proxy_pass 127.0.0.1:3000
-> PM2 load balances across 4 worker processes (round-robin)
-> Worker processes request -> response sent back through Nginx -> client
Process crash scenario:
Worker 2 crashes (uncaught exception)
PM2 detects crash within 100ms
PM2 restarts worker 2 automatically within 1 second
Requests during restart handled by workers 0, 1, 3 (no downtime)
KEY POINTS:
- pm2 reload = zero-downtime graceful restart (use this for deployments)
- pm2 restart = hard restart with brief downtime (use only when debugging)
- max_memory_restart prevents slow Node.js memory leaks from accumulating
- Connect Quest 4-core VPS with 8GB RAM recommended for production Node.js APIs
COMMON MISTAKES:
- Running node server.js directly (no crash recovery, no multi-core, no restart on reboot)
- Not running pm2 startup (API down after every server reboot)
- Using pm2 restart instead of pm2 reload during deployments (causes brief 502s)
QUICK FIX:
API returns 502 after deployment: Check pm2 logs my-api --err --lines 20
Usually: syntax error in code. Rollback: git revert HEAD && pm2 reload ecosystem.config.js
DIFFICULTY: Intermediate
RELATED: VPS Hosting, Nginx, Node.js, Cloud Hosting, Connect Quest VPS