Docker healthchecks: qué miden de verdad y qué no deberías prometer
¿Por qué tratamos a un healthcheck de Docker como si fuera un monitor de salud completo cuando la documentación oficial no promete nada de eso? Llevás meses corriendo contenedores, el panel de Railway muestra "healthy" en verde, y de alguna manera asumís que la app está bien. Esa suposición me incomoda. Y creo que vale la pena ponerla en crisis.
Mi tesis es directa: un healthcheck que solo confirma que el proceso responde puede esconder fallas de negocio enteras. El mecanismo hace lo que dice que hace — ni más, ni menos. El problema está en lo que le prometemos encima.
Docker healthcheck: qué dice la documentación y qué NO dice
La referencia oficial de HEALTHCHECK es clara en su alcance. La instrucción HEALTHCHECK define un comando que Docker ejecuta periódicamente dentro del contenedor para saber si ese contenedor "funciona". Si el comando devuelve exit code 0, el contenedor es healthy. Si devuelve 1, es unhealthy. Si devuelve 2, se ignora (código reservado).
# Ejemplo mínimo de la documentación oficial
HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
Los parámetros disponibles son:
-
--interval: cada cuánto se ejecuta (default: 30s) -
--timeout: tiempo máximo para que el comando responda (default: 30s) -
--start-period: gracia inicial para que el contenedor arranque (default: 0s) -
--retries: cuántos fallos consecutivos marcanunhealthy(default: 3)
Lo que la documentación no dice es que ese endpoint /health verifica el estado real de la aplicación. Eso es responsabilidad del desarrollador que lo implementa. Docker solo evalúa el exit code del comando — sin leer el body de la respuesta, sin interpretar JSON, sin saber si la base de datos está caída o si una queue acumula mensajes sin procesar.
Lo incómodo: la mayoría de los ejemplos que circulan en tutoriales usan curl -f contra un endpoint que devuelve {"status":"ok"} sin conectar nada real. Es un healthcheck que solo confirma que el proceso de Node, Go o Java arrancó y acepta conexiones TCP. Eso tiene valor — pero es un valor muy limitado.
Dónde se equivoca la gente: la receta común y su costo oculto
El patrón más frecuente que aparece en setups públicos de Docker es este:
# Patrón común — mide TCP, no lógica de negocio
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
Y del lado de la app, el endpoint hace algo así:
// Handler que solo confirma que el servidor responde
app.get('/health', (req, res) => {
res.status(200).json({ status: 'ok' });
});
El contenedor aparece como healthy. El orquestador no lo reinicia. Railway muestra verde. Y mientras tanto, la conexión a PostgreSQL puede estar en timeout, el pool de conexiones agotado, o un worker crítico silenciosamente caído.
El costo oculto no es que el healthcheck mienta — es que le creemos más de lo que debería. Cuando un equipo asume que "healthy = app sana", deja de mirar logs, métricas y señales más ricas. El healthcheck se convierte en el chivo expiatorio invisible: si el panel está verde, el problema tiene que estar en otro lado.
Hay un contraejemplo que vale para cualquier sistema típico: imaginá un backend que procesa pagos. El endpoint /health responde 200, el contenedor está healthy, pero el cliente de la pasarela de pago tiene un token expirado. Cada transacción falla silenciosamente. Docker no sabe nada de eso — y no tendría por qué saberlo, si el endpoint no lo verifica.
Matriz de decisión: cuándo confiar en el healthcheck y cuándo ir más lejos
Esta matriz no es absoluta — es un criterio prudente para decidir qué verificar y desde dónde:
| Señal | Qué detecta | Qué NO detecta | Herramienta más adecuada |
|---|---|---|---|
HEALTHCHECK básico (curl TCP) |
Proceso arrancado, puerto abierto | Fallas de DB, lógica rota, queues | Primer nivel de triaje |
Endpoint /health con DB ping |
Conectividad a base de datos | Datos corruptos, permisos, migraciones | Readiness básico |
Endpoint /health profundo |
Dependencias críticas respondiendo | Latencia degradada, errores silenciosos | Startup checks en prod |
| Métricas de aplicación (Prometheus, logs) | Errores por ruta, latencias, colas | Problemas fuera del scope de la app | Observabilidad real |
| Alertas externas (uptime monitors) | Disponibilidad desde fuera del contenedor | Estado interno del proceso | SLA y contratos |
Cuándo confiar en el healthcheck básico:
- Para que Docker/Railway reinicie el contenedor si el proceso muere o queda zombi.
- Para evitar que un contenedor que todavía está arrancando reciba tráfico (con
--start-period). - Como primer nivel de triaje en desarrollo local.
Cuándo ir más lejos:
- Si el servicio tiene dependencias externas críticas (DB, caches, APIs de terceros).
- Si una falla silenciosa tiene costo de negocio directo.
- Si el contenedor se reinicia pero el problema persiste porque el error no es de proceso.
Un criterio útil antes de escribir el endpoint: preguntá qué querés que haga Docker si este check falla. Si la respuesta es "reiniciar el contenedor", verificá que reiniciar realmente solucione ese tipo de falla. Si el problema es una DB caída, reiniciar el contenedor no ayuda — y puede generar un loop de reinicios que agrava la situación.
Errores comunes y gotchas que la documentación no anticipa
1. --start-period demasiado corto para apps pesadas.
Apps con migraciones, warmup de caché o inicialización lenta necesitan un --start-period generoso. Si los primeros checks fallan antes de que la app termine de arrancar, Docker puede marcar el contenedor como unhealthy antes de que esté listo. En Railway, eso puede traducirse en reinicios prematuros.
2. El endpoint /health que hace demasiado.
Si el check conecta a la DB, llama a una API externa y verifica storage en cada ejecución cada 30 segundos, estás agregando carga artificial y creando un punto de falla nuevo. Un check profundo puede fallar por timeout aunque todo lo demás esté bien.
3. exit 1 sin contexto en los logs.
Cuando el healthcheck falla, Docker solo registra que falló — no el output del comando. Si no agregás logging propio dentro del endpoint o del script, diagnosticar por qué el check falla en producción se vuelve más difícil de lo necesario.
# Mejor: script wrapper con logging antes del exit
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
CMD /bin/sh -c 'curl -fsS http://localhost:3000/health > /tmp/hc.log 2>&1 || (cat /tmp/hc.log; exit 1)'
4. Confundir liveness con readiness.
Docker HEALTHCHECK es básicamente un liveness check: ¿el proceso sigue vivo? Kubernetes separa liveness de readiness de manera explícita. En Docker standalone o en Railway, esa distinción no existe a nivel de orquestador — así que la responsabilidad cae en cómo diseñás el endpoint y cuándo lo exponés.
5. Asumir que "healthy" implica que el contenedor recibe tráfico correctamente.
HEALTHCHECK afecta el estado reportado por docker ps y puede influir en políticas de reinicio, pero no garantiza que el load balancer (si hay uno por delante) esté enrutando bien. Son capas distintas.
FAQ: Docker healthcheck buenas prácticas
¿Qué exit codes acepta HEALTHCHECK?
Solo 0 (healthy), 1 (unhealthy) y 2 (reserved, ignorado). Cualquier otro código mayor se trata como 1. La lógica está en el exit code del comando definido en CMD — Docker no lee el stdout ni el body HTTP.
¿Debería incluir un check de base de datos en el healthcheck?
Depende de qué querés que haga Docker cuando falle. Si la DB cae y reiniciar el contenedor no soluciona nada, un DB ping en el healthcheck puede generar loops de reinicio sin resolución. Una opción más prudente: verificar la DB en el startup de la app y fallar ahí; usar el healthcheck para confirmar que el proceso sigue respondiendo después de arrancar.
¿Cómo inspecciono el estado de un healthcheck en ejecución?
# Ver estado y logs del último healthcheck
docker inspect --format='{{json .State.Health}}' <container_id> | jq
Esto muestra el log del último intento, el exit code y el timestamp — información útil para diagnosticar sin adivinar.
¿El healthcheck afecta el comportamiento de docker compose up?
Sí, si usás depends_on con condition: service_healthy. En ese caso, el servicio dependiente no arranca hasta que el check pase. Eso es útil, pero requiere que --start-period esté bien calibrado; si no, el compose puede quedarse esperando indefinidamente.
¿Railway usa el HEALTHCHECK del Dockerfile?
Railway respeta el HEALTHCHECK definido en el Dockerfile para determinar si el deploy fue exitoso. Si el check no pasa dentro del período de gracia, el deploy puede quedar en estado de error. Vale revisar los logs del deploy si el servicio queda en loop.
¿Cuándo tiene sentido no definir HEALTHCHECK?
En contenedores de tareas cortas (batch jobs, scripts de migración), donde el concepto de "salud continua" no aplica. También en desarrollo local donde el overhead de configurarlo no aporta valor real.
Cierre: la señal es limitada, y eso está bien — si lo sabés
Un healthcheck bien configurado es una red de seguridad básica: detecta que el proceso murió o quedó zombi, y le da al orquestador una señal para actuar. Eso es valioso. Pero si construís toda la operación de un servicio sobre esa única señal, estás prometiéndote más de lo que el mecanismo puede darte.
Mi postura es esta: usá HEALTHCHECK para lo que hace bien — liveness — y construí observabilidad real para todo lo demás. Logs estructurados, métricas de aplicación, alertas externas. No porque el healthcheck sea malo, sino porque ninguna herramienta debería cargar con responsabilidades que no le corresponden.
El próximo paso concreto: abrí el Dockerfile de cualquier servicio que tengas corriendo y preguntate qué pasa si ese endpoint devuelve 200 pero la DB está caída. Si la respuesta es "no me entero hasta que alguien reporta un error", tenés un gap de observabilidad que el healthcheck nunca va a cerrar.
Si llegaste hasta acá y estás pensando en cómo evaluar las dependencias de un proyecto antes de meterlas en un stack operativo, puede interesarte lo que escribí sobre cómo evaluar dependencias npm antes de incorporarlas. Y si el tema de señales en sistemas te llama la atención, el post sobre Sniffnet para monitorear tráfico de red tiene una lógica parecida: herramientas con alcance claro, sin sobreprometer.
Fuente original:
- Docker HEALTHCHECK reference: https://docs.docker.com/reference/dockerfile/#healthcheck
Este artículo fue publicado originalmente en juanchi.dev
Top comments (0)