Healthcheck honesty fix — 2026-05-12
Sprint: SPRINT_20260512_015_DevOps_healthcheck_path_alignment Commit: df3ed409e — Fix lying healthchecks + flip defaults to /healthz + per-svc routes
This release closes the highest-impact finding from the 2026-05 orchestration cycle: every container healthcheck written before df3ed409e was reporting “healthy” without actually checking HTTP correctness. Operators reading prior “containers healthy” reports should re-evaluate them with this context. The fix is mandatory; rebuild images to propagate the corrected probe to running containers.
Operator impact (read this first)
Prior “containers healthy” reports were honest about TCP listen, but did NOT verify HTTP correctness. A service that bound its port but returned 404, 500, or never mapped a /health* route at all would report healthy in docker ps and docker inspect. Any operator runbook, smoke check, or CI gate that relies on Docker’s Status: healthy for the stellaops/<service>:* images is affected.
Concretely, this masked the following pre-existing latent bugs that SPRINT_20260512_015’s per-service survey then forced into the open:
- 10 services had no working
/health*route at all. They had been reporting healthy purely because their port listened. Tracked under*_HEALTH-01BLOCKED follow-ups in SPRINT_20260512_015 (SIGNER, WORKFLOW, UNKNOWNS, OPSMEMORY, RISKENGINE, NOTIFY, AIRGAP, EXPORT, EVIDENCE, TILEPROXY). - Healthcheck script defaults (
/health/liveness+/health/readiness) matched only the worker pattern. Most WebServices map/healthz+/readyz. The mismatch was hidden by the same /dev/tcp false-success bug. C:/temp/policy-engine-health-override.yml— an out-of-tree workaround for the policy-engine probe — had been silently masking the same class of problem.
Discovery
While preparing the 2026-05 release artifacts, the orchestration team inspected devops/docker/healthcheck.sh and noticed that the bash /dev/tcp fallback path (taken when curl and wget are unavailable — which is every stellaops image, none of them ship either tool) established a TCP connection and exited 0 without parsing the HTTP status line. A targeted probe via a curlimages/curl:latest sidecar attached to the stellaops docker network then surfaced the 10 services with no working /health* route and the dozen-plus services with a route under a non-default path.
Fix
devops/docker/healthcheck.sh was rewritten so the /dev/tcp fallback now:
- Issues a
GET ${PATH} HTTP/1.0request over the raw socket. - Parses the response status line.
- Exits 0 only when the status line is a non-empty 2xx or 3xx within
${HEALTH_TIMEOUT}seconds. - Exits 1 on any 4xx/5xx, on connection refusal, on timeout, or on an empty/garbled response.
Defaults were flipped to /healthz + /readyz to match the dotnet baseline and the strict majority of running services. 14 services received explicit LIVENESS_PATH / READINESS_PATH compose overrides that match the routes they actually map; 8 workers inherit /health/liveness + /health/readiness via the new &worker-health-env YAML anchor. 10 services with no /health* route were placed on a temporary TCP-listen-only probe via &healthcheck-port-listen while the app-side MapHealthChecks follow-ups are tracked under SPRINT_20260512_015’s *_HEALTH-01 BLOCKED tasks.
The retired C:/temp/policy-engine-health-override.yml was folded into the canonical compose entry; the temp file is no longer required.
Action required
Rebuild and recreate
The script lives in the runtime image layer, so a docker compose pull --no-build is not sufficient. To propagate the honest probe:
# Rebuild every service image so the corrected healthcheck.sh ships
docker compose -f devops/compose/docker-compose.stella-ops.yml \
-f devops/compose/docker-compose.stella-services.yml build
# Recreate containers
docker compose -f devops/compose/docker-compose.stella-ops.yml \
-f devops/compose/docker-compose.stella-services.yml up -d
The previous orchestration cycle verified 55/55 containers healthy post-recreate with --no-build because only compose env was changed; a full image rebuild is now required for the script fix itself to land in the runtime layer.
Re-evaluate prior smoke checks
If your CI pipeline, deployment runbook, or release gate accepts docker inspect ... healthy as truth — re-run those checks against the rebuilt images. Any service that flips from healthy to unhealthy was already broken; the new probe is only telling the truth.
Watch for the 10 known-broken services
The following services are pinned to a TCP-listen-only probe until their *_HEALTH-01 BLOCKED follow-up ships:
| Service | Sprint follow-up |
|---|---|
| signer | SIGNER_HEALTH-01 (DONE in SPRINT_20260512_023) |
| workflow | WORKFLOW_HEALTH-01 (DONE in SPRINT_20260512_023) |
| unknowns-web | UNKNOWNS_HEALTH-01 (DONE in SPRINT_20260512_023, runtime blocker remains) |
| opsmemory-web | OPSMEMORY_HEALTH-01 (DONE in SPRINT_20260512_023) |
| riskengine-web | RISKENGINE_HEALTH-01 (DONE in SPRINT_20260512_023) |
| notify-web | NOTIFY_HEALTH-01 (pending) |
| airgap-controller | AIRGAP_HEALTH-01 (pending) |
| export-web | EXPORT_HEALTH-01 (pending) |
| evidence-locker-web | EVIDENCE_HEALTH-01 (pending) |
| attestor-tileproxy | TILEPROXY_HEALTH-01 (DONE in SPRINT_20260512_022) |
Five of the eleven have landed AddHealthChecks() + MapHealthChecks since the discovery; the remaining six are queued behind the same pattern. The TCP-listen probe is not equivalent to an HTTP probe — treat any of these services with extra scrutiny until the per-service follow-up closes.
Follow-up sprints filed
SPRINT_20260512_015_DevOps_healthcheck_path_alignment— survey, script fix, per-service env overrides, retired workaround. DONE.SPRINT_20260512_022— TileProxy/healthz+/readyzlifted out of the auth-gated admin group. DONE.SPRINT_20260512_023— Batch-2 close-out for 5 of the 11 services. DONE. New unit tests added (2 per service).SPRINT_20260513_NNN(queued) — Wire the remaining 6 BLOCKED*_HEALTH-01follow-ups (notify, airgap, export, evidence-locker, and any newly-surfaced offenders).
Files of interest
devops/docker/healthcheck.sh— the rewritten probe.devops/compose/docker-compose.stella-services.yml— per-serviceLIVENESS_PATH/READINESS_PATHenv,&worker-health-envand&healthcheck-port-listenYAML anchors.docs/implplan/SPRINT_20260512_015_DevOps_healthcheck_path_alignment.md— full per-service survey table and decision log.
