Skip to main content

Compose Deployment with Rollback

Production deployments should be validated before, verified after, and rollback-ready always. This lesson provides a deployment script that handles all three.

Deployment Flow

flowchart TD
A["1. Validate Config<br/>docker compose config"] --> B["2. Pull Images<br/>docker compose pull"]
B --> C["3. Deploy<br/>docker compose up -d"]
C --> D["4. Health Check<br/>Wait for healthy status"]
D -->|"Healthy"| E["Success ✓<br/>Save current tags"]
D -->|"Unhealthy"| F["Rollback<br/>Restore previous tags"]
F --> G["Verify rollback"]

style E fill:#e8f5e9,stroke:#2e7d32
style F fill:#ffebee,stroke:#c62828

Deployment Script

#!/usr/bin/env bash
set -euo pipefail

# === Configuration ===
COMPOSE_FILE=${COMPOSE_FILE:-"compose.yaml"}
PROD_OVERRIDE=${PROD_OVERRIDE:-"compose.prod.yaml"}
HEALTH_TIMEOUT=${HEALTH_TIMEOUT:-120} # seconds to wait for healthy
ROLLBACK_FILE=".last-good-deploy.env"
COMPOSE_CMD="docker compose -f $COMPOSE_FILE -f $PROD_OVERRIDE"

echo "=== Compose Deployment: $(date) ==="

# Step 1: Validate configuration
echo ""
echo " Step 1: Validating configuration "
if ! $COMPOSE_CMD config --quiet 2>/dev/null; then
echo "ABORT: Compose configuration is invalid"
$COMPOSE_CMD config 2>&1 | tail -5
exit 1
fi
echo "Config: Valid ✓"

# Step 2: Save current state for rollback
echo ""
echo " Step 2: Saving current state "
if docker compose ps -q 2>/dev/null | head -1 | grep -q .; then
# Save current running image tags
docker compose ps --format '{{.Service}}={{.Image}}' > "$ROLLBACK_FILE"
echo "Rollback state saved to: $ROLLBACK_FILE"
else
echo "No running services (first deploy)"
fi

# Step 3: Pull new images
echo ""
echo " Step 3: Pulling images "
$COMPOSE_CMD pull

# Step 4: Deploy
echo ""
echo " Step 4: Deploying "
$COMPOSE_CMD up -d --remove-orphans

# Step 5: Wait for health
echo ""
echo " Step 5: Health verification (timeout: ${HEALTH_TIMEOUT}s) "
ELAPSED=0
ALL_HEALTHY=false

while [[ $ELAPSED -lt $HEALTH_TIMEOUT ]]; do
UNHEALTHY=0
TOTAL=0

while IFS= read -r service; do
TOTAL=$((TOTAL + 1))
STATUS=$(docker inspect "$service" --format '{{.State.Status}}' 2>/dev/null || echo "missing")
HEALTH=$(docker inspect "$service" --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}running{{end}}' 2>/dev/null || echo "unknown")

if [[ "$STATUS" != "running" ]]; then
UNHEALTHY=$((UNHEALTHY + 1))
elif [[ "$HEALTH" == "unhealthy" ]]; then
UNHEALTHY=$((UNHEALTHY + 1))
fi
done < <($COMPOSE_CMD ps -q)

if [[ $UNHEALTHY -eq 0 ]] && [[ $TOTAL -gt 0 ]]; then
ALL_HEALTHY=true
break
fi

echo " Waiting... ($ELAPSED/${HEALTH_TIMEOUT}s) - $UNHEALTHY/$TOTAL not ready"
sleep 5
ELAPSED=$((ELAPSED + 5))
done

# Step 6: Result
echo ""
if [[ "$ALL_HEALTHY" == "true" ]]; then
echo "=== DEPLOYMENT SUCCESSFUL ==="
$COMPOSE_CMD ps

# Update rollback file with new good state
$COMPOSE_CMD ps --format '{{.Service}}={{.Image}}' > "$ROLLBACK_FILE"
else
echo "=== DEPLOYMENT FAILED - INITIATING ROLLBACK ==="

# Show what went wrong
echo ""
echo " Failure Details "
$COMPOSE_CMD ps
echo ""
echo " Recent Logs "
$COMPOSE_CMD logs --tail=20

# Rollback if we have a previous state
if [[ -f "$ROLLBACK_FILE" ]]; then
echo ""
echo " Rolling Back "
# Pull and deploy previous images
while IFS='=' read -r service image; do
echo "Restoring: $service$image"
docker pull "$image" 2>/dev/null || true
done < "$ROLLBACK_FILE"

$COMPOSE_CMD up -d --remove-orphans

echo ""
echo " Rollback Status "
$COMPOSE_CMD ps
else
echo "No rollback state available. Manual intervention required."
fi

exit 1
fi

Usage

# Standard deployment
bash deploy.sh

# With custom Compose files
COMPOSE_FILE=docker-compose.yml PROD_OVERRIDE=docker-compose.prod.yml bash deploy.sh

# With longer health timeout
HEALTH_TIMEOUT=180 bash deploy.sh

Manual Rollback

If the script's automatic rollback fails:

# 1. Check what was running before
cat .last-good-deploy.env

# 2. Manually deploy previous version
docker compose -f compose.yaml -f compose.prod.yaml down
# Edit compose files to use previous image tags
docker compose -f compose.yaml -f compose.prod.yaml up -d

# 3. Verify
docker compose ps
docker compose logs --tail=50

Deployment Checklist

StepCommandPurpose
Validatedocker compose configCatch YAML errors
Pulldocker compose pullDownload images before stopping
Deploydocker compose up -dStart new containers
Verifydocker compose psCheck all services running
Logsdocker compose logs --tail=50Check for errors
Healthdocker inspect --format '{{json .State.Health}}'Confirm healthy

Key Takeaways

  • Always validate (docker compose config) before deploying.
  • Save the current state before deploying so you can roll back.
  • Wait for health checks to pass -- a running container is not necessarily a working service.
  • Automate rollback -- if health checks fail, restore the previous version immediately.
  • Use environment variables to make the script reusable across projects.

What's Next