How to Deploy Node.js with Nginx + PM2 on Ubuntu (2015)
Short answer: Install PM2 globally, start your app with pm2 start app.js --name myapp, run pm2 startup to register on boot, then configure Nginx to proxy proxy_pass http://localhost:3000. For zero-downtime deploys, use pm2 reload myapp (not restart).
Install Node.js 4.x LTS (October 2015)
# Node.js 4.x was the first LTS release — what we deployed in late 2015
curl -sL https://deb.nodesource.com/setup_4.x | sudo -E bash -
sudo apt-get install -y nodejs
node --version # v4.2.2
npm --version # 2.14.7
Install PM2
sudo npm install -g pm2
# PM2 2.x — process manager with cluster mode, log rotation, monitoring
pm2 --version # 0.15.x (v2 released later in 2015)
PM2 Configuration File
// ecosystem.json — PM2 process config
{
"apps": [
{
"name": "myapp",
"script": "/var/www/myapp/server.js",
"instances": "max", // One process per CPU core
"exec_mode": "cluster", // Cluster mode: shared port, load balanced
"node_args": "--harmony", // Enable ES6 features in Node 4
"env": {
"NODE_ENV": "production",
"PORT": 3000,
"DB_HOST": "localhost"
},
"log_date_format": "YYYY-MM-DD HH:mm:ss",
"error_file": "/var/log/pm2/myapp-error.log",
"out_file": "/var/log/pm2/myapp-out.log",
"max_memory_restart": "500M",
// Restart if memory exceeds 500MB — prevents memory leak accumulation
"watch": false,
// Never watch in production — file watches cause constant restarts
"min_uptime": "5s",
"max_restarts": 10
}
]
}
# Start with ecosystem file
pm2 start ecosystem.json
# Register PM2 to start on system boot
pm2 startup ubuntu # Outputs a command to run as sudo
sudo env PATH=$PATH:/usr/bin pm2 startup ubuntu -u deploy --hp /home/deploy
# Save current process list to be restored on reboot
pm2 save
Nginx Configuration
# /etc/nginx/sites-available/myapp
upstream nodejs {
server 127.0.0.1:3000;
keepalive 64;
# keepalive: maintain persistent connections to Node.js
# Without this: new TCP connection per request = latency overhead
}
server {
listen 80;
server_name myapp.com www.myapp.com;
# Redirect HTTP to HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl;
server_name myapp.com www.myapp.com;
# SSL — Let's Encrypt wasn't available until December 2015
# In mid-2015 we still bought certificates ($10/year from NameCheap)
ssl_certificate /etc/ssl/myapp/myapp.crt;
ssl_certificate_key /etc/ssl/myapp/myapp.key;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers HIGH:!aNULL:!MD5;
# Serve static files directly from Nginx (don't hit Node.js)
location /static/ {
alias /var/www/myapp/public/;
expires 30d;
add_header Cache-Control "public, immutable";
gzip_static on;
}
# Proxy API requests to Node.js
location / {
proxy_pass http://nodejs;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
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_cache_bypass $http_upgrade;
proxy_read_timeout 60s;
}
}
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t # Test config
sudo service nginx reload
Deployment Script
#!/bin/bash
# deploy.sh — zero-downtime deploy
APP_DIR="/var/www/myapp"
REPO="git@github.com:company/myapp.git"
cd $APP_DIR
echo "Pulling latest code..."
git pull origin main
echo "Installing dependencies..."
npm install --production
echo "Running migrations..."
node scripts/migrate.js
echo "Reloading app (zero downtime)..."
pm2 reload myapp
# reload = graceful restart: starts new processes before stopping old ones
# restart = hard restart: brief downtime
# In cluster mode, reload cycles workers one at a time
echo "Deploy complete"
pm2 status
PM2 Commands Reference
# Monitor
pm2 status # Process list with CPU/memory
pm2 monit # Real-time monitoring dashboard
pm2 logs myapp # Tail logs
pm2 logs myapp --lines 200 # Last 200 lines
# Control
pm2 reload myapp # Zero-downtime restart (cluster mode)
pm2 restart myapp # Hard restart
pm2 stop myapp
pm2 delete myapp
# After code deploy
pm2 reload ecosystem.json --update-env # Also reload env vars
# Check what's eating memory
pm2 describe myapp # Detailed process info
WebSocket Support
# For Socket.io or ws — add to location block:
location /socket.io/ {
proxy_pass http://nodejs;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 600s; # Keep WS connections alive longer
}
Common Problem: "Cluster mode but still single process"
# Check
pm2 describe myapp | grep exec_mode
# Should say: cluster_mode
# If running in fork mode by mistake:
pm2 delete myapp
pm2 start ecosystem.json # Re-read config file
Benchmark: Fork vs Cluster Mode
On a 4-core VPS with a CPU-bound route (image resize):
| Mode | Requests/sec | CPU utilization |
|---|---|---|
| Fork (1 process) | 420 req/s | 25% (1 core maxed) |
| Cluster (4 workers) | 1,580 req/s | 94% (all cores used) |
For I/O-bound workloads (database queries, file reads), the difference is smaller because Node's event loop handles concurrent I/O without multiple processes. But for CPU work — image processing, PDF generation, crypto — cluster mode is essential.