El health check falla, pero la aplicación funciona
Cuando un health check falla durante el despliegue mientras la aplicación funciona correctamente en local, normalmente se debe a una de cinco causas. Revisa el binding, la ruta, el puerto, los tiempos y las dependencias en el orden que permite encontrar el problema más rápido.
Hay una forma especialmente frustrante de quedarse bloqueado: un health check fallando en el despliegue bloquea cada release mientras la aplicación está, según todos los indicadores que puedes comprobar, perfectamente bien. Funciona en local. Funciona con Docker en local. Los logs muestran que está escuchando. Y la plataforma informa de un fallo tras otro, a veces veinte seguidos, sin que aparezca ninguna solicitud en el access log.
Este último detalle es importante y acota el problema de inmediato. Si tu aplicación nunca registró la solicitud, el check nunca llegó hasta ella; por tanto, nada relacionado con el código de la aplicación va a explicarlo.
Estas son las cinco causas, en el orden que permite encontrar el problema más rápido.
1. Estás haciendo bind a localhost
Esta es la causa más común y explica exactamente el síntoma de que «nunca llega tráfico».
Dentro de un contenedor, 127.0.0.1 significa el loopback del propio contenedor. Un health check que llega desde fuera del contenedor no puede acceder a él. El proceso está escuchando, tus logs lo confirman, pero el socket no es accesible desde ningún lugar relevante.
// Unreachable from outside the container
app.listen(3000, '127.0.0.1')
// Correct
app.listen(3000, '0.0.0.0')
Los frameworks tienen distintos valores predeterminados, y varios de ellos cambiaron el valor por defecto entre versiones principales. Comprueba a qué dirección hace bind realmente tu framework en lugar de confiar en lo que recuerdas.
# Confirm from inside the running container
dockup exec "ss -ltn || netstat -ltn" my-project/my-api
Si la dirección de escucha es 127.0.0.1:3000 en lugar de 0.0.0.0:3000, ya lo has encontrado y nada más de esta lista importa.
2. El puerto que comprueba la plataforma no es el puerto donde sirves la aplicación
Hay dos puertos implicados y es fácil confundirlos: el puerto en el que escucha tu proceso dentro del contenedor y el puerto al que enruta la plataforma. Si tu aplicación lee PORT del entorno y has dejado 3000 escrito directamente en algún punto de un Dockerfile, ambos pueden no coincidir sin que sea evidente.
El patrón fiable consiste en dejar que la plataforma te lo indique:
const port = process.env.PORT || 3000
app.listen(port, '0.0.0.0')
Después, configura el puerto del servicio una sola vez, en la plataforma, y deja de mantener ese número en dos sitios.
3. La ruta devuelve algo distinto de una respuesta correcta
La ruta de un health check se compara exactamente, y un número sorprendente de fallos se debe a una redirección. Si tu aplicación redirige /healthz a /healthz/, o fuerza HTTPS con un 301, un checker que solo considera correctas las respuestas 2xx fallará siempre, mientras que un navegador sigue la redirección y te muestra una página que funciona.
Tres problemas concretos:
- Redirecciones por la barra final.
/healthz→/healthz/es un 301. - HTTPS obligatorio. El check interno normalmente llega mediante HTTP sin cifrar a través del loopback. Una redirección incondicional a HTTPS hace que falle.
- Middleware de autenticación. Un guard global de autenticación que se ejecuta antes del routing también devolverá 401 para la ruta de health.
Excluye explícitamente la ruta de health de la autenticación y de la imposición de HTTPS. Es la única ruta que debería ser aburrida.
4. El check es más rápido que tu cold start
Si el check falla varias veces y después pasa, o falla durante el despliegue y funciona al reintentarlo, el problema está en los tiempos, no en la configuración.
El presupuesto que necesitas no es el de un solo intento: es intervalo × reintentos. Una aplicación que tarda doce segundos en conectarse a su base de datos y preparar una caché necesita un presupuesto total superior a doce segundos; de lo contrario, fallarás en cada release y acabarás desactivando la protección, eliminando así lo único que separa una build defectuosa de tus usuarios.
dockup info my-project/my-api --json | grep -A6 healthCheck
Configura el timeout por encima de la duración de tu intento legítimo más lento y establece suficientes reintentos para que intervalo × reintentos supere holgadamente el arranque legítimo más lento. Mide el arranque en lugar de adivinarlo: los logs tienen marcas de tiempo.
5. La aplicación realmente no está lista
El último caso es precisamente para lo que existe el check: tu aplicación se inició, no pudo acceder a una dependencia y está reintentándolo. No se ha detenido, así que nada la reinicia. No puede servir solicitudes, por lo que el check falla. El sistema funciona exactamente como está diseñado y te indica que esta release no debería recibir tráfico.
La forma de distinguir este caso de los otros cuatro es que tu aplicación registró la solicitud y respondió con un código distinto de 2xx. Si la solicitud aparece en tus logs, las causas 1 a 3 quedan descartadas.
El orden de diagnóstico que ahorra tiempo
# 1. Did the request reach the app at all?
dockup logs my-project/my-api --follow
# 2. What is the process actually bound to?
dockup exec "ss -ltn || netstat -ltn" my-project/my-api
# 3. Does the path answer from inside the container?
dockup exec "curl -si localhost:3000/healthz" my-project/my-api
# 4. What is the gate configured to expect?
dockup info my-project/my-api --json
El paso 3 es el que resuelve la mayoría de estos casos. Un curl desde dentro del contenedor elimina de golpe todas las variables de red: si allí devuelve 200 y la plataforma sigue fallando, el problema está en la dirección o el puerto, no en la aplicación. Si devuelve un 301 o un 401, has encontrado la causa sin tocar la plataforma.
Por qué merece la pena mantener el gate
Después del cuarto despliegue fallido, resulta tentador desactivar el health check y sacar la release adelante. Conviene recordar qué es exactamente lo que estás desactivando.
En Dockup, el health gate es lo que mantiene una release defectuosa alejada de tus usuarios. La nueva versión se compila y se inicia mientras la actual sigue sirviendo tráfico; el tráfico solo cambia cuando la nueva responde. Si desactivas el gate, vuelves a habilitar el escenario en el que un contenedor que se inicia pero no puede funcionar reemplaza a otro que estaba bien.
Un check que falla durante cuatro releases seguidas es molesto. Un check que siempre pasa es un check que no detendrá el despliegue importante.
Preguntas frecuentes
¿Por qué falla el health check cuando la aplicación funciona en local?
Casi siempre porque el contenedor hace bind a 127.0.0.1 en lugar de 0.0.0.0. En local te conectas a través del mismo loopback; desde fuera del contenedor, esa dirección no es accesible.
¿Debe el endpoint de health requerir autenticación? No. Exclúyelo del middleware de autenticación global; de lo contrario, el checker recibirá un 401 y el despliegue fallará aunque la aplicación funcione.
¿Qué timeout debería usar? Uno superior a la duración de tu intento legítimo más lento, con suficientes reintentos para cubrir tu cold start legítimo más lento. Consulta el tiempo de arranque en los logs en lugar de adivinarlo.
¿Es seguro desactivar el health check para desbloquear una release? Desbloquea la release, pero elimina la protección que impide que una versión defectuosa reciba tráfico. Corrige el check; en la mayoría de los casos, la causa es una dirección de bind o una redirección, y se soluciona en minutos.
