Deploy Without Fear, Roll Back in Seconds
Stop losing customers during deployments. The blue-green deployment strategy gives you <1 second downtime , instant rollback capability, and the confidence to deploy any time of day.
Atomic symlink switches mean users barely notice deployments
Bad deploy? Roll back to previous version in seconds
Test new version on alternate port before switching
Keep multiple releases, clean up automatically
Why Zero-Downtime Matters
Traditional deployments kill your running application, upload new files, and restart. During those 5-10 seconds, your users see error pages. For high-traffic applications, that's hundreds of frustrated users per deployment.
The Cost of Downtime
- Lost Revenue: E-commerce sites lose $5,600 per minute of downtime
- User Trust: 88% of users won't return after a bad experience
- SEO Impact: Search engines penalize sites with frequent outages
- Team Stress: Fear of breaking production slows development
Traditional vs Zero-Downtime Comparison
| Feature | Traditional Deploy | Zero-Downtime Deploy | 
|---|---|---|
| Downtime | 5-10 seconds | <1 second | 
| Rollback Time | 5-10 minutes | 10 seconds | 
| Pre-Deploy Testing | No testing | Health checks | 
| Risk Level | High (no safety net) | Low (instant rollback) | 
| Database Safety | Per-deploy risk | Shared & consistent | 
| Deploy Confidence | Deploy anxiety | Deploy anytime | 
๐ฏ Personalize This Guide
Enter your server details below to customize all commands throughout this guide. Your values will appear in green highlights .
How Blue-Green Deployment Works
Instead of overwriting your current application, blue-green deployment creates a new version alongside the old one, tests it, then atomically switches via symlink.
Directory Structure
/var/www/{{deploy.domain || 'your-domain.com'}}/
โโโ releases/
โ   โโโ 20251014_180000/  โ new version (green)
โ   โโโ 20251014_120000/  โ current (blue)
โ   โโโ 20251013_150000/  โ backup
โโโ current -> releases/20251014_120000/  โ symlink
โโโ shared/
    โโโ .env              โ shared config
    โโโ db.sqlite         โ shared databaseDeployment Flow
- Upload: New version goes to timestamped releases/ folder
- Install: Dependencies installed in isolation
- Test: Health check on alternate port (3102)
- Switch: Atomic symlink update points to new version
- Reload: PM2 graceful reload (<1 second downtime)
- Verify: Production health check
- Cleanup: Remove old releases (keep last 3)
One-Time Setup
          Before using zero-downtime deployments, you need to migrate your existing deployment to
          the blue-green structure. This is a one-time process.
          
          After this, you can follow the
          
            Quick Guide
          
          for all future deployments.
        
Migration Script
# SSH into your server
ssh {{deploy.sudouser || 'username'}}@{{deploy.domain || deploy.ipaddress || 'your-server'}}# Create directory structure
mkdir -p /var/www/{{deploy.domain || 'your-domain.com'}}/{releases,shared}
# Move existing deployment to versioned release
VERSION=$(date +%Y%m%d_%H%M%S)
if [ -d "/var/www/{{deploy.domain || 'your-domain.com'}}/deploy-folder" ]; then
  mv /var/www/{{deploy.domain || 'your-domain.com'}}/deploy-folder "/var/www/{{deploy.domain || 'your-domain.com'}}/releases/$VERSION"
  echo "โ
 Migrated deploy-folder to releases/$VERSION"
fi
# Move .env to shared location
if [ -f "/var/www/{{deploy.domain || 'your-domain.com'}}/releases/$VERSION/.env" ]; then
  cp "/var/www/{{deploy.domain || 'your-domain.com'}}/releases/$VERSION/.env" /var/www/{{deploy.domain || 'your-domain.com'}}/shared/.env
  ln -sf /var/www/{{deploy.domain || 'your-domain.com'}}/shared/.env "/var/www/{{deploy.domain || 'your-domain.com'}}/releases/$VERSION/.env"
  echo "โ
 Moved .env to shared location"
fi
# Move database to shared location (if using SQLite)
if [ -f "/var/www/{{deploy.domain || 'your-domain.com'}}/releases/$VERSION/prisma/db.sqlite" ]; then
  mkdir -p /var/www/{{deploy.domain || 'your-domain.com'}}/shared/prisma
  mv "/var/www/{{deploy.domain || 'your-domain.com'}}/releases/$VERSION/prisma/db.sqlite" /var/www/{{deploy.domain || 'your-domain.com'}}/shared/db.sqlite
  ln -sf /var/www/{{deploy.domain || 'your-domain.com'}}/shared/db.sqlite "/var/www/{{deploy.domain || 'your-domain.com'}}/releases/$VERSION/prisma/db.sqlite"
  echo "โ
 Moved database to shared location"
fi
# Create current symlink
ln -sfn "/var/www/{{deploy.domain || 'your-domain.com'}}/releases/$VERSION" /var/www/{{deploy.domain || 'your-domain.com'}}/current
echo "โ
 Created current symlink"
# Update PM2 to use symlinked path
cd /var/www/{{deploy.domain || 'your-domain.com'}}/current
pm2 delete {{deploy.appname || 'app-name'}} || true
pm2 start ecosystem.config.cjs --env production
pm2 save
echo "โ
 PM2 configured to use symlinked deployment"
# Verify PM2 is using the symlink path
pm2 describe {{deploy.appname || 'app-name'}} | grep cwd
echo "Should show: /var/www/{{deploy.domain || 'your-domain.com'}}/current"
# Verify everything works
curl -I http://127.0.0.1:{{deploy.prodport || '3101'}}/
pm2 logs {{deploy.appname || 'app-name'}} --lines 30Zero-Downtime Deployment Process
ecosystem.config.cjs
          has
          cwd: "/var/www/your-domain.com/current"
          (hardcoded symlink path). This allows PM2 to follow symlink changes without needing
          pm2 delete
          .
        Step 1: Local Build (Windows)
cd C:\www\your-project
$env:NETLIFY_BLOBS = 'false'
pnpm build
powershell -ExecutionPolicy Bypass -File prepare-deploy.ps1Step 2: Upload to Server
Option A: Command Line (WSL/Git Bash)
# Set version timestamp
$VERSION = Get-Date -Format "yyyyMMdd_HHmmss"
scp -r C:\www\project\deploy-folder\* {{deploy.sudouser || 'username'}}@{{deploy.domain || deploy.ipaddress || 'your-server'}}:/tmp/app-release-$VERSION/Option B: FileZilla/WinSCP
- 
            Connect to
            {{deploy.sudouser || 'username'}}@{{deploy.domain || deploy.ipaddress || 'your-server'}}
- 
            Create folder:
            /tmp/app-release-YYYYMMDD_HHMMSS
- 
            Upload contents of
            C:\www\project\deploy-folder\*
Step 3: Deploy on Server (The Magic Happens)
# SSH into server
ssh {{deploy.sudouser || 'username'}}@{{deploy.domain || deploy.ipaddress || 'your-server'}}# Set version (use same timestamp as upload)
VERSION=$(ls -t /tmp/app-release-* | head -1 | sed 's|/tmp/app-release-||')
DEPLOY_DIR="/var/www/{{deploy.domain || 'your-domain.com'}}/releases/$VERSION"
# Move uploaded files to releases directory
mkdir -p "$DEPLOY_DIR"
mv "/tmp/app-release-$VERSION"/* "$DEPLOY_DIR/"
rm -rf "/tmp/app-release-$VERSION"
# Install dependencies
cd "$DEPLOY_DIR"
nvm use 20.18.1
pnpm install --prod --frozen-lockfile
pnpm dlx prisma generate
# ๐๏ธ Sync database schema (handles Prisma changes)
echo "Syncing database schema..."
pnpm dlx prisma db push || echo "โ ๏ธ  Schema already in sync"
pnpm run preflight || true
pnpm run preflight:db || true
mkdir -p logs && chmod 775 logs
# Link shared resources
ln -sf /var/www/{{deploy.domain || 'your-domain.com'}}/shared/.env "$DEPLOY_DIR/.env"
ln -sf /var/www/{{deploy.domain || 'your-domain.com'}}/shared/db.sqlite "$DEPLOY_DIR/prisma/db.sqlite" 2>/dev/null || true
# ๐งช CRITICAL: Test new release on alternate port
PORT={{deploy.testport || '3102'}} node .output/server/index.mjs &
TEST_PID=$!
sleep 5
if curl -f -I http://127.0.0.1:{{deploy.testport || '3102'}}/ > /dev/null 2>&1; then
  echo "โ
 New release passed health check on port {{deploy.testport || '3102'}}"
  kill $TEST_PID
else
  echo "โ New release FAILED health check - ABORTING deployment"
  kill $TEST_PID 2>/dev/null || true
  exit 1
fi
# โก ATOMIC SWITCH: Update symlink to new release
ln -sfn "$DEPLOY_DIR" /var/www/{{deploy.domain || 'your-domain.com'}}/current
echo "โ
 Symlink switched to new release"
# โป๏ธ Graceful PM2 restart (follows symlink, <1 second downtime)
pm2 restart {{deploy.appname || 'app-name'}} --update-env
pm2 save
# โ๏ธ Verify production deployment
sleep 2
echo "Checking production endpoint..."
curl -I http://127.0.0.1:{{deploy.prodport || '3101'}}/
pm2 logs {{deploy.appname || 'app-name'}} --lines 30
# ๐งน Clean old releases (keep last 3)
cd /var/www/{{deploy.domain || 'your-domain.com'}}/releases
KEEP_COUNT=3
RELEASE_COUNT=$(ls -1 | wc -l)
if [ $RELEASE_COUNT -gt $KEEP_COUNT ]; then
  ls -t | tail -n +$((KEEP_COUNT + 1)) | xargs -r rm -rf
  echo "โ
 Cleaned old releases (kept last $KEEP_COUNT)"
fi
echo ""
echo "๐ Zero-downtime deployment complete!"
echo "Current release: $VERSION"
echo "Active releases: $(ls -1 /var/www/{{deploy.domain || 'your-domain.com'}}/releases | wc -l)"Instant Rollback (When Things Go Wrong)
Made a mistake? No problem. Rolling back to the previous version takes less than 10 seconds and requires just a few commands.
# SSH into server (if not already connected)
ssh {{deploy.sudouser || 'username'}}@{{deploy.domain || deploy.ipaddress || 'your-server'}}# List available releases
echo "Available releases:"
ls -lt /var/www/{{deploy.domain || 'your-domain.com'}}/releases/
# Get current and previous versions
CURRENT=$(readlink /var/www/{{deploy.domain || 'your-domain.com'}}/current | sed 's|.*/||')
PREVIOUS=$(ls -t /var/www/{{deploy.domain || 'your-domain.com'}}/releases/ | grep -v "^${CURRENT}$" | head -1)
echo "Current version: $CURRENT"
echo "Rolling back to: $PREVIOUS"
# โก Atomic rollback (instant symlink switch)
ln -sfn "/var/www/{{deploy.domain || 'your-domain.com'}}/releases/$PREVIOUS" /var/www/{{deploy.domain || 'your-domain.com'}}/current
# Restart PM2 (follows symlink to previous version)
pm2 restart {{deploy.appname || 'app-name'}} --update-env
# Verify rollback succeeded
sleep 2
curl -I http://127.0.0.1:{{deploy.prodport || '3101'}}/
pm2 logs {{deploy.appname || 'app-name'}} --lines 30
echo "โ
 Rolled back to $PREVIOUS"Database Migration Handling
Database changes require special attention in zero-downtime deployments. Here's how to handle Prisma schema changes safely.
Development: Quick Schema Sync
cd /var/www/{{deploy.domain || 'your-domain.com'}}/current
pnpm dlx prisma db push
pnpm dlx prisma generate
pm2 restart {{deploy.appname || 'app-name'}} --update-envProduction: Proper Migrations (Recommended)
# On your local machine (Windows) - create migration
cd C:\www\your-project
pnpm dlx prisma migrate dev --name add_user_fields
# Include migrations in deployment (prepare-deploy.ps1 does this automatically)
# On server - apply migrations during deployment
cd /var/www/{{deploy.domain || 'your-domain.com'}}/current
pnpm dlx prisma migrate deploy
pnpm dlx prisma generate
pm2 restart {{deploy.appname || 'app-name'}} --update-envTroubleshooting Schema Mismatches
          If you see Prisma errors like
          column does not exist
          :
        
# Check migration status
cd /var/www/{{deploy.domain || 'your-domain.com'}}/current
pnpm dlx prisma migrate status
# Option 1: Sync schema (development/staging)
pnpm dlx prisma db push
# Option 2: Apply migrations (production)
pnpm dlx prisma migrate deploy
# Regenerate client and restart
pnpm dlx prisma generate
pm2 restart {{deploy.appname || 'app-name'}} --update-env
# Verify in logs
pm2 logs {{deploy.appname || 'app-name'}} --lines 50
            cp /var/www/
            {{deploy.domain || 'your-domain.com'}}
            /shared/db.sqlite /var/www/
            {{deploy.domain || 'your-domain.com'}}
            /shared/db.sqlite.backup
          
        Automated Deployment Script
Save time with this automated script that handles the entire zero-downtime deployment process. Just upload your files and run one command.
Create the Script
# On your server, create the deployment script
nano /home/{{deploy.sudouser || 'username'}}/deploy-zero-downtime.sh#!/bin/bash
set -e
# Configuration
BASE_DIR="/var/www/{{deploy.domain || 'your-domain.com'}}"
SHARED_DIR="$BASE_DIR/shared"
RELEASES_DIR="$BASE_DIR/releases"
CURRENT_LINK="$BASE_DIR/current"
KEEP_RELEASES=3
TEST_PORT={{deploy.testport || '3102'}}
PROD_PORT={{deploy.prodport || '3101'}}
APP_NAME="{{deploy.appname || 'app-name'}}"
# Generate version timestamp
VERSION=$(date +%Y%m%d_%H%M%S)
DEPLOY_DIR="$RELEASES_DIR/$VERSION"
echo "๐ Starting zero-downtime deployment"
echo "Version: $VERSION"
echo ""
# Check if uploaded files exist
if [ ! -d "/tmp/app-release-$VERSION" ]; then
  echo "โ No uploaded files found at /tmp/app-release-$VERSION"
  echo "Please upload build first!"
  exit 1
fi
# Create release directory
mkdir -p "$DEPLOY_DIR"
mv "/tmp/app-release-$VERSION"/* "$DEPLOY_DIR/"
rm -rf "/tmp/app-release-$VERSION"
echo "โ
 Moved uploaded files to release directory"
# Install dependencies
cd "$DEPLOY_DIR"
echo "๐ฆ Installing dependencies..."
nvm use 20.18.1
pnpm install --prod --frozen-lockfile
pnpm dlx prisma generate
# Sync database schema
echo "๐๏ธ  Syncing database schema..."
if pnpm dlx prisma db push 2>&1 | grep -q "already in sync"; then
  echo "โ
 Schema already in sync"
else
  echo "โ
 Schema synced successfully"
fi
pnpm run preflight || true
pnpm run preflight:db || true
mkdir -p logs && chmod 775 logs
# Link shared resources
echo "๐ Linking shared resources..."
ln -sf "$SHARED_DIR/.env" "$DEPLOY_DIR/.env"
[ -f "$SHARED_DIR/db.sqlite" ] && ln -sf "$SHARED_DIR/db.sqlite" "$DEPLOY_DIR/prisma/db.sqlite"
# Health check on test port
echo "๐งช Testing new release on port $TEST_PORT..."
PORT=$TEST_PORT node .output/server/index.mjs &
TEST_PID=$!
sleep 5
if curl -f -I http://127.0.0.1:$TEST_PORT/ > /dev/null 2>&1; then
  echo "โ
 Health check PASSED"
  kill $TEST_PID
else
  echo "โ Health check FAILED - Aborting deployment"
  kill $TEST_PID 2>/dev/null || true
  rm -rf "$DEPLOY_DIR"
  exit 1
fi
# Atomic switch
echo "๐ Switching to new release..."
ln -sfn "$DEPLOY_DIR" "$CURRENT_LINK"
# Graceful PM2 restart (follows symlink)
echo "โป๏ธ  Restarting PM2 (graceful, <1s downtime)..."
pm2 restart $APP_NAME --update-env
pm2 save
# Verify production
sleep 2
echo "โ๏ธ  Verifying production endpoint..."
if curl -f -I http://127.0.0.1:$PROD_PORT/ > /dev/null 2>&1; then
  echo "โ
 Production endpoint healthy"
else
  echo "โ ๏ธ  Production endpoint not responding"
fi
# Clean old releases
cd "$RELEASES_DIR"
RELEASE_COUNT=$(ls -1 | wc -l)
if [ $RELEASE_COUNT -gt $KEEP_RELEASES ]; then
  echo "๐งน Cleaning old releases (keeping last $KEEP_RELEASES)..."
  ls -t | tail -n +$((KEEP_RELEASES + 1)) | xargs -r rm -rf
fi
echo ""
echo "๐ Deployment complete!"
echo "๐ฆ Version: $VERSION"
echo "๐ Active releases: $(ls -1 "$RELEASES_DIR" | wc -l)"
echo "๐ Current: $(readlink "$CURRENT_LINK" | sed 's|.*/||')"
echo ""
echo "View logs: pm2 logs $APP_NAME --lines 50"
echo "Rollback: ./rollback.sh"Make Script Executable
chmod +x /home/{{deploy.sudouser || 'username'}}/deploy-zero-downtime.shCreate Rollback Script
nano /home/{{deploy.sudouser || 'username'}}/rollback.sh#!/bin/bash
set -e
BASE_DIR="/var/www/{{deploy.domain || 'your-domain.com'}}"
CURRENT_LINK="$BASE_DIR/current"
RELEASES_DIR="$BASE_DIR/releases"
APP_NAME="{{deploy.appname || 'app-name'}}"
CURRENT=$(readlink "$CURRENT_LINK" | sed 's|.*/||')
PREVIOUS=$(ls -t "$RELEASES_DIR" | grep -v "^${CURRENT}$" | head -1)
if [ -z "$PREVIOUS" ]; then
  echo "โ No previous release found for rollback"
  exit 1
fi
echo "๐ Rolling back deployment"
echo "Current:  $CURRENT"
echo "Previous: $PREVIOUS"
echo ""
# Atomic rollback
ln -sfn "$RELEASES_DIR/$PREVIOUS" "$CURRENT_LINK"
echo "โ
 Symlink switched to previous release"
# Restart PM2 (follows symlink)
pm2 restart $APP_NAME --update-env
# Verify
sleep 2
if curl -f -I http://127.0.0.1:{{deploy.prodport || '3101'}}/ > /dev/null 2>&1; then
  echo "โ
 Rollback successful!"
  echo "Active version: $PREVIOUS"
else
  echo "โ ๏ธ  Rollback completed but endpoint not responding"
fi
pm2 logs $APP_NAME --lines 30chmod +x /home/{{deploy.sudouser || 'username'}}/rollback.shUsage
# Deploy
./deploy-zero-downtime.sh
# Rollback if needed
./rollback.sh
# Check status
ls -lt /var/www/{{deploy.domain || 'your-domain.com'}}/releases/
readlink /var/www/{{deploy.domain || 'your-domain.com'}}/current
pm2 logs {{deploy.appname || 'app-name'}}Troubleshooting
Health Check Fails
# Check what's wrong with the new release
cd /var/www/{{deploy.domain || 'your-domain.com'}}/releases/[VERSION]
node .output/server/index.mjs
# Check console output for errors
# Common issues:
# - Missing .env file (check symlink)
# - Database connection issues
# - Missing node_modules (reinstall)
        # - Port already in usePM2 Restart Fails
# Check PM2 status
pm2 list# Check PM2 logs
pm2 logs {{deploy.appname || 'app-name'}} --lines 100
# Force restart if needed
pm2 restart {{deploy.appname || 'app-name'}}
# Reset PM2 if corrupted
pm2 kill
pm2 start ecosystem.config.cjs --env productionSymlink Issues
# Check current symlink
ls -la /var/www/{{deploy.domain || 'your-domain.com'}}/current
# Manually fix symlink
ln -sfn /var/www/{{deploy.domain || 'your-domain.com'}}/releases/[VERSION] /var/www/{{deploy.domain || 'your-domain.com'}}/current
# Verify it points to the right place
readlink /var/www/{{deploy.domain || 'your-domain.com'}}/currentDatabase Migration Issues
# Check migration status
cd /var/www/{{deploy.domain || 'your-domain.com'}}/current
pnpm dlx prisma migrate status
# Reset migration if needed (DANGER: development only)
pnpm dlx prisma migrate reset --force
# Apply specific migration
pnpm dlx prisma migrate deploy
# Check database connection
pnpm run preflight:dbBest Practices
Pre-Deployment Checklist
- โ Test locally with production environment variables
- โ Run all tests and linting
- โ Verify database migrations work
- โ Check for breaking changes in dependencies
- โ Backup database before major migrations
- โ Deploy during low-traffic hours for first few times
Monitoring
- Monitor PM2 logs during and after deployment
- Check application performance metrics
- Verify all features work as expected
- Set up alerts for 5xx errors or high response times
Security
- Keep shared .env file secure (600 permissions)
- Don't commit sensitive data to git
- Regularly update dependencies for security patches
- Use SSH keys instead of passwords for server access
Maintenance
- Clean old releases regularly (script does this automatically)
- Monitor disk space usage
- Keep PM2 updated
- Backup database regularly
- Test rollback procedure periodically
Why This Approach Wins
For Developers
- Confidence: Deploy anytime without fear
- Speed: Instant rollback saves hours of debugging
- Testing: Verify deployment before users see it
- Peace of Mind: Sleep better knowing rollback is one command away
For Business
- Revenue: No lost sales during deployments
- Reputation: Customers never see broken pages
- Velocity: Deploy features faster with less risk
- Competitive Edge: Ship updates while competitors hesitate
