DEV Community

Sohana Akbar
Sohana Akbar

Posted on

Zero-Downtime Deployments for a React + Node App

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)