Deploying a new version of your app without kicking users out or causing errors sounds like a dream, right? Let's make it real.
The Problem
You git push, run your build, restart the server... and for 5–30 seconds, your users see:
Cannot GET /
502 Bad Gateway
Or just a blank white screen of sadness
That's downtime. In 2025, it's not acceptable.
The Setup
We'll assume:
Frontend: React (built with Vite or CRA)
Backend: Node.js + Express
Server: Linux (Ubuntu)
Process manager: PM2
The Strategy
The goal: Old version keeps running until new version is ready.
Here's the plan:
Build the new React bundle
Prepare the new Node backend
Switch traffic atomically
Handle in-flight requests gracefully
Step 1: Build in a Staging Directory
Don't overwrite your live files directly.
bash
Clone fresh or pull latest
git pull origin main
Build frontend to a timestamped folder
TIMESTAMP=$(date +%s)
npm run build -- --dest /var/www/builds/$TIMESTAMP
Copy backend files to a versioned folder
cp -r backend /var/www/backends/$TIMESTAMP
Step 2: Use PM2 Clustering (Zero-Downtime Reload)
PM2 can reload your Node app one worker at a time.
bash
Start with max CPU cores
pm2 start backend/server.js -i max --name my-api
Later, to reload zero-downtime:
pm2 reload my-api
This keeps at least one process alive while others restart.
Step 3: Atomic Symlink Swap for React
Serve your React build via Nginx. Keep a current symlink.
bash
Initial
ln -sfn /var/www/builds/old /var/www/current
After new build ready
ln -sfn /var/www/builds/$TIMESTAMP /var/www/current
Nginx config:
nginx
location / {
root /var/www/current;
try_files $uri $uri/ /index.html;
}
The symlink change is instantaneous — no copying, no downtime.
Step 4: Graceful Shutdown in Node
Your Express app must handle SIGINT properly:
javascript
const server = app.listen(PORT);
process.on('SIGINT', () => {
console.log('Closing gracefully...');
server.close(() => {
console.log('All connections closed');
process.exit(0);
});
// Force close after 10s
setTimeout(() => process.exit(1), 10000);
});
Now PM2's reload won't kill active requests.
Step 5: Put It All Together (Deploy Script)
bash
!/bin/bash
set -e
TIMESTAMP=$(date +%s)
Pull code
git pull origin main
Build backend
cp -r backend /var/www/backends/$TIMESTAMP
pm2 reload my-api --update-env
Build frontend
npm run build
cp -r dist /var/www/builds/$TIMESTAMP
Atomic swap
ln -sfn /var/www/builds/$TIMESTAMP /var/www/current
Clean old builds (keep last 5)
ls -1 /var/www/builds | head -n -5 | xargs -I {} rm -rf /var/www/builds/{}
ls -1 /var/www/backends | head -n -5 | xargs -I {} rm -rf /var/www/backends/{}
Step 6: Handle Database Migrations
Migrations are tricky. Two safe approaches:
Approach A (safer): Run migrations before deploying new code, ensuring backward compatibility.
Approach B: Run migrations during a low-traffic window with a lock file.
javascript
// Before starting new version
await runMigrations();
Step 7: Session & Auth Gotcha
If you store sessions in memory, users will be logged out after reload.
Fix: Use Redis for sessions.
javascript
const session = require('express-session');
const RedisStore = require('connect-redis')(session);
app.use(session({
store: new RedisStore({ host: 'localhost' }),
secret: 'keyboard cat',
resave: false
}));
Testing Zero-Downtime Locally
Before shipping to prod:
bash
Terminal 1: run app
pm2 start server.js -i 2
pm2 logs
Terminal 2: send constant traffic
while true; do curl http://localhost:3000; sleep 0.1; done
Terminal 3: reload
pm2 reload server
You should see zero errors and zero connection resets.
Common Pitfalls
Problem Solution
WebSocket connections drop Reconnect on client, or use Socket.IO with Redis adapter
File uploads in progress Increase graceful shutdown timeout
React app cache issues Use chunk hashing (Vite/CRA does this automatically)
Environment variables not updated Use pm2 reload --update-env
The Result
✅ Users stay logged in
✅ No 502 errors
✅ Deploy at 3 PM on a Tuesday
✅ Sleep better at night
Final Thoughts
Zero-downtime isn't magic — it's just careful swapping of assets and graceful process handling.
Start with the symlink trick for your React build + PM2 reload for Node. That covers 90% of apps.
For larger systems, add a load balancer (like HAProxy or Nginx upstreams) and do blue-green deployments. But that's a story for another day.
Have you tried this approach? What's your zero-downtime setup like? Drop a comment below!
Top comments (0)