Zero-downtime Node.js deployments with PM2 on a VPS
PM2 cluster mode, rolling restarts, and a simple deploy script. Ship updates without dropping a single request.
Deploying a Node.js app by SSHing in, killing the process, and restarting it will drop every active connection. For a low-traffic hobby project that's fine. For anything real, you need zero-downtime restarts.
PM2 makes this simple with cluster mode and reload. Here's exactly how to set it up.
Why cluster mode?
Node.js is single-threaded. On a multi-core server, a single Node process leaves CPU cores idle. PM2's cluster mode forks your app across all available cores and load-balances incoming connections between them.
Cluster mode also enables zero-downtime restarts: PM2 reloads workers one at a time, keeping at least one worker alive throughout the process.
Step 1: Install PM2
npm install -g pm2 Or use Simplewala's Node.js installer, which includes PM2 automatically.
Step 2: Start your app in cluster mode
pm2 start npm --name "my-app" -i max -- start -i max forks one worker per CPU core. You can also specify a number: -i 4.
Save the process list so PM2 restarts your app after a server reboot:
pm2 save
pm2 startup Step 3: Zero-downtime reload
Use reload instead of restart when deploying updates:
pm2 reload my-app
PM2 sends SIGINT to one worker at a time and waits for it to finish handling
in-flight requests before starting a new worker. Active connections are never dropped.
A simple deploy script
Create /srv/app/deploy.sh:
#!/bin/bash
set -e
cd /srv/app
git pull origin main
npm ci --production
npm run build
pm2 reload my-app
echo "Deploy complete" chmod +x /srv/app/deploy.sh Run it from your CI/CD pipeline or a GitHub Actions workflow:
- name: Deploy
run: ssh root@$SERVER_IP '/srv/app/deploy.sh' Environment variables
Never hardcode secrets in your source code. Create a .env file on the server and
reference it in your PM2 ecosystem config:
// ecosystem.config.cjs
module.exports = {
apps: [{
name: 'my-app',
script: 'npm',
args: 'start',
instances: 'max',
exec_mode: 'cluster',
env_file: '/srv/app/.env',
}]
}; pm2 start ecosystem.config.cjs Monitoring
PM2 includes a built-in monitor:
pm2 monit On Simplewala, PM2 logs are also visible in the portal under Server → Applications without needing SSH.
Where a reload still drops requests
A rolling reload only works if your application cooperates with it. Three things commonly break the illusion:
- No graceful shutdown. If your process ignores
SIGINT, in-flight requests are killed mid-response. Listen for the signal, stop accepting new connections, finish what is open, then exit. - Readiness reported too early. If the process signals ready before its database pool and caches are warm, traffic arrives at an app that cannot serve it. Signal ready once dependencies are actually connected.
- Long-lived connections. WebSockets and SSE streams do not drain on their own. Either close them deliberately with a reconnect signal, or accept that those clients reconnect during a deploy.
Test this properly: put a request loop against the app, run a deploy, and count non-200 responses. If the number is zero, the deploy is genuinely zero-downtime. If it is not, one of the three above is the reason.
Where the container model goes further
Process managers restart your app in place, which means a bad deploy is live before you know it is bad. Container-based deploys build the new version alongside the old one, health-check it, and only shift traffic once it passes — so a broken build never serves a request, and rolling back is pointing traffic at the previous image rather than redeploying from source.
That is how deploys work on Simplewala: a push builds an image, the new container starts beside the running one, and the Front Door proxy cuts over after the health check. Failed builds simply do not go live. It is the same discipline as a careful reload, with the failure case handled for you instead of discovered in production.
Summary
- Use
-i maxto fork across all CPU cores - Use
pm2 reload(notrestart) for zero-downtime deploys - Handle
SIGINTand drain in-flight requests before exiting - Persist your process list with
pm2 save+pm2 startup - Store secrets in
.envand reference them viaenv_file - Verify with a request loop during a deploy — count the non-200s
Keep reading
Ready to simplify your hosting?
Deploy your first server in 90 seconds. All features included.
Start free