Not every deploy needs Kubernetes. A single Node.js service on one or two VMs, managed by PM2 behind Nginx, can deploy with zero downtime if you use the right primitives — you just need to know which ones.
The problem with a naive restart
pm2 restart app kills the running process and starts a new one. Any request in flight when the process dies gets dropped, and there's a gap — however small — where nothing is listening on the port. For most apps that's a handful of failed requests per deploy. For anything with paying users watching, it's worth fixing.
PM2 cluster mode + graceful reload
Run your app in cluster mode so PM2 manages multiple worker processes behind one port:
pm2 start app.js -i max --name apiThen deploy with reload, not restart:
pm2 reload apireload restarts workers one at a time — PM2 waits for a worker to be ready before killing the next one, so there's always at least one worker accepting connections. Your app needs to handle SIGINT/SIGTERM cleanly (stop accepting new connections, finish in-flight ones, then exit) for this to actually be zero-downtime rather than just fewer-dropped-requests.
Where Nginx fits in
Nginx sits in front as the reverse proxy and load balancer across your PM2 workers (or across multiple VMs, if you're running more than one). Two settings matter most for smooth deploys:
- proxy_next_upstream: retry the request on the next upstream if the one it hit is mid-restart and refuses the connection.
- keepalive connections to upstream workers, so Nginx doesn't pay a new TCP handshake per request — this also reduces the window where a dying worker can accept a doomed connection.
What this doesn't solve
Database migrations that lock tables, or a new release that changes the request/response contract your old workers still expect — cluster reload doesn't fix either of those. Those need backward-compatible migrations and versioned APIs, which is a separate discipline from the deploy mechanics here.