Zero-downtime Docker deploys with health checks

I don’t need Kubernetes for a 3-node homelab. But I do want zero-downtime deploys. Here’s the bash script I use — pulls new image, swaps container atomically, rolls back if the new container fails health check.

The script

#!/bin/bash
set -euo pipefail

SERVICE="${1:?usage: deploy.sh <service> <image>}"
IMAGE="${2:?usage: deploy.sh <service> <image>}"
COMPOSE_DIR="/srv/compose/${SERVICE}"
HEALTH_URL="${HEALTH_URL:-http://localhost:8080/health}"
OLD_CONTAINER="${SERVICE}_app_1"

cd "$COMPOSE_DIR"

# Save current image for rollback
CURRENT=$(docker inspect --format='{{.Image}}' "$OLD_CONTAINER" 2>/dev/null || echo "")

echo "▶ Pulling $IMAGE"
docker pull "$IMAGE"

# Update image in compose and start new container alongside old
echo "▶ Starting new container"
IMAGE="$IMAGE" docker compose up -d --no-deps --scale app=2 app

# Wait for new container to be healthy
echo "▶ Waiting for health check"
NEW_CONTAINER="${SERVICE}_app_2"
RETRIES=30
until docker inspect --format='{{.State.Health.Status}}' "$NEW_CONTAINER" 2>/dev/null | grep -q healthy; do
  RETRIES=$((RETRIES-1))
  if [ $RETRIES -le 0 ]; then
    echo "✗ Health check failed, rolling back"
    docker compose up -d --no-deps --scale app=1 app
    exit 1
  fi
  sleep 2
done

# Stop old container
echo "▶ Stopping old container"
docker compose up -d --no-deps --scale app=1 app

echo "✓ Deployed $SERVICE"

How to use

./deploy.sh myapp myuser/myapp:v1.2.3

Why not docker-rollout / kubectl?

  • Simpler: One file, ~30 lines, no Go binary
  • No state: Stateless nodes can run this without a cluster manager
  • Rollback included: Keeps old container until health check passes
  • Auditable: Plain bash, every step is visible

Limitations (be honest)

  • Single-node only (fine for my homelab, not for prod at scale)
  • No blue/green across multiple hosts
  • Health check is HTTP-only — for TCP services you’d want to extend it

For anything bigger, use Kubernetes. For a homelab or a single production host, this is enough.