From 59e7517747f4af3780b3366e7fdb1661aa219795 Mon Sep 17 00:00:00 2001 From: Emi Griffith Date: Wed, 22 Jul 2026 15:48:23 -0700 Subject: [PATCH] deploy.sh: incoming image tag wins over .image-tags.env; health via container healthcheck Two bugs surfaced wiring dev auto-deploy: 1. Sourcing .image-tags.env clobbered the caller's incoming *_IMAGE_TAG. The first backend-only deploy persists FRONTEND_IMAGE_TAG=local (sibling unknown); the next frontend deploy then sourced that and pulled :local -> 'manifest unknown'. Now the incoming env is captured before sourcing and re-applied. 2. Health-checked services via a host-port curl, but the dev overlay leaves the frontend port unpublished (reached via the backend proxy) -> false failure. Now polls each container's own HEALTHCHECK status via docker inspect. --- deploy/deploy.sh | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/deploy/deploy.sh b/deploy/deploy.sh index 6580ec3..87295b1 100755 --- a/deploy/deploy.sh +++ b/deploy/deploy.sh @@ -114,9 +114,19 @@ export REGISTRY_HOST BACKEND_IMAGE_PATH FRONTEND_IMAGE_PATH # service being deployed then overrides its line below. This file is untracked # (see .gitignore), so `git reset --hard` above leaves it in place. TAGS_FILE="$APP_DIR/deploy/.image-tags.env" +# Capture the tags the caller explicitly passed BEFORE sourcing -- they must +# WIN. The file only supplies the *sibling's* last-known tag; sourcing it +# unconditionally would clobber an incoming tag (e.g. a frontend deploy whose +# FRONTEND_IMAGE_TAG got overwritten by the stale `local` the first backend-only +# deploy persisted for the not-yet-known sibling -> pull `:local` -> "manifest +# unknown"). So source for the sibling, then re-apply the caller's own value. +_incoming_backend="${BACKEND_IMAGE_TAG:-}" +_incoming_frontend="${FRONTEND_IMAGE_TAG:-}" if [ -f "$TAGS_FILE" ]; then set -a; . "$TAGS_FILE"; set +a fi +[ -n "$_incoming_backend" ] && BACKEND_IMAGE_TAG="$_incoming_backend" +[ -n "$_incoming_frontend" ] && FRONTEND_IMAGE_TAG="$_incoming_frontend" # Guard: the service(s) being rolled MUST have a concrete tag supplied now (the # sibling's may come from the persisted file). `all` needs both. @@ -207,21 +217,25 @@ EOF # Health check the service(s) we rolled: backend on 8137, frontend on 8080. # (For `all`, backend's `/` serving is the readiness signal the old script used # and the frontend depends_on backend anyway.) -declare -A HEALTH_PORTS=( [backend]=8137 [frontend]=8080 ) +# Health via each container's own HEALTHCHECK (docker inspect), NOT a host-port +# curl: the dev overlay leaves the frontend port UNpublished (reached through the +# backend's _proxy_to_frontend), so a localhost:8080 curl spuriously fails there. +# Both images HEALTHCHECK-curl /healthz internally, so this works published or not. health_ok=1 for svc in "${TARGETS[@]}"; do - port="${HEALTH_PORTS[$svc]}" - url="http://127.0.0.1:${port}/healthz" - echo "==> Health check: $svc ($url)" - ok=0 - for i in $(seq 1 30); do - if curl -fsS -o /dev/null "$url"; then ok=1; break; fi - sleep 1 + cid=$(docker compose ps -q "$svc" 2>/dev/null) + echo "==> Health check: $svc (container health)" + ok=0; st=unknown + for i in $(seq 1 40); do + st=$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$cid" 2>/dev/null || echo gone) + [ "$st" = healthy ] && { ok=1; break; } + if [ "$st" = none ] && [ "$(docker inspect --format '{{.State.Status}}' "$cid" 2>/dev/null)" = running ]; then ok=1; break; fi + sleep 2 done if [ "$ok" = 1 ]; then - echo "==> OK: $svc is serving" + echo "==> OK: $svc is healthy" else - echo "!! Health check failed for $svc ($url)" >&2 + echo "!! Health check failed for $svc (status=$st)" >&2 health_ok=0 fi done