A service has at least two questions to answer: is its main process still running, and does an application-specific probe consider it healthy? A live process can be stuck or unable to serve useful requests. A failed health probe can also result from a bad probe command or dependency assumption. Keeping process status separate from health makes those possibilities visible instead of translating every failure into “the container stopped.”
Docker tracks health when a HEALTHCHECK is configured; ordinary standalone Docker does not automatically restart an unhealthy container merely because the health status changes. Restart behavior and orchestration decisions require explicit policies. Our Python task models neither an orchestrator nor a daemon: it applies supplied events to a deterministic state machine.
A stop request begins a transition
A typical stop sends the configured stop signal, commonly SIGTERM, to the container's main process and allows a grace period. If the process remains alive after that period, the runtime can force termination with SIGKILL. The application must arrange its own cleanup behavior; SIGKILL does not permit a handler to finish buffered writes or release application resources gracefully.
That is why the fixture has a distinct stopping state. Receiving term does not itself prove successful exit. A later exit:0 records graceful completion; a supplied timeout records a forced stop. The fixture uses 137 as its forced-stop convention. In real observations, the numeric code alone is insufficient evidence of the underlying cause, and shell conventions can introduce another interpretation layer.
Probe behavior needs a contract
The model begins health at starting, counts failed probes immediately and marks unhealthy at the configured threshold. It deliberately has no startup grace period or elapsed-time calculation. A successful probe resets the streak and returns healthy. Until a failure streak reaches the threshold, the current health remains unchanged.
health, failures, retries = "starting", 0, 2
for ok in [False, False, True, False]:
if ok:
health, failures = "healthy", 0
else:
failures += 1
if failures >= retries:
health = "unhealthy"
print(health, failures)Connect launch structure to signal handling
The earlier exec-form entrypoint makes the intended Python service the long-lived command, but the application still needs to handle shutdown. A wrapper shell or parent process adds forwarding and child-reaping responsibilities. The optional project uses a signal flag and a bounded request loop so the main thread can leave the loop and close its server cleanly. Its behavior must still be tested with a real runtime before drawing operational conclusions.
Before building, write out the valid source states for every event. Repeated term is harmless in stopping by this contract; probes after stopping and restart after exited are rejected. That intentionally narrow state machine does not represent all Docker restart policies. The bug hunts isolate an off-by-one health threshold and an early exit on termination request.
Passing the build demonstrates event ordering, threshold handling and honest failure reports. It does not send signals, measure a grace period, observe real child processes or prove that a service flushes its data during shutdown.