Product

How to Use Blue-Green Deployment on a Single VPS

ReadyServer Team January 12, 2026 13 min read
How to Use Blue-Green Deployment on a Single VPS

Have you ever held your breath while hitting the "deploy" button, praying that your server doesn't crash while your users are actively browsing? The anxiety of downtime is a feeling every developer knows all too well. In the world of enterprise DevOps, the solution is often complex clusters and expensive load balancers. But what if you are running a lean operation on a single VPS?

Can you achieve the holy grail of zero-downtime deployment on a single Virtual Private Server?

The answer is a resounding yes. While Blue-Green deployment is traditionally associated with multi-server architectures, modern containerization tools allow us to replicate this powerful strategy on a single machine. This article will guide you through the intricacies of setting up a robust, zero-downtime deployment pipeline on a modest VPS.

Introduction: The Quest for Zero Downtime

In a traditional deployment on a single server, there is almost always a "maintenance window." You stop the old application, copy over the new files, and restart the service. During those few seconds—or minutes—your users see a 502 Bad Gateway error. This disrupts the user experience and can cost you sales.

Blue-Green deployment eliminates this gap. It ensures that you always have a working version of your application running on your VPS, even while you are deploying a new one.

What is Blue-Green Deployment?

At its core, Blue-Green deployment is a technique that reduces risk and downtime by running two identical production environments.

The "Blue" Environment (Current Live)

Think of "Blue" as your currently running version on your VPS. It is live, stable, and handling all user traffic.

The "Green" Environment (New Release)

"Green" is the new version you want to deploy. It is identical to Blue in terms of infrastructure but hosts the updated code. Crucially, the Green environment is completely idle regarding public traffic—it exists only for you to test and verify.

Once you are sure Green is working perfectly, you switch the router to point all traffic from Blue to Green.

Environment Status Traffic Port
Blue Live (current) 100% 3001
Green Staging (new) 0% 3002
After switch
Blue Retired 0% 3001
Green Live (current) 100% 3002

The Challenge of Single VPS Architecture

Usually, Blue and Green are separate clusters of servers. On a single VPS, we don't have that luxury. We have limited CPU, RAM, and disk space.

Overcoming Resource Constraints

On a single VPS hosting environment, we cannot run two heavy environments permanently—it would be wasteful. Instead, we use this strategy:

  1. Run the Blue environment permanently (handling live traffic)
  2. When deploying, spin up Green alongside Blue temporarily
  3. Once the switch is made, kill the old Blue environment to free up resources

This means your VPS only needs resources for two instances during the brief deployment window (typically 1-5 minutes).

Why Docker is Non-Negotiable

Attempting Blue-Green deployment with raw binaries or systemd services is a nightmare of file path conflicts and port collisions on your VPS.

To do this effectively on one machine, you must use Docker. Containers isolate the application environments, allowing you to run two versions of the same app on the same VPS without them fighting over dependencies. New to Docker? Start with our Docker on VPS beginner's guide.

Benefits of Docker for VPS deployment:

  • Isolation: Each version runs in its own container with separate dependencies
  • Reproducibility: Same image runs identically in staging and production
  • Port flexibility: Easy to map different host ports to each container
  • Quick cleanup: Removing old versions is a single command

Prerequisites for Your VPS

Before diving into the implementation, ensure your VPS environment is ready:

# Check Docker installation
docker --version
docker-compose --version

# Check Nginx installation
nginx -v

# Verify available resources
free -h
df -h

Requirements:

  • A VPS with enough RAM to run two copies of your app simultaneously (temporarily)
  • Docker and Docker Compose installed
  • Nginx installed on the host (acting as the reverse proxy)
  • Root or sudo access to the server

Step 1: Designing the Architecture

On a single VPS, we cannot use a hardware load balancer. Instead, we use a Reverse Proxy.

The Role of Nginx as a Gatekeeper

Nginx will listen on the public ports (80/443) on your VPS. It doesn't serve the application directly; it forwards traffic to a specific local port.

[Internet] → [Nginx :80/:443] → [App Container :3001 or :3002]

Port Management Strategy

This is the secret sauce for zero-downtime deployment on a VPS:

Container Internal Port Host Port Status
Blue (Live) 80 3001 Receiving traffic
Green (New) 80 3002 Idle, testing

Nginx is currently configured to send traffic to localhost:3001. When we deploy, we tell Nginx: "Stop sending traffic to 3001; send it to 3002 instead."

Step 2: Preparing the Docker Environment

Organize your project so that your Docker Compose configuration is dynamic. You don't want hardcoded ports in your docker-compose.yml if you want to automate VPS deployments.

Creating the Docker Compose File

Use environment variables to control the active port and container name:

# docker-compose.yml
version: '3.8'

services:
  web:
    image: myapp:${TAG:-latest}
    container_name: myapp_${COLOR:-blue}
    ports:
      - "${PORT:-3001}:80"
    environment:
      - NODE_ENV=production
      - DATABASE_URL=${DATABASE_URL}
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:80/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

When you deploy on your VPS, you define COLOR as either "blue" or "green" and PORT as "3001" or "3002".

Project Directory Structure

/opt/myapp/
├── docker-compose.yml
├── .env.blue          # Blue environment variables
├── .env.green         # Green environment variables
├── nginx/
│   └── myapp.conf     # Nginx configuration
└── deploy.sh          # Deployment script

Step 3: The Deployment Workflow

Let's walk through the actual steps of a deployment on your VPS. Assume Blue is currently live on port 3001.

Spinning Up the Green Container

Pull your new image and start the Green container on port 3002:

#!/bin/bash
# deploy.sh - Blue-Green Deployment Script for VPS

# Configuration
NEW_TAG="v2.0.0"
NEW_COLOR="green"
NEW_PORT="3002"
OLD_COLOR="blue"
OLD_PORT="3001"

echo "🚀 Starting Blue-Green Deployment on VPS..."
echo "   New version: $NEW_TAG"
echo "   New environment: $NEW_COLOR (port $NEW_PORT)"

# Step 1: Pull the new image
echo "📦 Pulling new Docker image..."
docker pull myapp:$NEW_TAG

# Step 2: Start the Green container
echo "🟢 Starting Green container..."
export TAG=$NEW_TAG
export COLOR=$NEW_COLOR
export PORT=$NEW_PORT

docker-compose -p $NEW_COLOR up -d

echo "⏳ Waiting for container to be healthy..."
sleep 10

At this moment, your VPS is working harder than usual. It is running: - The old version (handling users on port 3001) - The new version (waiting on port 3002)

Health Checking the New Version

This is where the magic happens. Because Green is running on port 3002, but the public internet only sees port 80 (via Nginx), users cannot access Green yet.

However, you can:

# Health check the new container locally
echo "🏥 Running health checks..."

# Check if the container is responding
if curl -sf http://localhost:$NEW_PORT/health > /dev/null; then
    echo "✅ Health check passed!"
else
    echo "❌ Health check failed! Aborting deployment."
    docker-compose -p $NEW_COLOR down
    exit 1
fi

# Optional: Run integration tests
# curl -sf http://localhost:$NEW_PORT/api/test

# Optional: Check database connectivity
# docker exec myapp_$NEW_COLOR php artisan migrate:status

You can also use an SSH tunnel to browse the Green version from your local machine:

# On your local machine
ssh -L 3002:localhost:3002 user@your-vps-ip
# Then open http://localhost:3002 in your browser

Step 4: The Switchover (Traffic Cutover)

If the health check passes, it is time to flip the switch on your VPS.

Nginx Configuration Setup

Your Nginx configuration file should look like this:

# /etc/nginx/sites-available/myapp.conf

upstream backend {
    server 127.0.0.1:3001;  # Currently pointing to Blue
}

server {
    listen 80;
    server_name yourdomain.com;

    location / {
        proxy_pass http://backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_cache_bypass $http_upgrade;
    }
}

Modifying the Nginx Upstream

Use a script to replace the port in your config file:

# Switch Nginx from Blue (3001) to Green (3002)
echo "🔄 Switching Nginx upstream..."

# Backup current config
cp /etc/nginx/sites-available/myapp.conf /etc/nginx/sites-available/myapp.conf.bak

# Update the port
sed -i "s/$OLD_PORT/$NEW_PORT/g" /etc/nginx/sites-available/myapp.conf

# Validate Nginx configuration
if nginx -t; then
    echo "✅ Nginx configuration valid"
else
    echo "❌ Nginx configuration invalid! Rolling back..."
    cp /etc/nginx/sites-available/myapp.conf.bak /etc/nginx/sites-available/myapp.conf
    exit 1
fi

Reloading Nginx Without Dropping Connections

Critical: Do not restart Nginx! Restarting drops connections. Instead, use the reload command:

# Graceful reload - no dropped connections
echo "🔄 Reloading Nginx..."
nginx -s reload

echo "🎉 Traffic now flowing to Green container!"

This tells Nginx to load the new configuration for new requests while allowing existing requests to finish on the old configuration. Zero downtime achieved on your VPS!

Step 5: Cleanup and Rollback

Once traffic is successfully flowing to Green, the old Blue container is now obsolete on your VPS.

Cleanup Script

# Wait a moment for any lingering requests
echo "⏳ Waiting for lingering requests to complete..."
sleep 30

# Stop and remove the old container
echo "🧹 Cleaning up old Blue container..."
docker stop myapp_$OLD_COLOR
docker rm myapp_$OLD_COLOR

# Optional: Remove old images to save disk space
echo "🗑️ Removing old Docker images..."
docker image prune -f

echo "✅ Deployment complete!"
echo "   Live environment: $NEW_COLOR (port $NEW_PORT)"

Now, "Green" effectively becomes the new "Blue" for the next deployment cycle.

Automated Rollback Strategy

What if the health check failed? Or what if you switched over and users started reporting bugs immediately?

Because you haven't killed the Blue container yet, rollback is instant:

#!/bin/bash
# rollback.sh - Emergency Rollback Script

echo "🚨 INITIATING ROLLBACK..."

LIVE_PORT="3002"    # Current (broken) version
ROLLBACK_PORT="3001" # Previous (working) version

# Revert Nginx configuration
sed -i "s/$LIVE_PORT/$ROLLBACK_PORT/g" /etc/nginx/sites-available/myapp.conf

# Validate and reload
nginx -t && nginx -s reload

echo "✅ Rollback complete! Traffic restored to previous version."

No downtime, no panic. This is the power of Blue-Green deployment on a VPS.

Handling Persistent Data and Databases

This is the most critical complexity in Blue-Green deployment on your VPS. While the application code is stateless, the database is stateful.

The Shared Database Challenge

Both the Blue (old) and Green (new) versions of your app will be connecting to the same database for a brief period during the switch:

[Blue Container] ──┐
                   ├──► [Shared Database]
[Green Container] ─┘

Database Migrations and Backward Compatibility

If your new version includes a database migration (like renaming a column), it might break the old version that is still running.

Solution: The Expand and Contract Pattern

Phase Action Both Versions Work?
Expand Add new column, keep old one ✅ Yes
Migrate Copy data to new column ✅ Yes
Deploy Deploy code using new column ✅ Yes
Contract Remove old column (next deploy) ✅ Yes

Example migration strategy:

# Phase 1: Expand (safe for both versions)
ALTER TABLE users ADD COLUMN full_name VARCHAR(255);

# Phase 2: Migrate data
UPDATE users SET full_name = CONCAT(first_name, ' ', last_name);

# Phase 3: Deploy new code that uses full_name

# Phase 4: Contract (next deployment cycle, after old version is retired)
ALTER TABLE users DROP COLUMN first_name;
ALTER TABLE users DROP COLUMN last_name;

Golden rule: Never make destructive database changes (dropping tables or columns) in a Blue-Green deployment until the old version is fully retired.

Complete Deployment Script

Here's a complete, production-ready deployment script for your VPS:

#!/bin/bash
# deploy.sh - Complete Blue-Green Deployment for VPS
set -e

# Configuration
APP_NAME="myapp"
NEW_TAG="${1:-latest}"
NGINX_CONF="/etc/nginx/sites-available/$APP_NAME.conf"
HEALTH_ENDPOINT="/health"
HEALTH_TIMEOUT=60

# Determine current and new environments
CURRENT_PORT=$(grep -oP 'server 127.0.0.1:\K[0-9]+' $NGINX_CONF)
if [ "$CURRENT_PORT" == "3001" ]; then
    NEW_COLOR="green"
    NEW_PORT="3002"
    OLD_COLOR="blue"
    OLD_PORT="3001"
else
    NEW_COLOR="blue"
    NEW_PORT="3001"
    OLD_COLOR="green"
    OLD_PORT="3002"
fi

echo "============================================"
echo "🚀 Blue-Green Deployment on VPS"
echo "============================================"
echo "   Application: $APP_NAME"
echo "   New version: $NEW_TAG"
echo "   Switching: $OLD_COLOR ($OLD_PORT) → $NEW_COLOR ($NEW_PORT)"
echo "============================================"

# Pull new image
echo "📦 Pulling Docker image..."
docker pull $APP_NAME:$NEW_TAG

# Start new container
echo "🟢 Starting $NEW_COLOR container..."
TAG=$NEW_TAG COLOR=$NEW_COLOR PORT=$NEW_PORT docker-compose -p $NEW_COLOR up -d

# Health check with timeout
echo "🏥 Running health checks (timeout: ${HEALTH_TIMEOUT}s)..."
SECONDS=0
until curl -sf http://localhost:$NEW_PORT$HEALTH_ENDPOINT > /dev/null; do
    if [ $SECONDS -ge $HEALTH_TIMEOUT ]; then
        echo "❌ Health check timeout! Rolling back..."
        docker-compose -p $NEW_COLOR down
        exit 1
    fi
    sleep 2
done
echo "✅ Health check passed!"

# Switch traffic
echo "🔄 Switching Nginx upstream..."
sed -i "s/$OLD_PORT/$NEW_PORT/g" $NGINX_CONF
nginx -t && nginx -s reload
echo "✅ Traffic switched to $NEW_COLOR"

# Cleanup
sleep 30
echo "🧹 Stopping old $OLD_COLOR container..."
docker-compose -p $OLD_COLOR down 2>/dev/null || true
docker image prune -f

echo "============================================"
echo "🎉 Deployment successful!"
echo "   Live: $NEW_COLOR ($NEW_PORT)"
echo "============================================"

Usage:

chmod +x deploy.sh
./deploy.sh v2.0.0

Conclusion: Professional Deployments on a Budget

Implementing Blue-Green deployment on a single VPS is a powerful way to professionalize your workflow without inflating your infrastructure costs. By leveraging Docker for isolation and Nginx for traffic shaping, you achieve the stability of a high-availability cluster on a budget.

Key takeaways for VPS zero-downtime deployment:

  • Docker is essential: Containers provide the isolation needed for running two versions
  • Nginx reload, don't restart: Graceful reloads prevent dropped connections
  • Health checks before switching: Never route traffic to an unhealthy container
  • Database migrations need care: Use the Expand and Contract pattern
  • Keep rollback ready: Don't remove the old version until you're confident

It requires discipline—specifically regarding database migrations and scripting the Nginx switchover—but the reward is a stress-free deployment process where "maintenance mode" screens become a thing of the past.

Ready to implement professional DevOps practices? Explore our VPS hosting plans with Docker support, full root access, and NVMe SSD storage to build your zero-downtime deployment pipeline today.

blue-green deployment vps hosting zero downtime docker deployment nginx reverse proxy devops continuous deployment vps deployment server management linux vps container deployment singapore vps

Share this article: