Node.js / PM2 Deployment¶
Prerequisites¶
# Install Node.js
curl -fsSL https://deb.nodesource.com/setup_18.x | bash -
apt install -y nodejs
# Install PM2 globally
npm install -g pm2
PM2 Basics¶
# Start app
pm2 start app.js --name my-app
# Start with ecosystem file
pm2 start ecosystem.config.js
# List all processes
pm2 list
# Stop
pm2 stop my-app
# Restart
pm2 restart my-app
# Delete
pm2 delete my-app
# View logs
pm2 logs my-app
pm2 logs --lines=100
# Monitor
pm2 monit
pm2 status
Ecosystem File¶
ecosystem.config.js
module.exports = {
apps: [{
name: 'api',
script: 'dist/server.js',
instances: 'max', // Cluster mode (all CPUs)
exec_mode: 'cluster',
env: {
NODE_ENV: 'development',
PORT: 3001
},
env_production: {
NODE_ENV: 'production',
PORT: 3000
},
watch: false,
max_memory_restart: '512M',
log_date_format: 'YYYY-MM-DD HH:mm:ss',
error_file: './logs/err.log',
out_file: './logs/out.log',
merge_logs: true,
autorestart: true,
max_restarts: 10,
restart_delay: 4000
}]
}
# Start with production env
pm2 start ecosystem.config.js --env production
# Reload (zero-downtime)
pm2 reload ecosystem.config.js --env production
Nginx Reverse Proxy¶
server {
listen 443 ssl http2;
server_name api.example.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';
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;
}
}
Startup on Boot¶
# Generate systemd service
pm2 startup systemd -u www-data
# Save current process list
pm2 save
# Verify
systemctl status pm2-www-data
Docker Compose with PM2¶
services:
app:
image: node:18-alpine
working_dir: /app
command: npx pm2-runtime start ecosystem.config.js --env production
ports:
- "127.0.0.1:3000:3000"
volumes:
- ./app:/app
environment:
- NODE_ENV=production
Multi-App Setup¶
ecosystem.config.js
module.exports = {
apps: [
{
name: 'api',
script: 'api/dist/server.js',
instances: 2,
env_production: { PORT: 3000 }
},
{
name: 'worker',
script: 'worker/index.js',
instances: 1,
env_production: { PORT: 3001 }
},
{
name: 'websocket',
script: 'ws/server.js',
instances: 1,
env_production: { PORT: 3002 }
}
]
}
Deploy with PM2¶
ecosystem.config.js
module.exports = {
apps: [{ name: 'app', script: 'server.js' }],
deploy: {
production: {
user: 'deploy',
host: 'server.example.com',
ref: 'origin/main',
repo: 'git@github.com:org/app.git',
path: '/opt/app',
'post-deploy': 'npm ci && npm run build && pm2 reload ecosystem.config.js --env production'
}
}
}
pm2 deploy production setup
pm2 deploy production
GitHub Actions (PM2 Deploy)¶
name: Deploy Node.js
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy via SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USER }}
key: ${{ secrets.SSH_KEY }}
script: |
cd /opt/app
git pull origin main
npm ci --production
npm run build
pm2 reload ecosystem.config.js --env production
Monitoring¶
# Process list
pm2 ls
# Real-time metrics
pm2 monit
# CPU/Memory per process
pm2 prettylist
# API metrics
pm2 web 9615
# Logs
pm2 logs --lines=50
Troubleshooting¶
| Symptom | Cause | Fix |
|---|---|---|
| Port in use | Another process on same port | kill $(lsof -ti:3000) |
| Out of memory | Memory leak | Set max_memory_restart |
| "EADDRINUSE" | Port conflict | Change PORT in ecosystem |
| App not restarting | Max restarts exceeded | pm2 reset app && pm2 restart app |
| Zero-downtime fails | Health check missing | Add -i flag or listen_timeout |
Verification¶
- App starts with
pm2 start - Cluster mode enabled (multi-core)
- Startup on boot configured
- Log rotation configured
- Max memory restart set
- Nginx reverse proxy working
- Deploy via GitHub Actions works