Run this on any Kubernetes cluster that has containers with HEALTHCHECK defined in their Dockerfile and no livenessProbe or readinessProbe configured in the manifest:
kubectl get pods -o wide
docker ps --filter "name=mi-servicio" --format "table {{.Names}}\t{{.Status}}"
You're going to see two different stories. docker ps might say Up 3 minutes (healthy). Kubernetes, meanwhile, has zero idea that HEALTHCHECK even exists. For the kubelet, that pod is Running and ready to take traffic from the moment the process started — Docker's internal state isn't part of its equation at all.
I ran into this the annoying way: debugging a service that kept getting traffic mid-restart, convinced the healthcheck I'd carefully written in the Dockerfile was doing its job. It wasn't doing anything. Nobody on the Kubernetes side was even asking it a question.
My thesis is simple and I back it with Kubernetes' own official docs: Docker's HEALTHCHECK is an information layer that lives inside the Docker daemon, not a routing guarantee. If the orchestrator sitting on top isn't reading that state — because you're on Kubernetes with your own probes, or because you assumed Swarm and Kubernetes behave the same way — the container can be "healthy" according to Docker and still keep receiving traffic that's going to fail.
This isn't a correction to my previous post about Actuator endpoints in Spring Boot, it's a nuance that this kind of setup leaves out almost every time: exposing /actuator/health does nothing if nobody on the orchestration side is querying it with the right semantics.
Docker healthcheck, compose, and orchestrator: three layers that don't talk to each other
There are three layers, and each one runs on its own clock:
-
Docker Engine — runs the
HEALTHCHECKfrom the Dockerfile ordocker-compose.yml, stores the result indocker inspect, and that's it. It doesn't restart anything on its own unless you usedepends_on: condition: service_healthyin Compose. -
Docker Swarm — actually reads that state. If a Swarm service goes
unhealthy, Swarm pulls it out of rotation and can replace it according to therestartpolicy. -
Kubernetes — completely ignores Docker's
HEALTHCHECK. It uses its own probes:livenessProbe,readinessProbe, andstartupProbe, defined in the pod manifest, each with its own period, timeout, and failure threshold.
The gap between point 2 and point 3 is exactly where expectations break. Someone coming from Swarm or Compose assumes that "defining a healthcheck" is enough. On Kubernetes it isn't: if you don't write a readinessProbe, the Service is going to send traffic to the pod the moment the container starts, regardless of whether the app finished connecting to the database or warming up its connection pool.
flowchart LR
A[Contenedor arranca] --> B{Docker HEALTHCHECK}
B -->|healthy| C[docker inspect: healthy]
C -.->|Swarm SÍ lee esto| D[Swarm rota tráfico]
C -.->|Kubernetes NO lee esto| E[kubelet ignora]
E --> F{readinessProbe definido?}
F -->|no| G[Service enruta igual]
F -->|sí| H[kubelet decide según su propia probe]
What Kubernetes' docs say — and what they don't
The official Kubernetes docs on probes are clear on a point a lot of people skip past: liveness, readiness, and startup are three distinct mechanisms, with distinct purposes, and none of them defer to the underlying Docker image's HEALTHCHECK.
- livenessProbe: if it fails, the kubelet restarts the container. Good for catching deadlocks or hung processes.
- readinessProbe: if it fails, the pod gets pulled from the Service's endpoints. It doesn't restart anything, it just stops receiving traffic.
- startupProbe: protects slow-starting apps from getting killed by liveness before they're ready.
What the docs don't say — because it's not their job to say it — is what happens if you define a HEALTHCHECK in the Dockerfile and no readinessProbe in the manifest. The answer, and this is my own call, not doc wording: nothing happens with that HEALTHCHECK. Docker keeps running it and reporting it in docker inspect, but it's a dead signal as far as Kubernetes routing goes. It's the exact same structural problem I laid out with Actuator endpoints: expose the signal, and if nobody on the right side is reading it, the signal is decorative.
Where people get it wrong: the recipe that "worked in Compose"
The common pattern is this: a team develops and tests with docker-compose.yml, defines a nice HEALTHCHECK there with curl hitting the health endpoint, sees Compose respecting depends_on: condition: service_healthy, and assumes that same behavior carries over when they move to Kubernetes. It doesn't carry over. The Kubernetes manifest needs its own block:
# Sin esto, Docker HEALTHCHECK es ruido para kubelet
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 8080
periodSeconds: 15
The hidden cost isn't just the missing auto-translation. It's that the team ends up with a false sense of coverage: "we have a healthcheck" gets checked off on the deploy checklist, when the healthcheck they checked off lives on a layer the production orchestrator doesn't even query. The classic counterexample: a pod with a zombie process still returning 200 on /health for Docker, that Kubernetes restarts for OOM before the internal HEALTHCHECK notices a thing — two systems, two truths, neither synced with the other.
Decision matrix: when to look at each layer
| Scenario | What to check first | Why |
|---|---|---|
| Running Compose with no orchestrator on top | Dockerfile HEALTHCHECK + depends_on: condition
|
It's the only signal that exists in that context |
| Running Swarm |
HEALTHCHECK + deploy.restart_policy
|
Swarm consumes that state directly |
| Running Kubernetes |
readinessProbe and livenessProbe in the manifest |
Docker's HEALTHCHECK is invisible to the kubelet |
| Slow-starting apps (JVM, frameworks with cache warmup) |
startupProbe before touching liveness |
Avoids cascading restarts from false negatives |
| Migrating from Compose to Kubernetes | Manually translate HEALTHCHECK into probes |
There's no automatic conversion, no official tool does it |
This matrix is judgment call, not law. Every row assumes the health endpoint you're checking actually reflects the state of critical dependencies — database, queues, cache — and not just "the process responds." That's a separate topic I touched on indirectly when I talked about stateless JWT vs stateful sessions: the surface of what you decide to check matters just as much as the mechanism doing the checking.
Limits: what this evidence doesn't let me conclude
I don't have my own production data on failure-detection latency between Docker and Kubernetes, nor benchmarks on how many failed requests that desync window generates. The official Kubernetes docs describe the probe mechanism, they don't measure the operational impact of skipping it — that's on whoever runs the experiment with their own cluster and their own logs.
I also can't claim this happens "in most deploys," or throw a number at it. What I can stand behind, because it's in the official docs and baked into Kubernetes' design itself, is that the kubelet doesn't treat Docker's HEALTHCHECK as a source of truth. That's a verifiable fact with kubectl describe pod, compared against docker inspect --format='{{json .State.Health}}' on the node. The rest — how much it actually costs you in practice not to have it configured right — depends on your traffic, your Service's blast radius, and how long it takes the app to fail visibly.
FAQ
Do Docker Compose and Kubernetes use the same healthcheck mechanism?
No. Compose reads the HEALTHCHECK from the Dockerfile or the healthcheck block in the YAML. Kubernetes uses probes defined in the pod manifest (livenessProbe, readinessProbe, startupProbe), independent of the image's HEALTHCHECK.
If my image has a HEALTHCHECK, does Kubernetes use it automatically?
No. The kubelet doesn't read it. You need to explicitly declare the probes in the manifest, even if they hit the same endpoint the HEALTHCHECK uses.
Does Docker Swarm actually respect the Dockerfile's HEALTHCHECK?
Yes. Swarm consumes that state to decide whether to rotate or replace a service's tasks, according to whatever restart_policy you set.
Can I use the same endpoint for HEALTHCHECK and for Kubernetes probes?
Yes, technically you can point both at the same HTTP endpoint. But they're separate configurations: you have to declare both, one alone isn't enough.
What happens if I only define livenessProbe and not readinessProbe?
The pod restarts if liveness fails, but the Service is still going to route traffic to the pod from startup, without waiting for it to be ready. It's the combination that causes the most "hot-start" incidents.
Is there a tool that automatically translates Docker HEALTHCHECK into Kubernetes probes?
There's no official direct-conversion tool. Tools like kompose migrate some Compose fields to Kubernetes manifests, but you should manually check that the probes came out right — don't assume the migration got them correct.
Final take
If the stack includes Kubernetes, the Dockerfile's HEALTHCHECK is documentation for whoever reads the image, not an operational setting. Define it anyway, because it helps with local development in Compose and manual debugging with docker inspect. Just don't confuse it with the signal your production orchestrator actually uses to decide who gets traffic.
The next thing I'd do before closing out a Kubernetes deploy: run kubectl describe pod <name> and confirm the Liveness and Readiness sections don't say <none>. If they say <none>, you've got a Docker HEALTHCHECK talking to nobody.
Original source: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/
This article was originally published on juanchi.dev
Top comments (0)