Nothing restarts the API when it goes unhealthy — Docker Compose does not act on healthcheck status #137
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Severity: Medium · Confidence: High (verified empirically) · Effort: S · Category: ops
Problem
#97 added a real
/healthprobe (DB + Redis) and a containerHEALTHCHECK, so a wedged-but-running API is now detected. Nothing acts on it.restart: unless-stoppedonly reacts to the process exiting. Docker Engine marks a containerunhealthyand takes no further action — restarting on health status is Swarm/Kubernetes behaviour, and there is norestart_on_unhealthyin Compose.So #97's second acceptance criterion ("Compose restarts the api container when the healthcheck fails") rests on a mistaken premise about Docker and cannot be met by configuration alone.
Evidence (dev server, 2026-07-17)
Stopped Redis and waited for the healthcheck to fail its 3 retries:
The container sat
unhealthyindefinitely, still serving 503s, and was never restarted.Impact
The exact scenario #97 set out to fix — "a wedged-but-alive API is never detected or restarted" — is now half-solved. It's detected (
docker compose psshows(unhealthy), anddepends_on: service_healthyrespects it), but a silent brownout still requires someone to notice anddocker compose restart apiby hand.Options
willfarrell/autoheal) — watches forunhealthycontainers and restarts them; the standard answer for plain Compose. Security cost: it needs/var/run/docker.sockmounted, which is root-equivalent on the host. That's a real decision for a box also running ~55 other containers, and shouldn't be taken by default.process.exit(1), lettingrestart: unless-stoppeddo the work. No new privileges, no new container; but it means the app judges its own health, and a flapping dependency could turn into a restart loop. Would want a generous threshold and to reuse the #85 graceful-shutdown path.Rhoving/iac-repo); alert onunhealthyand let a human decide. No auto-recovery, but no new attack surface either.depends_ongating and visibility; a wedged API is rare enough to fix by hand.My weak preference is (2) or (3) over (1) — mounting the docker socket to fix a rare brownout is a poor trade on a host with that much else on it. But this is a judgement call about your infrastructure, not a code question.
Acceptance criteria
Notes
Split out of #97, which delivered the probe, the
HEALTHCHECKand thedepends_on: service_healthygating. Filed rather than folded in because the fix needs new infrastructure and a security trade-off, not more code. Related: #134 (auto-deploy) gates on/health, so it benefits from the probe regardless of what's decided here.Decision (from @rbrooks): autoheal is out. Recording per this issue's acceptance criterion.
So option 1 (the
willfarrell/autohealsidecar) is rejected — it would need/var/run/docker.sockmounted (root-equivalent) on a host running ~55 other containers, too much standing attack surface to fix a rare brownout.That leaves the two socket-free options. My recommendation between them:
Rhoving/iac-repo); alert on a container inunhealthystate (or scrape/health→ non-200) and page a human. Given how rare a wedged-but-alive API is — and thatdepends_on: service_healthyalready prevents the app from proxying to a not-yet-ready API — visibility + an alert is a proportionate answer./healthchecks) that, after N consecutive failures over a generous window, calls the existing #85 graceful-shutdown path with a non-zero exit sorestart: unless-stoppedbrings it back clean. The risk to design against is a restart-loop while a dependency is simply down (Redis/DB unreachable isn't the API's fault) — so the threshold must be generous and ideally distinguish "I am wedged" from "my dependency is absent".Proposed: go with option 3 now (it's a config/alerting change in iac-repo, not TeaLeaves code), and hold option 2 as a fast-follow if a real brownout ever actually bites. Leaving this issue open for the monitoring wiring; happy to implement option 2 in TeaLeaves instead if you'd rather have auto-recovery — your call.
Decided: option 3 (external monitoring), tracked in Rhoving/iac-repo#163.
@rbrooks picked external monitoring over the self-terminate option, and will implement it in the IaC repo. Opened Rhoving/iac-repo#163 with the full details: a blackbox HTTP probe of the
/healthendpoints (prod10.1.1.7:8055, dev10.1.1.14:18090) scraped by the existing Prometheus stack, with an Alertmanager rule firing when/healthreturns non-2xx — no docker.sock, no agent on the app hosts.The TeaLeaves side of this is already done and shipped in v7.2.0: the real
/healthDB+Redis probe (#97) that returns 503 when degraded is exactly the signal the monitor consumes. Nothing further to build here.Closing — decision recorded (this issue's acceptance criterion), implementation handed to iac-repo#163.