Corré esto en cualquier clúster de Kubernetes que tenga contenedores con HEALTHCHECK definido en su Dockerfile y sin livenessProbe ni readinessProbe configurados en el manifiesto:
kubectl get pods -o wide
docker ps --filter "name=mi-servicio" --format "table {{.Names}}\t{{.Status}}"
Vas a ver dos historias distintas. docker ps capaz dice Up 3 minutes (healthy). Kubernetes, mientras tanto, no tiene ni idea de que ese HEALTHCHECK existe. Para el kubelet ese pod está Running y listo para recibir tráfico desde el momento en que el proceso arrancó — el estado interno de Docker no forma parte de su ecuación.
Mi tesis, y la razón por la que este post existe, es esta: el HEALTHCHECK de Docker no es una feature de disponibilidad, es un log con forma de feature. Da la sensación de resolver algo porque tiene sintaxis de resolver algo — interval, retries, healthy/unhealthy — pero esa sensación es exactamente el problema. Un mecanismo que solo Swarm consume, que Compose respeta a medias y que Kubernetes ignora del todo no es "un healthcheck genérico": es tres cosas distintas con el mismo nombre, y ese nombre compartido es lo que hace que la gente confíe de más.
Esto no es una corrección al post anterior sobre Actuator endpoints en Spring Boot, es un matiz que ese tipo de configuraciones deja afuera casi siempre: exponer /actuator/health no sirve de nada si nadie del lado de la orquestación lo está consultando con la semántica correcta.
Docker healthcheck, compose y orquestador: tres capas que no hablan entre sí
Hay tres capas y cada una tiene su propio reloj:
-
Docker Engine — corre el
HEALTHCHECKdel Dockerfile o deldocker-compose.yml, guarda el resultado endocker inspect, y eso es todo. No reinicia nada por sí solo salvo que usesdepends_on: condition: service_healthyen Compose. -
Docker Swarm — sí lee ese estado. Si un servicio de Swarm queda
unhealthy, Swarm lo saca de rotación y puede reemplazarlo según la política derestart. -
Kubernetes — ignora completamente el
HEALTHCHECKde Docker. Usa sus propias probes:livenessProbe,readinessProbeystartupProbe, definidas en el manifiesto del pod, con su propio período, timeout y umbral de fallas.
La diferencia entre punto 2 y punto 3 es la que rompe expectativas. Alguien que viene de Swarm o de Compose asume que "definir un healthcheck" alcanza. En Kubernetes no alcanza: si no escribís un readinessProbe, el Service va a mandar tráfico al pod apenas el contenedor arranca, sin importar si la app terminó de conectarse a la base o de calentar el pool de conexiones.
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]
Qué dice la documentación de Kubernetes y qué NO dice
La documentación oficial de Kubernetes sobre probes es clara en un punto que mucha gente se salta: liveness, readiness y startup son tres mecanismos distintos, con propósitos distintos, y ninguno delega en el HEALTHCHECK de la imagen Docker subyacente.
- livenessProbe: si falla, el kubelet reinicia el contenedor. Sirve para detectar deadlocks o procesos colgados.
- readinessProbe: si falla, el pod se saca de los endpoints del Service. No lo reinicia, solo deja de recibir tráfico.
- startupProbe: protege a apps con arranque lento de que el liveness las mate antes de tiempo.
Lo que la documentación no dice — porque no es su rol decirlo — es qué pasa si vos definís un HEALTHCHECK en el Dockerfile y ningún readinessProbe en el manifiesto. La respuesta, y esto es criterio propio, no letra de la doc: nada pasa con ese HEALTHCHECK. Docker lo va a seguir ejecutando y reportando en docker inspect, pero es un dato muerto para efectos de enrutamiento en Kubernetes. Es exactamente el mismo problema estructural que expuse con los endpoints de Actuator: exponés la señal, y si nadie del lado correcto la lee, la señal es decorativa. Lo incómodo de esto es que decorativo no es lo mismo que inofensivo: ocupa un lugar en la checklist de deploy que debería estar ocupado por una probe real.
Dónde se equivoca la gente: la receta que "funcionaba en Compose"
El patrón común es este: un equipo desarrolla y testea con docker-compose.yml, define ahí un HEALTHCHECK prolijo con curl al endpoint de salud, ve que Compose respeta depends_on: condition: service_healthy, y da por hecho que ese mismo comportamiento se traduce al pasar a Kubernetes. No se traduce. El manifiesto de Kubernetes necesita su propio bloque:
# 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
El costo oculto no es solo la falta de traducción automática. Es que el equipo tiene una falsa sensación de cobertura: "tenemos healthcheck" queda como ítem tildado en la checklist de deploy, cuando en realidad el healthcheck que tildaron vive en una capa que el orquestador de producción ni consulta. El contraejemplo típico: un pod con proceso zombie que sigue respondiendo 200 en /health para Docker pero que Kubernetes reinicia por OOM antes de que el HEALTHCHECK interno note nada — dos sistemas, dos verdades, ninguna sincronizada.
Matriz de decisión: cuándo mirar cada capa
| Escenario | Qué mirar primero | Por qué |
|---|---|---|
| Corrés en Compose sin orquestador arriba |
HEALTHCHECK del Dockerfile + depends_on: condition
|
Es la única señal que existe en ese contexto |
| Corrés en Swarm |
HEALTHCHECK + política deploy.restart_policy
|
Swarm consume directamente ese estado |
| Corrés en Kubernetes |
readinessProbe y livenessProbe en el manifiesto |
El HEALTHCHECK de Docker es invisible para el kubelet |
| Tenés apps con arranque lento (JVM, frameworks con cache warmup) |
startupProbe antes que ajustar el liveness |
Evita reinicios en cascada por falsos negativos |
| Migrás de Compose a Kubernetes | Traducir manualmente el HEALTHCHECK a probes |
No hay conversión automática ni herramienta oficial que lo haga |
Esta matriz es criterio, no ley. Cada fila asume que el endpoint de salud que estás chequeando realmente refleja el estado de dependencias críticas — base de datos, colas, cache — y no solo "el proceso responde". Eso es un tema aparte que toqué indirectamente cuando hablé de JWT sin estado vs sesiones con estado: la superficie de lo que decidís chequear importa tanto como el mecanismo que lo chequea.
Límites: lo que esta evidencia no permite concluir
No tengo datos productivos propios de latencia de detección de falla entre Docker y Kubernetes, ni benchmarks de cuántos requests fallidos genera esa ventana de desincronización. La documentación oficial de Kubernetes describe el mecanismo de las probes, no mide el impacto operativo de omitirlas — eso queda para quien corra el experimento con su propio clúster y sus propios logs.
Tampoco puedo afirmar que esto pase "en la mayoría de los deploys" ni poner una cifra ahí. Lo que sí puedo sostener, porque está en la documentación oficial y en el diseño mismo de Kubernetes, es que el kubelet no consume el HEALTHCHECK de Docker como fuente de verdad. Eso es un hecho verificable con kubectl describe pod y comparando contra docker inspect --format='{{json .State.Health}}' en el nodo. El resto — cuánto te cuesta en la práctica no tenerlo bien configurado — depende del tráfico, del blast radius de tu Service y de cuánto tarda la app en fallar de forma visible.
FAQ
¿Docker Compose y Kubernetes usan el mismo mecanismo de healthcheck?
No. Compose lee el HEALTHCHECK del Dockerfile o el bloque healthcheck del YAML. Kubernetes usa probes definidas en el manifiesto del pod (livenessProbe, readinessProbe, startupProbe), independientes del HEALTHCHECK de la imagen.
¿Si mi imagen tiene HEALTHCHECK, Kubernetes lo aprovecha automáticamente?
No. El kubelet no lo lee. Necesitás declarar explícitamente las probes en el manifiesto, aunque apunten al mismo endpoint que usa el HEALTHCHECK.
¿Docker Swarm sí respeta el HEALTHCHECK del Dockerfile?
Sí. Swarm consume ese estado para decidir si rota o reemplaza tareas de un servicio, según la política de restart_policy que definas.
¿Puedo usar el mismo endpoint para HEALTHCHECK y para las probes de Kubernetes?
Sí, técnicamente podés apuntar ambos al mismo endpoint HTTP. Pero son configuraciones separadas: tenés que declarar ambas, no alcanza con una sola.
¿Qué pasa si solo defino livenessProbe y no readinessProbe?
El pod se reinicia si el liveness falla, pero el Service va a seguir enrutando tráfico al pod desde el arranque, sin esperar a que esté listo. Es la combinación que más incidentes de "arranque en caliente" genera.
¿Existe una herramienta que traduzca HEALTHCHECK de Docker a probes de Kubernetes automáticamente?
No hay una herramienta oficial de conversión directa. Herramientas como kompose migran algunos campos de Compose a manifiestos de Kubernetes, pero conviene revisar manualmente que las probes queden bien definidas — no asumir que la migración las trajo correctas.
Postura final
Si el stack incluye Kubernetes, el HEALTHCHECK del Dockerfile es documentación para quien lee la imagen, no una configuración operativa. Definilo igual, porque ayuda en desarrollo local con Compose y en debugging manual con docker inspect. Pero no lo confundas con la señal que el orquestador de producción realmente usa para decidir a quién le manda tráfico. Mi punto de fondo es que el problema no es técnico, es de nombres: si HEALTHCHECK se llamara distinto en cada motor, nadie asumiría que se comporta igual en los tres.
Lo próximo que haría antes de cerrar un deploy a Kubernetes: correr kubectl describe pod <nombre> y confirmar que las secciones Liveness y Readiness no digan <none>. Si dicen <none>, tenés un HEALTHCHECK de Docker que le habla a nadie. La pregunta incómoda que vale la pena hacerse antes del próximo deploy: ¿tildaste "healthcheck" en la checklist porque lo verificaste, o porque viste la palabra en el Dockerfile?
Fuente original: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/
Este artículo fue publicado originalmente en juanchi.dev
Top comments (0)