S Simplewala
Node.js DevOps

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.

Simplewala Team •

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 max to fork across all CPU cores
  • Use pm2 reload (not restart) for zero-downtime deploys
  • Handle SIGINT and drain in-flight requests before exiting
  • Persist your process list with pm2 save + pm2 startup
  • Store secrets in .env and reference them via env_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