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.

<1s
Downtime

Atomic symlink switches mean users barely notice deployments

โšก
Instant Rollback

Bad deploy? Roll back to previous version in seconds

๐Ÿงช
Pre-Deploy Testing

Test new version on alternate port before switching

๐Ÿ“ฆ
Version Management

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

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 .

Enter your domain name without protocol
Enter application name without spaces
Enter your server username for SSH access
Enter IPv4 address of your server
Port for production application (PM2)
Port for testing new releases before switching

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 database

Deployment Flow

  1. Upload: New version goes to timestamped releases/ folder
  2. Install: Dependencies installed in isolation
  3. Test: Health check on alternate port (3102)
  4. Switch: Atomic symlink update points to new version
  5. Reload: PM2 graceful reload (<1 second downtime)
  6. Verify: Production health check
  7. Cleanup: Remove old releases (keep last 3)
Key Insight: The symlink switch is atomic - it either happens completely or not at all. This guarantees your application is never in a broken state.

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 30
Important: This migration process briefly restarts your application. Plan it during low-traffic hours. After this one-time setup, all future deployments will be zero-downtime.

Zero-Downtime Deployment Process

๐Ÿ“‹ Prerequisites: Ensure your 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.ps1

Step 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

  1. Connect to {{deploy.sudouser || 'username'}}@{{deploy.domain || deploy.ipaddress || 'your-server'}}
  2. Create folder: /tmp/app-release-YYYYMMDD_HHMMSS
  3. 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)"
Success! Your new version is now live with less than 1 second of downtime. Users barely noticed the switch, and you have instant rollback capability.

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"
Rollback Time: Usually completes in 5-10 seconds. The atomic symlink switch happens instantly, and PM2 reload adds a few seconds of graceful transition.

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-env

Production: 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-env

Troubleshooting 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
Database Backup: Always backup your database before applying migrations: 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.sh

Create 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 30
chmod +x /home/{{deploy.sudouser || 'username'}}/rollback.sh

Usage

# 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 use

PM2 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 production

Symlink 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'}}/current

Database 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:db

Best Practices

Pre-Deployment Checklist

Monitoring

Security

Maintenance

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
Bottom Line: Zero-downtime deployment transforms deployment from a scary, risky event into a routine, confident process. Your users stay happy, your developers stay productive, and your business keeps growing.