| |

Docker 99 🐳 Zero-Downtime Rolling Deployments and Blue-Green Container Deployment Patterns

⏱️ Estimated read: 11 min | |

Zero-downtime deployment is not a single technique but a family of patterns that share one requirement: the old version must keep serving traffic until the new version is proven ready. The two patterns that matter most in Docker environments are rolling updates, which replace replicas incrementally, and blue-green, which runs two complete environments and switches traffic atomically. The choice between them is determined by the cost of running duplicate capacity, the need for instant rollback, and whether the application can tolerate two versions running simultaneously.

Key point: Zero downtime is not automatic. A rolling update only achieves it when the service has spare capacity for the new replica to start before the old one stops, and when the application’s health check accurately reports readiness. A blue-green switch only achieves it when the proxy reload is graceful and the health check passes before the switch.


Why these patterns exist

The downtime problem. The naive deployment pattern is stop-then-start: stop the old container, start the new one, and accept the gap. In a Docker Compose stack, docker compose up -d after an image change recreates containers, and the recreation is not instantaneous. For a single-replica service, the gap is the container start time. For a service with users, that gap is downtime.

The rollback problem. A deployment that replaces the old version immediately has no rollback path except redeploying the old image. If the new version fails health checks or produces errors under real traffic, the time to detect and revert is the time the users experience the failure. Blue-green addresses this by keeping the old environment running until the new one is validated. Rolling updates address it through the orchestrator’s failure action, which can automatically roll back a failed update .

The capacity problem. Blue-green requires double the capacity during the deployment window: two full environments running side by side. Rolling updates require only enough spare capacity for one extra replica. For a resource-constrained host, rolling is the only viable option. For a service where instant rollback is worth the cost, blue-green is the better trade .


a. Rolling Updates with Docker Swarm

Docker Swarm has native support for rolling updates through the update_config block in the service definition. The key parameters are parallelism (how many replicas to update at a time), delay (how long to wait between batches), order (start-first or stop-first), and failure_action (what to do when an update fails) .

The order: start-first setting is what enables zero downtime. It starts the new replica before stopping the old one, so the service briefly has more replicas than its desired count. Without start-first, the default stop-first order stops the old replica before starting the new one, and the service is degraded during the gap.

services:
  api:
    image: myapp/api:${IMAGE_TAG}
    deploy:
      replicas: 4
      update_config:
        parallelism: 1
        delay: 15s
        order: start-first
        monitor: 60s
        failure_action: rollback
        max_failure_ratio: 0
      rollback_config:
        parallelism: 2
        delay: 5s
        order: stop-first
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 20s

The monitor window is the period after a task starts during which Swarm observes whether it stays running. If the task stops within the window, the update counts as failed, and failure_action: rollback triggers an automatic revert . The health check is what makes the monitor window meaningful: without it, Swarm only knows whether the process is running, not whether the application is ready.

A critical caveat: start-first requires spare capacity. If the host is already at its memory or CPU limit, the new replica cannot start, and the update fails. The rolling update is zero-downtime only when the infrastructure has room for the overlap .


b. Blue-Green with an Nginx Reverse Proxy

Blue-green runs two identical environments—blue and green—behind a reverse proxy. Only one is live at a time. The deployment process builds the new version in the idle color, health-checks it directly, and then switches the proxy’s upstream to the new color. The switch is a configuration file change and a graceful Nginx reload, not a container restart .

The proxy configuration uses an upstream block that points to the active color. Switching is a file swap:

# active_upstream.conf
upstream app_active {
    server app-blue:8080;
    # server app-green:8080;  # inactive
}

The deployment script detects the current live color, starts the new version in the idle color, waits for the health endpoint to return 200, swaps the upstream file, and reloads Nginx. The reload is graceful: existing connections complete, and new connections go to the new upstream .

#!/bin/bash
# deploy.sh — blue-green switch
NEW_TAG=$1
CURRENT=$(cat active_color.txt)

if [ "$CURRENT" = "blue" ]; then
    NEW_COLOR="green"
else
    NEW_COLOR="blue"
fi

# Start new version in idle color
docker compose up -d --build app-$NEW_COLOR

# Health check (10 retries, 3s apart)
for i in {1..10}; do
    if curl -sf http://app-$NEW_COLOR:8080/health; then
        break
    fi
    sleep 3
done

# Switch upstream and reload
echo "upstream app_active { server app-$NEW_COLOR:8080; }" > nginx/active_upstream.conf
docker compose exec nginx nginx -s reload

# Verify
curl -sf http://localhost/health && echo "Switched to $NEW_COLOR"
echo "$NEW_COLOR" > active_color.txt

The critical detail is that the health check must target the new color directly, bypassing the proxy. Checking through the proxy verifies the old color, not the new one. The verification after the switch confirms that traffic is actually reaching the new color .

A shared database is required for blue-green to work. If each color had its own database, the switch would lose data written during the deployment. The shared database means schema migrations must be backward-compatible: the old color must tolerate the new schema, or rollback will break .


c. Choosing Between the Patterns

The choice depends on four factors: capacity, rollback speed, database complexity, and orchestration.

FactorRolling UpdateBlue-Green
Capacity requiredOne extra replicaFull duplicate environment
Rollback speedAutomatic on failure, but only to previous imageInstant switch back to running old environment
DatabaseStandard migrationsRequires backward-compatible migrations
OrchestrationSwarm, Kubernetes, ECSAny proxy + two environments
Resource costLowHigh during deployment

For a stateless API with four replicas on a host with spare capacity, rolling is the right choice. For a service where a failed deployment must be reverted in seconds, and the budget allows running two full environments, blue-green is the right choice. For a service on a single host without Swarm, a simple blue-green with Nginx is the most direct path to zero downtime.


Complete Example Session

# ============================================
# PART 1: SWARM ROLLING UPDATE
# ============================================
docker service create \
  --name api \
  --replicas 4 \
  --update-parallelism 1 \
  --update-delay 15s \
  --update-order start-first \
  --update-failure-action rollback \
  --update-monitor 60s \
  --health-cmd "curl -f http://localhost:8000/health || exit 1" \
  --health-interval 10s \
  --health-retries 3 \
  myregistry/api:v1.0.0

# Deploy new version
docker service update --image myregistry/api:v1.1.0 api

# Monitor
docker service ps api --filter desired-state=running


# ============================================
# PART 2: BLUE-GREEN WITH NGINX
# ============================================
# Initial state: blue is live
# nginx/active_upstream.conf:
#   upstream app_active { server app-blue:8080; }


# ============================================
# PART 3: DEPLOY NEW VERSION TO GREEN
# ============================================
docker compose up -d --build app-green


# ============================================
# PART 4: HEALTH CHECK GREEN DIRECTLY
# ============================================
for i in {1..10}; do
  if curl -sf http://localhost:8082/health; then
    echo "Green is healthy"
    break
  fi
  sleep 3
done


# ============================================
# PART 5: SWITCH UPSTREAM
# ============================================
echo "upstream app_active { server app-green:8080; }" > nginx/active_upstream.conf
docker compose exec nginx nginx -s reload


# ============================================
# PART 6: VERIFY TRAFFIC
# ============================================
curl -s http://localhost/health
# Should return green's response


# ============================================
# PART 7: ROLLBACK IF NEEDED
# ============================================
echo "upstream app_active { server app-blue:8080; }" > nginx/active_upstream.conf
docker compose exec nginx nginx -s reload


# ============================================
# PART 8: ZERO-DOWNTIME PROOF
# ============================================
# In a separate terminal, run:
while true; do curl -s -o /dev/null -w "%{http_code} " http://localhost/; sleep 0.2; done
# Run the deploy script
# The output should be an unbroken stream of 200s

The ten parts trace the two patterns. Parts 1 shows the Swarm rolling update with the key flags. Parts 3 through 6 show the blue-green switch with the health check and the proxy reload.


Quick Reference

Swarm Rolling Update Flags

FlagPurpose
--update-parallelismReplicas updated at once
--update-delayWait between batches
--update-order start-firstNew replica before old stops
--update-failure-action rollbackAuto-rollback on failure
--update-monitorObservation window after start

Blue-Green Components

ComponentPurpose
Two environmentsBlue and green, identical
Reverse proxyNginx with upstream switch
Health endpointDirect check on new color
Shared databaseData persistence across switch
Graceful reloadnginx -s reload

When to Use Which

ScenarioPattern
Spare capacity limitedRolling
Instant rollback requiredBlue-green
No orchestratorBlue-green with Nginx
Database migrationsRolling (simpler)
Full validation before switchBlue-green

Best Practices

✅ Do This:

# Use start-first for zero-downtime rolling
order: start-first                                                    # ✅
# Health check the new color directly, bypassing the proxy
curl -sf http://app-green:8080/health                                 # ✅
# Use graceful reload for the proxy switch
nginx -s reload                                                       # ✅
# Keep the old color running until the new one is verified
# Instant rollback is the point of blue-green                          # ✅

❌ Don’t Do This:

# Use stop-first and expect zero downtime
order: stop-first  # old stops before new starts                      # ❌
# Health check through the proxy during blue-green deploy
# This checks the old color, not the new one                           # ❌
# Run incompatible schema migrations during blue-green
# The old color must tolerate the new schema for rollback              # ❌
# Assume start-first works without spare capacity
# The new replica cannot start if the host is full                     # ❌

Common Pitfalls

PitfallWhy It HappensFix
Rolling update causes downtimestop-first order usedUse start-first
New replica never becomes healthyInsufficient capacityAdd capacity or reduce replicas
Blue-green switch causes errorsProxy not reloaded gracefullyUse nginx -s reload
Rollback failsDatabase schema incompatibleBackward-compatible migrations
Health check passes but app is brokenHealth check too shallowCheck a real dependency

Real-World Examples

1. Swarm Rolling Update

docker service update --image myapp:v2 --update-order start-first myapp

2. Swarm Auto-Rollback

failure_action: rollback

3. Blue-Green Upstream File

upstream app_active { server app-green:8080; }

4. Graceful Nginx Reload

nginx -s reload

5. Direct Health Check

curl -sf http://app-green:8080/health

6. Rollback Switch

upstream app_active { server app-blue:8080; }

7. Shared Database

# Both colors point to the same DB service

8. Zero-Downtime Test

while true; do curl -s -o /dev/null -w "%{http_code} " http://localhost/; sleep 0.2; done

9. Health Check in Compose

healthcheck:
  test: ["CMD", "curl", "-f", "http://localhost:8080/health"]

10. Monitor Window

monitor: 60s

Visual

Rolling Update with start-first

┌──────────────────────────────────────────────────────────────┐
│  BEFORE: 4 replicas of v1                                     │
│  ┌────┐ ┌────┐ ┌────┐ ┌────┐                                 │
│  │ v1 │ │ v1 │ │ v1 │ │ v1 │                                 │
│  └────┘ └────┘ └────┘ └────┘                                 │
│                                                              │
│  STEP 1: start v2, keep v1 (5 replicas briefly)               │
│  ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐                         │
│  │ v1 │ │ v1 │ │ v1 │ │ v1 │ │ v2 │  ← new starts          │
│  └────┘ └────┘ └────┘ └────┘ └────┘                         │
│                                                              │
│  STEP 2: stop one v1 (back to 4)                              │
│  ┌────┐ ┌────┐ ┌────┐ ┌────┐                                 │
│  │ v1 │ │ v1 │ │ v1 │ │ v2 │                                 │
│  └────┘ └────┘ └────┘ └────┘                                 │
│                                                              │
│  REPEAT until all are v2.                                    │
│  The service never drops below 4 replicas.                   │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Blue-Green Switch

┌──────────────────────────────────────────────────────────────┐
│  BEFORE                                                       │
│  ┌─────────────────────────────────────────────────────────┐│
│  │ Nginx upstream → blue                                   ││
│  └─────────────────────────────────────────────────────────┘│
│  ┌─────────┐  ┌─────────┐                                   │
│  │ BLUE v1 │  │ GREEN   │  ← idle, no traffic             │
│  │ (live)  │  │ (idle)  │                                   │
│  └─────────┘  └─────────┘                                   │
│                                                              │
│  DEPLOY: build v2 in green, health-check green directly      │
│  ┌─────────┐  ┌─────────┐                                   │
│  │ BLUE v1 │  │ GREEN v2│  ← healthy, ready                │
│  │ (live)  │  │ (ready) │                                   │
│  └─────────┘  └─────────┘                                   │
│                                                              │
│  SWITCH: swap upstream file, nginx -s reload                 │
│  ┌─────────────────────────────────────────────────────────┐│
│  │ Nginx upstream → green                                  ││
│  └─────────────────────────────────────────────────────────┘│
│  ┌─────────┐  ┌─────────┐                                   │
│  │ BLUE v1 │  │ GREEN v2│  ← now live                     │
│  │ (idle)  │  │ (live)  │                                   │
│  └─────────┘  └─────────┘                                   │
│                                                              │
│  ROLLBACK: swap back, reload. Blue is still running.         │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Zero-Downtime Proof

┌──────────────────────────────────────────────────────────────┐
│  Terminal 1: continuous requests                             │
│  while true; do curl -s -o /dev/null -w "%{http_code} ";      │
│    sleep 0.2; done                                           │
│                                                              │
│  Terminal 2: deploy script                                   │
│  ./deploy.sh v2                                              │
│                                                              │
│  Output of Terminal 1 during deploy:                          │
│  200 200 200 200 200 200 200 200 200 200 200 200 200 200     │
│                                                              │
│  No 5xx. No connection refused. No gap.                      │
│  That is the definition of zero downtime.                    │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
Rolling updateIncremental replica replacement
start-firstNew replica before old stops
stop-firstOld stops before new starts (downtime)
failure_action: rollbackAuto-revert on failed update
Blue-greenTwo environments, proxy switch
Proxy switchUpstream file swap + graceful reload
Health checkMust target the new environment directly
Shared databaseRequired for blue-green
RollbackInstant in blue-green; image revert in rolling

Key takeaways:

  • Rolling updates are zero-downtime only with start-first and spare capacity. The default stop-first order stops the old replica before starting the new one, which creates a gap. start-first starts the new replica first, but it requires the host to have room for the overlap .
  • Blue-green requires a reverse proxy and a shared database. The proxy’s upstream is switched from the old color to the new color after the new color passes health checks. The shared database ensures that data written during the deployment is not lost when the switch happens .
  • The health check must target the new environment directly. Checking through the proxy during a blue-green deployment verifies the old environment, not the new one. The check must bypass the proxy and hit the new color’s health endpoint .
  • Swarm’s failure_action: rollback automates the revert. When an updated task fails within the monitor window, Swarm rolls back to the previous image automatically. The monitor window and the health check together determine whether the new version is considered healthy .
  • Blue-green rollback is a proxy switch; rolling rollback is an image revert. Blue-green keeps the old environment running, so rollback is a file swap and a reload. Rolling has already replaced the old image, so rollback means redeploying the previous image, which takes longer.

Remember: Zero downtime is a property of the deployment process, not a property of Docker. A rolling update without start-first is a stop-and-start with extra steps. A blue-green switch without a direct health check is a switch to an unverified environment. The patterns provide the mechanism; the configuration determines whether the mechanism achieves the goal. The test is the continuous request loop during the deployment: if every request returns 200, the deployment is zero-downtime. If any request fails, the configuration has a gap, and the gap is in the order of operations, the health check, or the proxy reload.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!