Un HEALTHCHECK de Docker contesta una sola pregunta: ¿el comando que definiste salió con código 0? Nada más. No mide si la app está "sana" en ningún sentido amplio — mide si ese comando específico, en ese momento, no falló.
La documentación oficial de Docker lo dice sin vueltas: la instrucción HEALTHCHECK le dice a Docker cómo testear un contenedor para chequear que "sigue funcionando". El ejemplo que da la propia doc es elocuente:
HEALTHCHECK --interval=5m --timeout=3s \
CMD curl -f http://localhost/ || exit 1
Ese curl -f http://localhost/ prueba una cosa: que el servidor web contesta algo en la ruta raíz dentro de 3 segundos. No prueba que la base de datos esté arriba, que la cola de mensajes tenga workers vivos, ni que el servicio de autenticación que la app necesita para loguear usuarios esté respondiendo. Si curl recibe cualquier respuesta — incluso una página de error con status 200 mal configurado — el healthcheck pasa.
Los tres estados y qué dispara cada uno
Según la doc, un contenedor con healthcheck definido tiene un estado de salud además de su estado normal. Arranca en starting. Cada vez que el chequeo pasa, pasa a healthy. Después de una cierta cantidad de fallos consecutivos, pasa a unhealthy.
El exit code del comando es lo único que mueve esa máquina de estados:
-
0: healthy — el contenedor está "sano" según ese comando -
1: unhealthy — el contenedor no funciona correctamente -
2: reservado, no usar
Eso es todo lo que Docker sabe. No interpreta el contenido de la respuesta, no valida lógica de negocio, no chequea dependencias transitivas. Si tu comando de chequeo es curl -f http://localhost/health y ese endpoint devuelve 200 OK con un JSON hardcodeado que nunca cambia, vas a tener un contenedor eternamente healthy aunque la lógica real de la app esté rota.
Liveness superficial vs salud real
Acá está la distinción que importa para decidir qué poner en el healthcheck: un chequeo de "liveness" superficial solo confirma que el proceso responde — que no está colgado, que no entró en loop infinito, que el puerto escucha. Un chequeo de salud real valida que las dependencias que la app necesita para servir tráfico funcionan: conexión a la base de datos, acceso a servicios externos críticos, estado de colas si la app depende de ellas.
La doc da el ejemplo exacto del problema que resuelve un liveness check básico: "detectar casos como un servidor web atascado en un loop infinito e incapaz de manejar nuevas conexiones, aunque el proceso del servidor siga corriendo". Eso es exactamente lo que cubre, ni más ni menos. Si el proceso responde pero no puede escribir en la base de datos porque se cortó la conexión, un healthcheck que solo pega un curl a / nunca se va a enterar — el servidor sigue respondiendo con 200 en esa ruta mientras cada request real a la app falla.
Un HEALTHCHECK que solo verifica que el proceso responde da una falsa sensación de seguridad si no valida las dependencias reales de la app. El estado healthy en docker ps puede convivir perfectamente con una app que no puede completar ninguna operación útil.
stateDiagram-v2
[*] --> starting
starting --> healthy: exit 0
starting --> unhealthy: fallos consecutivos
healthy --> unhealthy: fallos consecutivos
unhealthy --> healthy: exit 0
Qué pasa con start_period y los reintentos
Dos opciones de HEALTHCHECK definen cuánta paciencia tiene Docker antes de declarar unhealthy: --start-period y --retries. La doc explica que el start period da tiempo de inicialización para contenedores que necesitan bootstrapear — si el chequeo falla durante esa ventana, no cuenta para el máximo de reintentos. Pero si el chequeo pasa una vez durante el start period, el contenedor se considera arrancado y desde ahí todos los fallos consecutivos sí cuentan.
Esto importa en un detalle que la doc no resalta pero que se desprende de la mecánica: si la app tarda en levantar y tu start_period es corto, podés terminar contando como "fallo real" algo que en realidad era "todavía estoy arrancando". El ajuste correcto de start_period y retries depende del tiempo real de bootstrap de la app — no hay un valor universal que sirva para toda imagen.
Qué no mide, aunque el healthcheck esté bien escrito
Ni el mejor CMD dentro de HEALTHCHECK resuelve esto:
- Dependencias externas que no estás chequeando explícitamente. Si el comando no toca la base de datos, el healthcheck no sabe nada sobre ella.
- Degradación parcial. Un servicio que responde lento pero dentro del timeout pasa igual que uno que responde instantáneo. El exit code no tiene gradientes.
- Estado de negocio. Si la app devuelve 200 pero con datos corruptos o una respuesta vacía donde debería haber contenido, y tu chequeo solo valida el código HTTP, pasa igual.
- Lo que pasa fuera del contenedor. HEALTHCHECK corre dentro del contenedor, con la visibilidad que tiene desde ahí. No ve el estado del host, de la red externa, ni de otros contenedores salvo que tu comando los consulte directamente.
Cada uno de estos puntos se resuelve agregando lógica al comando del healthcheck — no cambiando cómo Docker interpreta el resultado. Si necesitás que el chequeo refleje la salud real del servicio, el trabajo está en escribir un endpoint o un script que efectivamente pruebe las dependencias que importan, no en ajustar intervalos o reintentos.
Fuente original
Este artículo fue publicado originalmente en juanchi.dev
Top comments (1)
La distinción entre liveness y una operación útil es la que más me llevo. Un
curl -f http://localhost/puede demostrar que el servidor responde dentro de 3 segundos, pero no que la base de datos, la cola o la autenticación estén disponibles. Yo separaría el chequeo barato del contenedor de un endpoint de readiness que pruebe solo las dependencias necesarias para recibir tráfico, y documentaría qué garantía ofrece cada uno. También revisaríastart_periodcontra el bootstrap real, porque no hay un valor universal. ¿Cómo estás delimitando las dependencias que el readiness debe probar sin convertirlo en una prueba de extremo a extremo?