#!/bin/bash # Zero-downtime blue-green deploy. # # Builds the inactive color, starts it, waits for its healthcheck to pass, # then gracefully stops the old color. Caddy health-checks both ports # (8080 = blue, 8081 = green) and routes traffic to whichever is healthy, # so the swap is invisible to clients. # # Caveats: # - Migrations run on the new container's startup while the old one still # serves, so they must be backward-compatible (additive changes only; # destructive changes need a two-step deploy). # - During the brief overlap both containers run pull-only Obsidian sync # against the shared ./obvault mount. set -euo pipefail cd "$(dirname "$0")/.." running_services=$(docker compose ps --status running --services) if echo "$running_services" | grep -qx "raggr-blue"; then OLD=blue NEW=green elif echo "$running_services" | grep -qx "raggr-green"; then OLD=green NEW=blue else OLD="" NEW=blue fi echo "==> Active: ${OLD:-none} | Deploying: $NEW" echo "==> Building raggr-$NEW..." docker compose --profile "$NEW" build "raggr-$NEW" echo "==> Starting raggr-$NEW and waiting for healthy..." if ! docker compose --profile "$NEW" up -d --wait "raggr-$NEW"; then echo "!! raggr-$NEW failed to become healthy${OLD:+; raggr-$OLD is still serving}" >&2 docker compose --profile "$NEW" logs --tail 50 "raggr-$NEW" >&2 || true docker compose --profile "$NEW" stop -t 5 "raggr-$NEW" || true exit 1 fi if [ -n "$OLD" ]; then # Allow application run draining plus cleanup; override for operational needs. STOP_SECONDS=${DEPLOY_STOP_SECONDS:-$(( ${CHAT_RUN_DRAIN_SECONDS:-1800} + 60 ))} echo "==> raggr-$NEW healthy; draining raggr-$OLD (${STOP_SECONDS}s grace)..." docker compose --profile "$OLD" stop -t "$STOP_SECONDS" "raggr-$OLD" docker compose --profile "$OLD" rm -f "raggr-$OLD" fi echo "==> Deploy complete: raggr-$NEW is live"