La interrupción más costosa es la que parece saludable. Los paneles están en verde, los despliegues se marcan como “exitosos”,
y mientras tanto los clientes se quedan mirando timeouts porque el contenedor está arriba, pero el servicio no.
Docker informa felizmente running. Tu orquestador se encoge de hombros. Aun así recibes la alerta.
Los healthchecks deberían ser el detector de mentiras. Muy a menudo son una pegatina en la frente que dice “OK”
porque alguien ejecutó curl localhost una vez y dio por terminado el trabajo. Arreglemos eso—con comprobaciones que coincidan
con los modos reales de fallo, que no creen otros nuevos y que realmente te ayuden a tomar decisiones bajo presión.
Qué son (y qué no son) los healthchecks de Docker
Un healthcheck de Docker es un comando que Docker ejecuta dentro del contenedor según un cronograma.
El comando sale con código 0 si está sano, distinto de cero si está enfermo. Docker rastrea un estado:
starting, healthy o unhealthy.
Eso es todo. No hay magia. No hay consenso distribuido. No hay garantía de “mi app funciona para los clientes”.
Es una sonda local. Útil, pero solo si la orientas al objetivo correcto.
Lo que un healthcheck de Docker puede hacer bien
- Detectar deadlocks y procesos trabados que aún tienen un PID.
- Detectar fallos de dependencias locales (socket de base de datos que no acepta, autenticación de cache fallando).
- Controlar la secuencia de arranque en Docker Compose para que no se acumulen sobre la base de datos.
- Proveer una señal a orquestadores y humanos: “este contenedor te está mintiendo”.
Lo que no puede hacer (deja de pedírselo)
- Probar éxito de extremo a extremo para el usuario. Una comprobación local no valida DNS, enrutamiento, ACLs externas o comportamiento del cliente.
- Sustituir métricas. “Healthy” es un booleano. Latencia, saturación y presupuesto de errores no lo son.
- Arreglar despliegues malos. Si tu comprobación está equivocada, certificará con confianza lo incorrecto.
Aquí está la verdad operacional: un healthcheck es un contrato. Defines “suficientemente sano para recibir tráfico”
o “suficientemente sano para seguir ejecutándose.” Escribe ese contrato como si tu pager dependiera de él, porque depende.
Datos interesantes y contexto (lo que explica el desorden actual)
- Docker añadió
HEALTHCHECKen 2015 (era Docker 1.12), en gran parte para soportar patrones de orquestación antes de que Swarm/Kubernetes dominaran. - Los healthchecks se ejecutan dentro del namespace del contenedor, por lo que ven DNS del contenedor, sockets locales y localhost de forma distinta que el host.
- Docker rastrea el estado de salud en los metadatos del contenedor y lo expone vía
docker inspect; no es una línea de log a menos que la busques. - El
depends_onoriginal de Compose no esperaba readiness; versiones posteriores introdujeron dependencias condicionales basadas en salud, pero muchas pilas aún usan el modelo mental antiguo. - Kubernetes popularizó “liveness” y “readiness” separados, lo que hizo que la gente se diera cuenta de que un único endpoint de salud a menudo mezcla dos objetivos incompatibles.
- Los primeros “endpoints de salud” a menudo devolvían simplemente “200 OK” porque los balanceadores solo necesitaban un latido; los sistemas modernos necesitan readiness consciente de dependencias y comportamiento de fallo rápido.
- cgroups y la estrangulación de CPU pueden hacer que apps sanas parezcan muertas; un timeout de 1s bajo presión de CPU es básicamente un lanzamiento de moneda.
- Las políticas de reinicio existen desde antes de los healthchecks en muchas configuraciones; los operadores unieron checks a reinicios y accidentalmente construyeron bucles de denegación de servicio auto-infligidos.
Modos de fallo que los despliegues “verdes” ocultan
1) El proceso está vivo; el servicio está muerto
El clásico: tu proceso principal aún se ejecuta, pero está atascado. Deadlock, GC infinito, esperando una dependencia rota,
bloqueado por disco o trabado en un mutex. Docker informa “Up.” Tu reverse proxy hace timeout.
Un buen healthcheck prueba el comportamiento del servicio, no la existencia de un PID.
2) Puerto abierto, app no lista
Los sockets TCP en escucha aparecen pronto. Los frameworks hacen eso. Tu app aún está calentando caches, ejecutando migraciones,
cargando modelos o esperando a la base de datos.
Un chequeo de puerto es una señal tipo liveness. La readiness requiere confirmación a nivel de aplicación.
3) Falla parcial de dependencias
Tu servicio puede responder a /health, pero no puede hablar con Redis por un desajuste de auth,
no puede escribir en Postgres por cambios de permisos o no puede alcanzar una API externa porque cambiaron las reglas de egress.
Si no incluyes comprobaciones de dependencias, desplegarás un fallo bellamente saludable.
4) Falla lenta: funciona, pero no a tiempo
Bajo carga o vecinos ruidosos, la latencia supera los timeouts del cliente. Tu endpoint de salud aún devuelve 200—eventualmente.
Mientras tanto, los usuarios ven errores.
Tu healthcheck debe tener un presupuesto de latencia y aplicarlo con timeouts. Un endpoint “saludable después de 30 segundos” es solo optimismo.
5) La propia comprobación causa la interrupción
Comprobaciones que golpean endpoints costosos, ejecutan migraciones o abren nuevas conexiones a la BD cada segundo son una gran forma de
tumbar un sistema mientras te felicitas por “añadir fiabilidad”.
Broma #1: Un healthcheck que DDoSea tu propia base de datos sigue siendo técnicamente “probando producción”. Simplemente no es el tipo de prueba que querías.
Principios de diseño: comprobaciones que dicen la verdad
Define qué significa “saludable” operacionalmente
Elige una de estas y sé explícito:
- Listo para tráfico: puede servir solicitudes reales dentro de una latencia tipo SLO y tiene las dependencias requeridas.
- Seguro para seguir ejecutándose: el proceso no está trabado; puede progresar; puede apagarse con gracia.
Docker te da un solo estado de salud. Eso es molesto. Aun así puedes modelar ambos eligiendo qué te importa para ese contenedor.
Para proxies de borde importa “listo para tráfico”. Para un worker en background a menudo importa más “seguro para seguir ejecutándose”.
Fallar rápido, pero con cabeza
Una buena comprobación falla rápidamente cuando el contenedor está genuinamente roto, pero no hace flapping durante el arranque normal
o breves problemas de dependencia.
- Usa
start_periodpara evitar penalizar calentamientos lentos. - Usa timeouts para que una llamada trabada no bloquee el healthcheck para siempre.
- Usa reintentos para evitar que una pérdida de paquete convierta en una tormenta de reinicios.
Prefiere sondas locales, baratas y determinísticas
Los healthchecks se ejecutan con frecuencia. Haz que sean:
- Locales: golpea localhost, un socket UNIX o estado en proceso.
- Baratas: evita consultas pesadas, evita crear nuevos pools de conexiones.
- Determinísticas: misma entrada, misma salida; sin aleatoriedad; sin “a veces es lento”.
Incluye dependencias, pero elige la profundidad adecuada
Si tu servicio no puede operar sin Postgres, tu chequeo debe confirmar que puede autenticarse y ejecutar una consulta trivial.
Si puede degradarse (servir contenido cacheado), no marques el contenedor como malo solo porque Redis está caído.
Operacionalmente: tu healthcheck debe reflejar tu comportamiento de fallo previsto.
Usa códigos de salida intencionalmente
A Docker solo le importa cero vs distinto de cero, pero a los humanos les importa por qué. Haz que tu chequeo imprima una razón corta
en stderr/stdout antes de salir con código distinto de cero. Esa razón aparece en docker inspect.
Haz que las comprobaciones prueben el mismo camino que usa tu tráfico
Si el tráfico real pasa por Nginx hacia tu app, verificar la app directamente puede saltarse toda una clase de fallos:
configuración rota de Nginx, conexiones de worker agotadas, DNS upstream malo, problemas TLS. A veces quieres que el proxy verifique su upstream.
Otras veces quieres que un LB externo verifique el proxy. Enmarca tus comprobaciones como capas, igual que tus fallos.
Cita (idea parafraseada) — Jim Gray: “Trata la falla del sistema como normal; diseña como si los componentes fallaran en cualquier momento.”
Guía de diagnóstico rápido: encuentra el cuello de botella pronto
Cuando un contenedor está “saludable” pero los usuarios fallan, no necesitas filosofía. Necesitas una secuencia.
Este es el orden que suele sacar a la luz la restricción real más rápido.
Primero: ¿la señal de salud miente o el sistema cambió?
- Revisa el estado de salud de Docker y los últimos logs de salud de ese contenedor.
- Confirma que tu comprobación está probando lo correcto (dependencia, ruta, latencia).
- Compara la configuración antes/después del deploy respecto a cambios en el healthcheck, timeouts y start_periods.
Segundo: ¿el servicio realmente sirve en la interfaz esperada?
- Dentro del contenedor: verifica puertos en escucha, DNS y conectividad local.
- Desde el host: verifica el mapeo de puertos y reglas de firewall.
- Desde un contenedor par: verifica descubrimiento de servicios y equivalentes de políticas de red.
Tercero: ¿estamos bloqueados por CPU, memoria, disco o una dependencia?
- La estrangulación de CPU y picos de carga pueden convertir un manejador de 200ms en un timeout de 5s.
- Los OOM kills pueden producir ciclos de “funciona… hasta que no funciona”.
- La saturación de disco puede congelar servicios intensivos en I/O mientras el proceso permanece vivo.
- La saturación de dependencias (máx conexiones DB) a menudo parece timeouts aleatorios de la app.
Cuarto: ¿el healthcheck está causando daño?
- Frecuencia y coste: ¿está golpeando la BD o el pool de hilos de la app?
- Concurrencia: los healthchecks no deben acumularse.
- Efectos secundarios: los endpoints de salud deben ser de solo lectura.
El truco es tratar los healthchecks como cualquier otro generador de carga en producción. Porque lo son.
Tareas prácticas (comandos, salida esperada y qué decides)
Estos son movimientos reales de operador. Cada tarea incluye: un comando, lo que significa la salida y la decisión que tomas.
Úsalos durante incidentes y en momentos de calma cuando intentas prevenir el siguiente.
Tarea 1: Ver el estado de salud de un vistazo
cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Image}}'
NAMES STATUS IMAGE
api-1 Up 12 minutes (healthy) myorg/api:1.9.3
db-1 Up 12 minutes (healthy) postgres:16
worker-1 Up 12 minutes (unhealthy) myorg/worker:1.9.3
Qué significa: Docker está ejecutando healthchecks e informando estado. (unhealthy) no es sutil.
Decisión: Si un componente crítico está unhealthy, deja de tratar “Up” como éxito. Investiga antes de escalar o enrutar tráfico.
Tarea 2: Inspeccionar el log de salud y la razón del fallo
cr0x@server:~$ docker inspect --format '{{json .State.Health}}' worker-1 | jq
{
"Status": "unhealthy",
"FailingStreak": 5,
"Log": [
{
"Start": "2026-01-02T08:11:12.123456789Z",
"End": "2026-01-02T08:11:12.223456789Z",
"ExitCode": 1,
"Output": "redis ping failed: NOAUTH Authentication required\n"
}
]
}
Qué significa: La comprobación falla consistentemente y muestra una razón útil. Bendito quien escribió esa salida.
Decisión: Arregla credenciales/config en lugar de reiniciar a ciegas. También considera si el fallo de auth en Redis debe marcar al worker como unhealthy o degradado.
Tarea 3: Ejecutar manualmente el comando del healthcheck dentro del contenedor
cr0x@server:~$ docker exec -it api-1 sh -lc 'echo $0; /usr/local/bin/healthcheck.sh; echo exit=$?'
sh
ok: http=200 db=ok redis=ok latency_ms=27
exit=0
Qué significa: Estás ejecutando la misma sonda que Docker ejecuta. Tuvo éxito y respondió rápido.
Decisión: Si los usuarios siguen fallando, el problema probablemente esté fuera de la vista local de este contenedor (red, proxy, LB, DNS) o hay un desajuste entre criterios de salud y ruta de usuario.
Tarea 4: Confirmar qué healthcheck ejecuta Docker realmente
cr0x@server:~$ docker inspect --format '{{json .Config.Healthcheck}}' api-1 | jq
{
"Test": [
"CMD-SHELL",
"/usr/local/bin/healthcheck.sh"
],
"Interval": 30000000000,
"Timeout": 2000000000,
"StartPeriod": 15000000000,
"Retries": 3
}
Qué significa: Interval 30s, timeout 2s, start period 15s, retries 3. Estos números son el comportamiento.
Decisión: Si ves timeout=1s en un servicio JVM bajo límites de CPU, has encontrado un incidente futuro. Afínalo ahora.
Tarea 5: Ver transiciones de salud en vivo
cr0x@server:~$ docker events --filter container=api-1 --filter event=health_status --since 10m
2026-01-02T08:03:12.000000000Z container health_status: healthy api-1
2026-01-02T08:08:42.000000000Z container health_status: unhealthy api-1
2026-01-02T08:09:12.000000000Z container health_status: healthy api-1
Qué significa: Flapping: pasa a unhealthy y luego se recupera. Eso es o un problema intermitente real o una comprobación demasiado sensible.
Decisión: Si el flapping coincide con la carga, probablemente haya saturación de recursos. Si es aleatorio, aumenta timeout/retries e investiga red/DNS.
Tarea 6: Validar DNS y enrutamiento contenedor-a-contenedor
cr0x@server:~$ docker exec -it api-1 sh -lc 'getent hosts db && nc -zvw2 db 5432'
172.20.0.3 db
db (172.20.0.3:5432) open
Qué significa: DNS resuelve y la conexión TCP funciona desde el contenedor de la app hacia la base de datos.
Decisión: Si esto falla, no pierdas tiempo dentro de la app. Arregla la red, el nombre del servicio o la configuración de la red de Compose.
Tarea 7: Probar que la app escucha donde crees
cr0x@server:~$ docker exec -it api-1 sh -lc 'ss -lntp | head'
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 4096 0.0.0.0:8080 0.0.0.0:* users:(("java",pid=1,fd=123))
Qué significa: El servicio escucha en 0.0.0.0:8080. Si estuviera en 127.0.0.1 solamente, el mapeo de puertos podría estar “up” pero inalcanzable externamente.
Decisión: Si está ligado mal, arregla la dirección de bind de la app. No lo soluciones con networking de host a menos que disfrutes del arrepentimiento.
Tarea 8: Probar la ruta real de la petición desde el host (mapeo de puertos)
cr0x@server:~$ curl -fsS -m 2 -D- http://127.0.0.1:18080/healthz
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 2
ok
Qué significa: A través del puerto publicado, el endpoint de salud responde dentro de 2 segundos.
Decisión: Si curl desde el host falla pero curl dentro del contenedor funciona, tu problema es mapeo, firewall, proxy o la app ligada a la interfaz equivocada.
Tarea 9: Identificar estrangulación de CPU que hace que los checks hagan timeout
cr0x@server:~$ docker stats --no-stream api-1
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
a1b2c3d4e5f6 api-1 198.32% 1.2GiB / 1.5GiB 80.12% 1.3GB / 1.1GB 25MB / 2MB 93
Qué significa: Alto uso de CPU y memoria. Si además configuraste timeouts ajustados, verás falsos negativos bajo carga.
Decisión: O asigna más CPU/memoria, reduce trabajo o amplía el timeout de salud para que “saludable bajo carga esperada” siga siendo cierto.
Tarea 10: Revisar OOM kills o reinicios que se hacen pasar por “inestabilidad”
cr0x@server:~$ docker inspect --format 'RestartCount={{.RestartCount}} OOMKilled={{.State.OOMKilled}} ExitCode={{.State.ExitCode}}' api-1
RestartCount=2 OOMKilled=true ExitCode=137
Qué significa: Exit code 137 y OOMKilled=true: el kernel lo mató. Tu healthcheck no falló; el contenedor murió.
Decisión: Arregla límites de memoria, fugas o picos. También asegúrate de que tu start_period de arranque no sea demasiado corto, o acumularás fallos durante la recuperación.
Tarea 11: Detectar stalls de I/O de disco que congelan procesos “saludables”
cr0x@server:~$ docker exec -it db-1 sh -lc 'ps -o stat,comm,pid | head'
STAT COMMAND PID
Ss postgres 1
Ds postgres 72
Ds postgres 73
Qué significa: Procesos en estado D están atrapados en espera de I/O no interrumpible. No responderán a tus consultas amables.
Decisión: Deja de culpar a la aplicación. Investiga latencia de almacenamiento, saturación de disco del host, vecinos ruidosos o drivers de volumen.
Tarea 12: Confirmar la readiness de una dependencia con una sonda específica (Postgres)
cr0x@server:~$ docker exec -it db-1 sh -lc 'pg_isready -U postgres -h 127.0.0.1 -p 5432; echo exit=$?'
127.0.0.1:5432 - accepting connections
exit=0
Qué significa: Postgres acepta conexiones. Esto es mejor que una prueba de puerto abierto porque habla el protocolo.
Decisión: Si esto falla intermitentemente, revisa max connections, checkpoints, disco o recuperación. No aumentes retries de salud esperando que eso lo arregle.
Tarea 13: Validar que tu endpoint de salud es lo suficientemente rápido (presupuesto de latencia)
cr0x@server:~$ docker exec -it api-1 sh -lc 'time -p curl -fsS -m 1 http://127.0.0.1:8080/healthz >/dev/null'
real 0.04
user 0.00
sys 0.00
Qué significa: 40ms localmente. Genial. Si ves 0.9–1.0s con un timeout de 1s, vives al límite.
Decisión: Define un timeout con margen. Los healthchecks deben fallar por lentitud real, no por jitter normal bajo carga.
Tarea 14: Atrapar suposiciones erróneas sobre “localhost” (proxy vs app)
cr0x@server:~$ docker exec -it nginx-1 sh -lc 'curl -fsS -m 1 http://127.0.0.1:8080/healthz || echo "upstream unreachable"'
upstream unreachable
Qué significa: Dentro del contenedor de Nginx, localhost es Nginx—no tu contenedor de app. Esta es una de las 10 causas principales de healthchecks inútiles.
Decisión: Apunta las comprobaciones al hostname/servicio upstream correcto, o ejecuta la comprobación en el contenedor correcto. “Funciona en mi contenedor” no es una estrategia de red.
Patrones de healthcheck para servicios comunes
Patrón A: Servicio HTTP con readiness consciente de dependencias
Para una API, una buena comprobación suele validar:
el hilo del servidor HTTP puede responder rápidamente y las dependencias centrales son alcanzables y autenticadas.
Manténlo minimal: una consulta diminuta, un ping de caché, una comprobación interna superficial.
Ejemplo: HEALTHCHECK en Dockerfile llamando a un script
cr0x@server:~$ cat Dockerfile
FROM alpine:3.20
RUN apk add --no-cache curl ca-certificates
COPY healthcheck.sh /usr/local/bin/healthcheck.sh
RUN chmod +x /usr/local/bin/healthcheck.sh
HEALTHCHECK --interval=30s --timeout=2s --start-period=20s --retries=3 CMD ["/usr/local/bin/healthcheck.sh"]
cr0x@server:~$ cat healthcheck.sh
#!/bin/sh
set -eu
t0=$(date +%s%3N)
# Fast local HTTP check (service path)
code=$(curl -fsS -m 1 -o /dev/null -w '%{http_code}' http://127.0.0.1:8080/healthz || true)
if [ "$code" != "200" ]; then
echo "http failed: code=$code"
exit 1
fi
# Optional dependency: DB shallow check via app endpoint (preferred) or direct driver probe
# Here we assume /readyz includes db connectivity check inside the app.
code2=$(curl -fsS -m 1 -o /dev/null -w '%{http_code}' http://127.0.0.1:8080/readyz || true)
if [ "$code2" != "200" ]; then
echo "ready failed: code=$code2"
exit 1
fi
t1=$(date +%s%3N)
lat=$((t1 - t0))
echo "ok: http=200 ready=200 latency_ms=$lat"
exit 0
Por qué funciona: La comprobación impone un presupuesto de tiempo y valida la interfaz real del servicio.
Evita lógica profunda de dependencias en shell cuando la app puede hacerlo mejor (y reutilizar pools existentes).
Patrón B: Contenedores de base de datos: usa herramientas nativas, no “puerto abierto”
Si haces healthcheck de Postgres, usa pg_isready. Para MySQL, usa mysqladmin ping.
Para Redis, usa redis-cli ping. Estas sondas hablan suficiente protocolo para ser significativas.
Ejemplo: Postgres en Compose
cr0x@server:~$ cat compose.yaml
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: example
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -h 127.0.0.1 -p 5432"]
interval: 5s
timeout: 3s
retries: 10
start_period: 10s
Por qué funciona: Captura “el proceso está arriba pero no acepta conexiones”, incluyendo recuperación o misconfig.
No requiere una consulta real, así que es barato.
Patrón C: Workers en background: comprobaciones basadas en progreso
Los workers a menudo no tienen HTTP. Tu comprobación debe validar:
que el proceso puede hablar con la cola y que no está atascado.
Si puedes emitir un archivo heartbeat o un timestamp ligero de “último trabajo procesado”, hazlo.
Un worker que puede conectarse a Redis pero no ha hecho progreso en 10 minutos no está sano. Solo está en línea.
Patrón D: Proxies inversos: comprueba el upstream, no a ti mismo
Nginx “saludable” mientras el upstream está caído es irrelevante si el proxy es la puerta de entrada del tráfico.
O bien compruebas la alcanzabilidad del upstream o configuras el load balancer para comprobar un endpoint que refleje el estado del upstream.
Broma #2: Un proxy inverso que responde 200 mientras el upstream está en llamas es como una recepcionista diciendo “todos están en reunión” durante una evacuación.
Compose, orquestación, dependencias y la realidad del arranque
Docker Compose es donde los healthchecks o te salvan o te hacen sobreconfiar.
La trampa: la gente asume que depends_on significa “esperar hasta que esté listo.” Históricamente significaba “iniciar en orden.”
Esos no son lo mismo, y puedes imaginar cuál de los dos le importa a tu base de datos.
Usa dependencias basadas en salud donde realmente importe
Si la API se caerá en bucle hasta que la BD esté arriba, puedes sujetar el arranque de la API a la salud de la BD en Compose.
Esto reduce ruido y evita estampidas sobre la base de datos al arrancar.
Ejemplo: Dependencia de Compose en salud
cr0x@server:~$ cat compose.yaml
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: example
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -h 127.0.0.1 -p 5432"]
interval: 5s
timeout: 3s
retries: 12
start_period: 10s
api:
image: myorg/api:1.9.3
depends_on:
db:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "curl -fsS -m 1 http://127.0.0.1:8080/readyz || exit 1"]
interval: 10s
timeout: 2s
retries: 3
start_period: 30s
Chequeo de realidad: Esto mejora el orden de arranque, no la fiabilidad a largo plazo. Si la BD muere después, Compose no orquestará mágicamente un failover elegante.
Pero evita la “todo arranca a la vez, todo falla a la vez” durante el arranque.
No dejes que los healthchecks se conviertan en puertas de despliegue que no puedes razonar
Si haces que la readiness incluya todas las dependencias externas, un upstream inestable puede bloquear despliegues y rollbacks.
Eso no es resiliencia; es acoplamiento. Define lo que es requerido para servir tu tráfico.
Separa el calentamiento de inicio de la salud en estado estable
El arranque es especial. Se ejecutan migraciones. Los compiladores JIT despiertan. Se cargan certificados. Las caches DNS están frías.
Por eso existe start_period: para evitar reinicios y estados “unhealthy” durante un calentamiento esperado.
Pero no lo abuses. Un start period de 10 minutos oculta fallos reales durante 10 minutos. Si lo necesitas, probablemente necesites un mejor diseño de inicialización.
Reinicios, healthchecks y la espiral de muerte
Los healthchecks de Docker no reinician contenedores automáticamente por sí mismos. Algo más lo hace: tu orquestador, tus scripts, supervisores,
o una automatización “lista” que interpreta unhealthy como “reiniciar ahora”.
Aquí es donde las buenas intenciones van a morir.
La clásica espiral de muerte
- Una dependencia se vuelve lenta (picos de latencia en DB).
- El healthcheck hace timeout (demasiado agresivo).
- La automatización reinicia contenedores.
- La tormenta de reinicios aumenta la carga (caches fríos, reconexiones, migraciones, replays).
- La dependencia se vuelve más lenta.
Cómo evitarla
- Los healthchecks deben ser diagnósticos primero. Reinicia como último recurso, no por reflejo.
- Usa backoff en cualquier sistema que decida reiniciar.
- Haz tu comprobación barata y acotada en tiempo, y ajusta timeouts al jitter esperado bajo carga.
- Diseña modos degradados: si Redis está caído, ¿puedes seguir sirviendo solo lectura? Entonces no falles la readiness por Redis.
Un healthcheck que desencadena reinicios es un arma cargada. Trátalo como tal.
Healthchecks vs monitoring vs métricas: deja de confundirlos
Un healthcheck es una sonda local binaria. El monitoring es un sistema que te dice cuando violas objetivos.
Las métricas son los datos que explican el porqué.
Cuándo usar healthchecks
- Para evitar enrutar tráfico a contenedores que no pueden servirlo.
- Para evitar iniciar servicios dependientes demasiado pronto.
- Para sacar a la luz estados rotos obvios de forma rápida y consistente.
Cuándo no usar healthchecks
- Como sustituto de percentiles de latencia y tasas de error.
- Como señal única para reiniciar. Así construyes amplificadores de fallo elegantes.
- Para “probar todo el mundo” desde dentro de un contenedor. Para eso están las pruebas de integración y el synthetic monitoring.
Patrón de dos niveles: salud interna + chequeos sintéticos externos
El patrón robusto es por capas:
- Healthcheck interno: barato, rápido, lo bastante consciente de dependencias para la corrección local.
- Chequeo sintético externo: golpea la ruta pública real, ejerce DNS/TLS/enrutamiento y mide latencia.
Esto captura las dos grandes clases de mentiras: “el contenedor está bien pero la ruta está rota” y “la ruta está bien pero el contenedor está muerto.”
Tres micro-historias corporativas desde el terreno
Micro-historia 1: El incidente causado por una suposición equivocada
Una empresa ejecutaba una pequeña plataforma interna: un contenedor de proxy inverso delante de varios contenedores API.
Añadieron un healthcheck de Docker al proxy que hacía curl http://127.0.0.1/ y devolvía 0 con 200.
El proxy siempre respondía 200, incluso cuando los upstreams estaban caídos, porque la ruta raíz servía una página estática de “bienvenida”.
Durante un despliegue rutinario, un servicio upstream no arrancó por una variable de entorno faltante.
El proxy siguió devolviendo 200 al balanceador. El tráfico fluyó. Los usuarios recibieron un 502 amigable y un timeout.
El monitoring mostraba “proxy healthy”. El pipeline de despliegue mostraba “todos los contenedores corriendo”. Todos se quedaron mirando gráficas sin creerlo,
como si la incredulidad pudiera restaurar el servicio.
La suposición equivocada fue sutil y muy humana: “Si el proxy está arriba, el servicio está arriba.”
Pero para los clientes, el proxy es solo la puerta de entrada. Si la habitación detrás está en llamas, que la puerta esté abierta no es éxito.
Lo arreglaron definiendo dos endpoints:
/healthz para “el proceso del proxy está vivo”, y /readyz para “upstreams críticos alcanzables y devolviendo estado esperado.”
El balanceador revisaba /readyz. El healthcheck de Docker del proxy también comprobaba /readyz,
con timeouts sensatos para evitar fallos en cascada cuando el upstream estaba lento.
El resultado inmediato fue menos drama: los upstreams mal configurados dejaron de convertirse en agujeros negros de tráfico en producción.
El resultado a largo plazo fue cultural: la gente dejó de usar “contenedor corriendo” como sinónimo de “el servicio funciona.”
Micro-historia 2: La optimización que salió mal
Otra organización quería “detección más rápida” de contenedores rotos. Ajustaron los healthchecks:
interval 2 segundos, timeout 200ms, retries 1. Se felicitaron por tomarse la fiabilidad en serio.
En un día tranquilo, parecía bien.
Luego llegaron picos normales. La estrangulación de CPU se activó porque los contenedores tenían límites de recursos para ahorrar costes.
Las pausas de GC crecieron. La latencia de disco aumentó un poco por snapshots en segundo plano del host.
Las peticiones todavía tenían éxito, pero a veces en 400–700ms en lugar de 80ms. Los healthchecks hicieron timeout.
Su automatización trató “unhealthy” como “reiniciar inmediatamente.” Un reinicio provocó misses de cache, que provocaron más tráfico a la BD,
que provocó más latencia, que provocó más fallos en los healthchecks. Pronto tuvieron un desfile sincronizado de reinicios.
Los sistemas no estaban “caídos” al principio; la política de salud los hizo caer.
Se recuperaron revirtiendo la agresividad del healthcheck y añadiendo backoff a los reinicios.
También separaron las comprobaciones: un check rápido de liveness para “proceso responde” y una readiness más tolerante
con timeout más largo y múltiples reintentos.
La lección no fue “nunca optimices.” Fue “optimiza lo correcto.”
La detección más rápida es inútil si tu detector es más frágil que el servicio que debería proteger.
Micro-historia 3: La práctica aburrida pero correcta que salvó el día
Un tercer equipo ejecutaba servicios stateful con volúmenes persistentes. Tenían una costumbre que nadie celebraba:
cada servicio tenía un contrato de salud documentado, y el comando de healthcheck imprimía un resumen de estado en una sola línea
incluyendo un timestamp y estados clave de dependencias.
Una mañana, un subconjunto de contenedores quedó unhealthy tras una ventana de mantenimiento del host.
Las aplicaciones estaban arriba, pero sus healthchecks informaban db=ok y disk=slow, con latencias en milisegundos.
Ese “disk=slow” no fue genialidad. Fue un umbral simple: escribir un archivo temporal pequeño y medir cuánto tardó.
El on-call no tuvo que adivinar. Revisó estadísticas de disco del host, encontró que un backend de volúmenes estaba fallando,
y drenó el host afectado. El tráfico cambió de lugar. Los errores bajaron. Nadie reescribió código a las 3 a.m., que siempre es la victoria real.
Más tarde, ajustaron el healthcheck para que la lentitud de disco no marcara inmediatamente unhealthy a menos que persistiera varias intervalos.
La práctica aburrida—salida de salud consistente y contrato documentado—convirtió un incidente vago en un árbol de decisiones limpio.
Esto es lo que la “excelencia operacional” parece en la vida real: mayormente poco sexy, ocasionalmente salvavidas.
Errores comunes (síntomas → causa raíz → solución)
1) Síntoma: El contenedor está “saludable” pero los usuarios reciben 502/504
Causa raíz: El healthcheck solo prueba proceso local o una página estática, no dependencias upstream ni la ruta real de la petición.
Solución: Comprueba la misma ruta que el tráfico real (proxy-a-upstream), o expón un /readyz que valide dependencias críticas con presupuesto de tiempo.
2) Síntoma: Contenedores hacen flap entre healthy/unhealthy bajo carga
Causa raíz: Timeout demasiado bajo, el healthcheck compite con tráfico real por CPU/hilos, o picos de latencia en dependencias.
Solución: Aumenta timeout, añade reintentos, usa start_period y haz la comprobación más barata. Valida estrangulación de CPU y saturación de pools de hilos.
3) Síntoma: Los despliegues se quedan colgados porque los servicios nunca se vuelven healthy
Causa raíz: El healthcheck requiere dependencias externas que son opcionales, o depende de migraciones de datos que tardan más que start_period.
Solución: Reduce la readiness a “puede servir tráfico de forma segura” (no “el mundo es perfecto”), y mueve migraciones largas a un job one-shot.
4) Síntoma: La BD se ve asediada cada pocos segundos incluso en reposo
Causa raíz: El healthcheck ejecuta consultas pesadas o abre nuevas conexiones DB en cada intervalo; multiplicado por réplicas, se convierte en carga real.
Solución: Usa sondas nativas (pg_isready), consultas superficiales o checks a nivel de app que reutilicen pools. Aumenta intervalo.
5) Síntoma: El healthcheck pasa localmente pero falla solo en producción
Causa raíz: Suposiciones sobre localhost, DNS, certificados, cabeceras de proxy o auth específico del entorno.
Solución: Ejecuta la comprobación en el mismo namespace de red donde se ejecutará (el contenedor). Valida descubrimiento de servicio desde pares, no solo desde tu laptop.
6) Síntoma: Unhealthy desencadena reinicios, y los reinicios empeoran la situación
Causa raíz: Automatización de reinicios sin backoff + check demasiado estricto + amplificación por arranque en frío.
Solución: Añade backoff, amplia umbrales, separa semánticas de liveness y readiness, y evita reiniciar por jitter transitorio de dependencias.
7) Síntoma: El propio healthcheck causa picos de latencia
Causa raíz: El check golpea endpoints costosos (grafo completo de dependencias, rebuild de caches, handshake de auth) con demasiada frecuencia.
Solución: Crea un endpoint dedicado y barato, cachea resultados de salud brevemente en la app y asegúrate de que la comprobación no tenga efectos secundarios.
8) Síntoma: El healthcheck siempre devuelve healthy, incluso cuando la app está trabada
Causa raíz: La comprobación es solo “puerto abierto” o “proceso existe”, o llama a un handler que no ejerce el subsistema trabado.
Solución: Incluye una operación trivial que requiera progreso (por ejemplo, encolar/desencolar noop, ejecutar una pequeña consulta DB, o verificar un tick del event loop).
Listas de verificación / plan paso a paso
Paso a paso: escribe un healthcheck que no te avergüence
- Escribe el contrato: “Saludable significa X. No saludable significa Y.” Guárdalo en el repo junto al Dockerfile.
- Elige el objetivo: tipo liveness (progreso) o tipo readiness (seguro para servir tráfico). No finjas que un booleano hace ambas cosas perfectamente.
- Escoge un presupuesto de latencia: elige un timeout que refleje condiciones reales más margen (no tu laptop en Wi‑Fi).
- Start period: configura
start_periodsegún tiempos de arranque medidos, no por esperanza. - Reintentos: pon retries para tolerar jitter transitorio pero sin ocultar fallos persistentes.
- Hazlo barato: evita endpoints pesados y tormentas de conexiones. Prefiere checks en-app con pool o sondas nativas.
- Hazlo observable: imprime una razón de fallo de una línea y un resumen conciso en éxito.
- Prueba bajo estrés: ejecuta healthchecks mientras CPU e I/O están limitados; mira si hacen flap.
- Decide la política de reinicio: quién reinicia por unhealthy, con qué backoff. Documenta e implementa deliberadamente.
- Revisa regularmente: los healthchecks envejecen al cambiar sistemas. Agrégalos a tu lista de revisiones de “corrección en producción”.
Checklist: sanity previa al despliegue para stacks Compose
- ¿Las dependencias usan
condition: service_healthydonde corresponda? - ¿Los healthchecks usan hostnames correctos (nombres de servicio), no
127.0.0.1entre contenedores? - ¿Son los intervalos razonables (no 1–2s en docenas de réplicas a menos que la comprobación sea realmente trivial)?
- ¿Están timeouts y retries ajustados para la carga y jitter esperados?
- ¿Evitan las comprobaciones efectos secundarios y consultas pesadas?
- ¿Falla las comprobaciones por los modos de fallo que realmente te importan?
Checklist: respuesta a incidentes cuando están involucrados healthchecks
- ¿Falla el healthcheck porque el servicio está roto o porque la comprobación es demasiado estricta?
- ¿La automatización de reinicio está amplificando el problema?
- ¿Los logs de salud muestran una dependencia específica fallando (auth, DNS, timeout)?
- ¿El cuello de botella es CPU, memoria, disco o saturación upstream?
- ¿Puedes degradar en lugar de fallar rotundamente?
Preguntas frecuentes
1) ¿Debería cada contenedor tener un healthcheck?
No. Añade healthchecks donde impulsan una decisión: enrutamiento, dependencias o diagnóstico rápido.
Para jobs one-shot o sidecars triviales, un healthcheck puede ser ruido. No lo apliques por moda.
2) ¿Cuál es la diferencia entre “liveness” y “readiness” en Docker?
Docker solo expone un estado de salud, pero puedes elegir la semántica.
“Liveness” es “el proceso aún puede progresar.” “Readiness” es “seguro para recibir tráfico.”
Para frontends y APIs, prioriza readiness. Para workers, prioriza progreso.
3) ¿Por qué no simplemente comprobar que el puerto está abierto?
Porque un puerto puede estar abierto mientras la app está rota: handlers deadlocked, pools de hilos agotados,
dependencias fallando o un proxy sirviendo una página estática. Las comprobaciones de puerto solo detectan los fallos más perezosos.
4) ¿Con qué frecuencia debo ejecutar un healthcheck?
Empieza con 10–30 segundos para la mayoría de servicios. Comprobaciones más rápidas aumentan carga y riesgo de flapping.
Si necesitas detección subsegundo, normalmente estás resolviendo el problema equivocado o usando la herramienta equivocada.
5) ¿Qué timeouts y reintentos debería usar?
Los timeouts deben ser menores que los timeouts de cliente y reflejar latencia esperada bajo carga más margen.
Los reintentos deben tolerar problemas transitorios (1–3) sin enmascarar fallos persistentes. Mide inicio y estado estable por separado.
6) ¿Debe un healthcheck probar dependencias como bases de datos y caches?
Si el servicio no puede funcionar sin ellas, sí—de forma superficial.
Si el servicio puede degradarse, no falles la readiness por dependencias opcionales. Así evitas convertir fallos parciales en totales.
7) ¿Por qué mi healthcheck pasa en un contenedor pero falla en otro?
Namespaces. 127.0.0.1 dentro de un contenedor es ese contenedor. El DNS de servicio difiere entre redes.
Además, certificados y auth pueden ser específicos del entorno. Prueba siempre desde el mismo camino de red desde el que correrá la comprobación.
8) ¿Docker reinicia automáticamente contenedores unhealthy?
No por defecto. Docker informa el estado de salud; los reinicios los manejan políticas de reinicio (al salir) o automatización externa.
Ten mucho cuidado al ligar “unhealthy” a reinicios. Añade backoff y evita tormentas de reinicios.
9) ¿Debe mi endpoint de salud devolver diagnósticos detallados?
Internamente, sí: una cadena de razón corta es oro durante incidentes. Externamente, ten cuidado: no filtres secretos ni topología.
Muchos equipos sirven un /healthz externo minimal y un endpoint de diagnóstico interno protegido.
10) ¿Cómo evito que los healthchecks sobrecarguen la base de datos?
Usa sondas nativas (pg_isready) o checks a nivel de app que reutilicen pools de conexiones.
Aumenta intervalo. Evita consultas que escaneen tablas. Tu healthcheck debe ser más barato que una petición real.
Próximos pasos que puedes desplegar esta semana
Deja de permitir que “verde” signifique “bien”. Los healthchecks son controles de producción, no YAML decorativo.
Si tu comprobación no coincide con los modos reales de fallo, certificará fallos con confianza.
- Audita tus 5 servicios principales: qué prueban exactamente sus healthchecks y si es la misma ruta que toman tus usuarios.
- Añade una señal de readiness consciente de dependencias donde importa (API, proxy, gateway). Manténla superficial y acotada en tiempo.
- Afina timeouts y start periods usando mediciones de arranque y comportamiento bajo carga, no suposiciones.
- Haz la salida accionable: razones de fallo en una línea que aparezcan en
docker inspect. - Revisa el comportamiento de reinicio: si “unhealthy” provoca reinicios, añade backoff y verifica que no estás construyendo una espiral de muerte.
El objetivo no es hacer los healthchecks estrictos. Es hacerlos honestos. Las comprobaciones honestas no evitan todos los incidentes.
Previenen el peor tipo: el que tus sistemas juran que no está ocurriendo.