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
| Step | Command | Purpose |
|---|---|---|
| Validate | docker compose config | Catch YAML errors |
| Pull | docker compose pull | Download images before stopping |
| Deploy | docker compose up -d | Start new containers |
| Verify | docker compose ps | Check all services running |
| Logs | docker compose logs --tail=50 | Check for errors |
| Health | docker 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
- Return to Strategy and Automation module overview.