Healthchecks de Docker que realmente detectan fallos (no solo «proceso en ejecución»)

¿Te fue útil?

Seguro lo has visto: el contenedor aparece “Up”, el PID está vivo y los paneles están verdes. Mientras tanto los usuarios reciben 502,
la cola crece como mala hierba y el de guardia aprende nuevas malas palabras en tres idiomas.

Los healthchecks de Docker están pensados para evitar este tipo de autoengaño. Con demasiada frecuencia se escriben como el primer script
de monitorización de un becario nervioso: “¿el proceso está en ejecución?” Eso no es un healthcheck. Es un control de pulso en un paciente
que puede ya ser clínicamente inútil.

Define “saludable” con intención

“Saludable” no es “en ejecución”. Un contenedor puede estar en ejecución mientras no hace nada útil: atascado en un mutex, bloqueado por DNS,
sin descriptores de archivo, incapaz de alcanzar su base de datos, devolviendo solo páginas de error o descartando mensajes silenciosamente. Tu trabajo es
elegir una definición de salud que se alinee con el éxito visible para el usuario.

Piensa en capas:

  • Salud del proceso: el binario está vivo, no está en crash-loop, no fue OOM-killado.
  • Salud del servicio: puede aceptar peticiones y producir respuestas correctas dentro de límites (latencia, códigos de estado, saneamiento del payload).
  • Salud de dependencias: puede alcanzar lo que necesita (DB, cache, broker, DNS) y degradarse intencionalmente cuando no puede.
  • Salud de capacidad: no está “vivo pero saturado” (pool de hilos agotado, cola llena, disco lleno).

Para Docker, los healthchecks son más útiles para responder a una pregunta: ¿Este contenedor debe considerarse elegible para atender tráfico?
Esa es una cuestión de readiness. Docker no tiene una sonda de readiness separada como Kubernetes, así que debes:

  1. Usar los healthchecks de Docker como “readiness” y no hacerlos tan agresivos que maten un contenedor por un fallo transitorio.
  2. O tratarlos como “liveness”, pero entonces acepta que perderás casos de “vivo pero roto”.

Mi postura: en pilas solo Docker (Compose, Swarm, docker run simple), haz que los healthchecks se comporten como readiness:
“¿puedo hacer el trabajo útil mínimo ahora mismo?” Eso captura los fallos que importan a los usuarios.

Datos interesantes y un poco de historia

  • Dato 1: Los healthchecks de Docker se introdujeron en la era de Docker 1.12, principalmente para soportar comportamiento orquestado sin herramientas externas.
  • Dato 2: El estado de salud se guarda por contenedor y es visible vía docker inspect; no es una métrica de primera clase salvo que la exportes.
  • Dato 3: Compose inicialmente no condicionaba depends_on en la salud; Compose moderno puede hacerlo, pero la gente todavía asume que siempre fue así.
  • Dato 4: Swarm usa el estado de salud para decisiones de scheduling de servicio, pero “healthy” no siempre significa “en el balanceador” en todas las configuraciones.
  • Dato 5: Un healthcheck se ejecuta dentro de los namespaces del contenedor, por lo que ve DNS del contenedor, enrutamiento y sistema de archivos—con ventajas y desventajas.
  • Dato 6: El comando del healthcheck se ejecuta con /bin/sh -c cuando usas CMD-SHELL; los bugs de comillas y sorpresas con PATH son comunes.
  • Dato 7: Importa el código de salida, no stdout. A los humanos les encanta la salida bonita; a Docker le gusta 0 y “no 0”.
  • Dato 8: Los “endpoints de salud” iniciales en aplicaciones web se popularizaron porque los balanceadores necesitaban una señal barata sí/no; nunca fueron pensados como una suite de monitorización completa.
  • Dato 9: Un “check” que tarda demasiado es efectivamente una denegación de servicio contra tu propio contenedor si se acumula o consume recursos escasos.

Cómo Docker evalúa realmente los healthchecks

Los healthchecks de Docker son una pequeña máquina de estados: empiezan en starting, luego pasan a healthy si las comprobaciones pasan,
y a unhealthy tras un número configurado de fallos. Es simple, eso es su atractivo y su trampa.

Qué hacen realmente los controles

  • interval: con qué frecuencia ejecutar la comprobación.
  • timeout: cuánto esperar antes de considerar que la comprobación falló.
  • retries: fallos consecutivos necesarios para marcar como unhealthy.
  • start_period: ventana de gracia donde los fallos no cuentan (pero las comprobaciones siguen ejecutándose).

Un healthcheck debería ser:

  • Barato (milisegundos a decenas de milisegundos cuando todo está bien).
  • Acotado (timeouts estrictos; sin colgarse).
  • Representativo (prueba lo que los usuarios realmente necesitan).
  • Difícil de falsear (no solo comprobar que existe un archivo).

Una cita operativa útil para escribir en una nota adhesiva:
“La esperanza no es una estrategia.” — Gene Kranz (citado a menudo en contextos de fiabilidad ingeniería).

Principios de diseño que detectan fallos reales

1) Sondea la ruta crítica, no solo el camino feliz

Si tu servicio existe para servir HTTP respaldado por una base de datos, la ruta crítica es “aceptar petición → ejecutar una consulta DB barata → devolver”.
Tu healthcheck debería ejercitar ese camino al coste mínimo.

Evita una comprobación que solo toque un endpoint en memoria que nunca toca dependencias. Así tienes checks verdes mientras
la base de datos está ardiendo.

2) Prefiere transacciones sintéticas baratas sobre “auto-pruebas” completas

Un buen check es una transacción diminuta con presupuesto acotado: una consulta ligera, un ping, un pequeño GET que ejerza enrutamiento, auth y serialización.
Un mal check es una suite de integración completa dentro de contenedores de producción.

3) Decide qué fallos deben disparar un reinicio vs. solo sacarte del tráfico

Los healthchecks de Docker no reinician contenedores automáticamente a menos que los combines con políticas o comportamiento de un orquestador. Además,
reiniciar ante cada fallo de dependencia es la manera clásica de convertir “un fallo transitorio de DB” en “una caída total”.

Si la dependencia está abajo, muchas veces quieres permanecer arriba y servir respuestas degradadas, o al menos mantener el proceso caliente mientras espera.
Los healthchecks pueden usarse para sacarte de la rotación (o evitar que entres demasiado pronto), no necesariamente para matarte.

4) Falla rápido ante la exhaustión de recursos

“Vivo pero saturado” es uno de los modos de fallo más caros porque parece un éxito parcial. Tu check debería detectar:
pools de hilos atascados, colas de peticiones llenas, disco lleno o imposibilidad de crear archivos/sockets.

Un truco útil es comprobar que puedes hacer una pequeña asignación o abrir un socket, sin convertir el healthcheck en un benchmark.

5) Haz el check determinista y local, pero no despistado

Debe ejecutarse rápido y de forma consistente. Eso significa:

  • Usar timeouts fijos (curl --max-time, timeout).
  • Minimizar saltos de red.
  • Evitar llamar APIs de terceros desde healthchecks. Si lo haces, estás subcontratando tu disponibilidad a sus límites de tasa.

Chiste #1: Un healthcheck que llama a una API externa es como preguntarle al vecino si tu casa está en llamas—por mensaje—durante un tornado.

6) Devuelve códigos de salida significativos y registra suficiente contexto

Docker registra las últimas salidas del healthcheck. Úsalo. Imprime una línea con contexto en caso de fallo: qué dependencia falló, qué timeout, qué estado.
No imprimas megabytes. Esto no es una caja negra de vuelo.

Patrones de healthcheck que funcionan en producción

Patrón A: Endpoint HTTP de salud con sampling de dependencias

Construye un /healthz que compruebe:

  • la app puede aceptar peticiones
  • el pool de conexiones DB puede pedir una conexión y ejecutar un SELECT 1 (o equivalente)
  • conectividad a cache/broker si son requisitos estrictos

Mantenlo barato. Si necesitas una comprobación profunda, ponla en otro endpoint (por ejemplo, /readyz vs /healthz),
pero Docker te da una sola palanca, así que probablemente elegirás la versión tipo “readiness”.

Patrón B: Sondeo directo de dependencias desde el contenedor

Cuando no puedes cambiar la app, haz lo siguiente: sondea el servicio externamente desde el namespace del contenedor.
Por ejemplo: verificar que el puerto HTTP local devuelve 200 y que el puerto TCP de la DB es alcanzable.

Esto es menos semánticamente rico que checks a nivel de app, pero aún detecta la clase de fallos “el proceso está vivo, pero el socket no escucha”.

Patrón C: Lag de la cola / umbral de backlog (con cuidado)

Para consumidores, “saludable” puede significar “estoy haciendo progreso”. Eso puede ser:

  • el offset de mensajes avanza
  • profundidad de cola por debajo de un umbral
  • la tasa de dead letters no se dispara

El peligro es el flapping: la profundidad de la cola puede subir de forma natural. Usa umbrales con reintentos y ventanas temporales, no una sola muestra.

Patrón D: Detectar deadlocks y stalls del event loop

Algunos de los peores incidentes son deadlocks y stalls: el proceso está vivo, el puerto está abierto, pero las peticiones nunca terminan.
Tu healthcheck debe incluir un presupuesto estricto de tiempo de respuesta. Un “éxito” lento es un fallo disfrazado.

Patrón E: Sanidad de disco y sistema de archivos para contenedores con estado

Si el contenedor escribe en un volumen, necesitas saber cuándo el sistema de archivos está lleno, montado en solo-lectura o los permisos fallaron tras
un cambio de imagen. Comprueba que puedes escribir y fsync un archivo pequeño en la ruta prevista—una vez en un tiempo, no cada segundo.

Patrón F: Sanidad DNS (porque los fallos de DNS parecen “todo está roto”)

Un servicio que no puede resolver nombres internos fallará de maneras que parecen fallos de dependencias. Un sencillo getent hosts
o nslookup contra un nombre requerido puede ahorrarte tiempo.

Ejemplos base (Dockerfile / Compose)

En un Dockerfile, usa healthchecks solo si la imagen realmente se hace cargo del contrato de runtime. Si la salud depende de dependencias específicas del despliegue,
los healthchecks a nivel de Compose suelen encajar mejor.

cr0x@server:~$ cat Dockerfile
FROM alpine:3.20
RUN apk add --no-cache curl
HEALTHCHECK --interval=10s --timeout=2s --retries=3 --start-period=20s \
  CMD curl -fsS --max-time 1 http://127.0.0.1:8080/healthz || exit 1
cr0x@server:~$ cat compose.yaml
services:
  api:
    image: example/api:1.9.3
    healthcheck:
      test: ["CMD-SHELL", "curl -fsS --max-time 1 http://127.0.0.1:8080/healthz || exit 1"]
      interval: 10s
      timeout: 2s
      retries: 3
      start_period: 25s
    depends_on:
      db:
        condition: service_healthy
  db:
    image: postgres:16
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres -h 127.0.0.1 || exit 1"]
      interval: 5s
      timeout: 3s
      retries: 10
      start_period: 20s

Observa la inclinación: la API se chequea a sí misma en localhost (sin ambigüedad de red), y Postgres usa su propia herramienta barata de readiness.
Si puedes usar sondas diseñadas para el propósito (pg_isready, redis-cli ping), hazlo.

Tareas prácticas: comandos, salidas, decisiones (12+)

Los healthchecks fallan por razones que rara vez son misteriosas. Suelen ser “red”, “DNS”, “timeouts”, “permisos” o “exhaustión de recursos”.
Aquí hay tareas concretas que te permiten dejar de adivinar.

Task 1: Ver el estado de salud del contenedor y la última salida del check

cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Status}}'
NAMES           STATUS
api             Up 2 hours (unhealthy)
db              Up 2 hours (healthy)

Qué significa: Docker piensa que el healthcheck de api está fallando repetidamente.

Decisión: Inspecciona los logs de salud inmediatamente; no reinicies a ciegas aún.

cr0x@server:~$ docker inspect --format '{{json .State.Health}}' api | jq
{
  "Status": "unhealthy",
  "FailingStreak": 7,
  "Log": [
    {
      "Start": "2026-02-04T09:44:00.123456789Z",
      "End": "2026-02-04T09:44:01.126789012Z",
      "ExitCode": 1,
      "Output": "curl: (28) Operation timed out after 1000 milliseconds with 0 bytes received\n"
    }
  ]
}

Qué significa: Tu check está expirando, no devolviendo un código distinto de 200. Eso es un stall, un problema de binding o una saturación severa.

Decisión: Comprueba si el servicio está escuchando y si las peticiones a localhost se completan dentro del presupuesto.

Task 2: Validar exactamente el comando del healthcheck como Docker lo ejecuta

cr0x@server:~$ docker inspect --format '{{json .Config.Healthcheck.Test}}' api
["CMD-SHELL","curl -fsS --max-time 1 http://127.0.0.1:8080/healthz || exit 1"]

Qué significa: Se ejecuta vía shell, así que aplican las semánticas de shell.

Decisión: Ejecuta ese mismo comando dentro del contenedor para confirmar que el entorno y herramientas coinciden con las suposiciones.

Task 3: Ejecutar la sonda manualmente dentro del contenedor

cr0x@server:~$ docker exec -it api sh -lc 'curl -fsS --max-time 1 http://127.0.0.1:8080/healthz; echo "rc=$?"'
curl: (28) Operation timed out after 1000 milliseconds with 0 bytes received
rc=28

Qué significa: El endpoint no responde en 1 segundo desde dentro del contenedor.

Decisión: Determina si no está escuchando, está atascado o es demasiado lento. Pasa a comprobaciones de socket y logs de la app.

Task 4: Comprobar si el puerto está escuchando (sin asumir que netstat existe)

cr0x@server:~$ docker exec -it api sh -lc 'ss -lntp | head'
State  Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
LISTEN 0      4096   0.0.0.0:8080      0.0.0.0:*    users:(("api",pid=1,fd=7))

Qué significa: El servicio está escuchando en 8080.

Decisión: Si escucha pero el healthcheck expira, sospecha stall de la aplicación, agotamiento de hilos o bloqueo por dependencias.

Task 5: Medir latencia y código de estado con un presupuesto estricto

cr0x@server:~$ docker exec -it api sh -lc 'curl -s -o /dev/null -w "code=%{http_code} time=%{time_total}\n" --max-time 2 http://127.0.0.1:8080/healthz'
code=200 time=1.873

Qué significa: Devuelve 200, pero es lento—casi 2 segundos.

Decisión: O aumentas el presupuesto del healthcheck (con cautela) o arreglas la lentitud. No “arregles” esto subiendo timeouts a 30 segundos.

Task 6: Revisar logs del contenedor alrededor de la ventana de fallo

cr0x@server:~$ docker logs --since 10m --tail 200 api
2026-02-04T09:41:12Z WARN db pool exhausted; waiting for connection
2026-02-04T09:41:13Z WARN db query timeout after 1500ms
2026-02-04T09:41:15Z ERROR /healthz dependency=db timeout=1500ms

Qué significa: El endpoint de salud está haciendo lo correcto: reporta problemas con la DB. El servicio no está lo bastante sano para atender.

Decisión: Deja de ajustar los healthchecks. Arregla la DB o el dimensionamiento del pool de conexiones. Investiga la carga en DB y la alcanzabilidad.

Task 7: Validar conectividad DB desde dentro del contenedor de la app

cr0x@server:~$ docker exec -it api sh -lc 'nc -vz -w 1 db 5432; echo "rc=$?"'
db (172.20.0.3:5432) open
rc=0

Qué significa: Existe conectividad TCP; no es una partición de red básica.

Decisión: Mira la carga en DB, autenticación, configuración de pools, latencia de DNS o consultas lentas—cosas por encima de TCP.

Task 8: Comprobar tiempo de resolución DNS (asesino oculto)

cr0x@server:~$ docker exec -it api sh -lc 'time getent hosts db'
172.20.0.3      db

real    0m0.003s
user    0m0.000s
sys     0m0.002s

Qué significa: La búsqueda DNS/hosts es rápida aquí.

Decisión: Si esto es lento (centenares de ms), arregla la configuración de resolv, DNS de Docker o dominios de búsqueda. No culpes primero a la DB.

Task 9: Comprobar agotamiento de descriptores de archivo

cr0x@server:~$ docker exec -it api sh -lc 'cat /proc/1/limits | grep -i "open files"'
Max open files            1024                 1024                 files

Qué significa: Límite bajo de FD. Bajo carga puedes alcanzar esto y volverte “vivo pero inútil”.

Decisión: Aumenta ulimit en la definición del servicio; también busca fugas de FD.

Task 10: Buscar OOM kills y presión de memoria

cr0x@server:~$ docker inspect --format 'OOMKilled={{.State.OOMKilled}} ExitCode={{.State.ExitCode}}' api
OOMKilled=false ExitCode=0

Qué significa: No es un OOM kill en este ciclo de vida del contenedor.

Decisión: Si fuera true, arregla límites de memoria, fugas o dimensionamiento de heap en JVM/node. Los healthchecks no salvarán un proceso que el kernel está matando.

Task 11: Confirmar que la herramienta del healthcheck existe en la imagen

cr0x@server:~$ docker exec -it api sh -lc 'command -v curl || echo "curl missing"'
/usr/bin/curl

Qué significa: El binario existe donde se esperaba.

Decisión: Si falta, no “arregles” usando rarezas de busybox. Añade la herramienta o reescribe el check en lo que realmente incluyes en la imagen.

Task 12: Verificar que el healthcheck no esté consumiendo CPU o creando zombies

cr0x@server:~$ docker exec -it api sh -lc 'ps -o pid,ppid,stat,comm | head -n 10'
PID  PPID STAT COMMAND
1    0    S    api
42   1    S    worker
77   1    S    worker
201  1    S    curl

Qué significa: Si ves una pila creciente de procesos curl, tu healthcheck puede estar colgándose y acumulándose.

Decisión: Añade timeouts (--max-time) y asegura que el comando no pueda bloquearse indefinidamente.

Task 13: Usa events para correlacionar “unhealthy” con reinicios y despliegues

cr0x@server:~$ docker events --since 30m --filter container=api
2026-02-04T09:31:00Z container health_status: healthy
2026-02-04T09:40:10Z container health_status: unhealthy
2026-02-04T09:40:11Z container exec_start: curl -fsS --max-time 1 http://127.0.0.1:8080/healthz || exit 1

Qué significa: Se volvió unhealthy en un momento específico. Ese es tu punto de pivote.

Decisión: Revisa historial de despliegues, recargas de config, ventanas de mantenimiento de DB, rotaciones de certificados, cambios de DNS en ese instante.

Task 14: Inspeccionar el gateo de dependencias en Compose (¿te estás fiando de una mentira?)

cr0x@server:~$ docker compose ps
NAME            IMAGE             COMMAND                  SERVICE   STATUS
stack-api-1     example/api       "..."                    api       running (unhealthy)
stack-db-1      postgres:16       "docker-entrypoint..."   db        running (healthy)

Qué significa: Compose inició la API y sigue en ejecución pero marcada como unhealthy.

Decisión: Si esperabas que Compose retrasara el inicio de la API hasta que la DB esté lista, verifica que las condiciones de depends_on son soportadas y correctas.

Task 15: Verificar que “unhealthy” afecta al tráfico (puede que no lo haga)

cr0x@server:~$ docker inspect --format '{{.State.Health.Status}} {{.Name}}' api
unhealthy /api

Qué significa: Docker sabe que está unhealthy, pero tu proxy reverso puede seguir enviando tráfico.

Decisión: Confirma que tu balanceador/proxy integra el estado de salud, o implementa un mecanismo explícito de salud en el upstream.

Chiste #2: Si tu proxy ignora la salud del contenedor, esa línea “HEALTHCHECK” es básicamente una planta decorativa—viva, verde y sin resolver problemas.

Playbook de diagnóstico rápido

Cuando suena el pager y un contenedor está “unhealthy”, tu tarea es encontrar el cuello de botella antes de “arreglarlo” y convertirlo en un fallo mayor.
Aquí el orden que minimiza el tiempo hasta la verdad.

Primero: prueba exactamente qué está fallando

  1. Inspecciona la última salida del healthcheck (docker inspect ... .State.Health). ¿Es timeout, no-200, error DNS, error de autenticación?
  2. Ejecuta el mismo comando manualmente dentro del contenedor. Si pasa interactivamente, puede haber diferencias de entorno, PATH o temporización.
  3. Comprueba si el servicio escucha en el puerto esperado (ss -lntp).

Segundo: clasifica el modo de fallo

  1. Timeout: sospecha deadlock, agotamiento de pool de hilos, pausa de GC, bloqueo por downstream o problemas de recursos del kernel.
  2. Connection refused: proceso no escucha, se cayó o está ligado a la interfaz equivocada.
  3. Fallo DNS: problemas de resolv, red equivocada, dominios de búsqueda pesados o nombre de servicio inexistente.
  4. No-200: lógica a nivel de app, dependencia fallando, config errónea, migraciones incompletas.

Tercero: verifica dependencias y recursos

  1. Alcanzabilidad TCP a dependencias (nc -vz).
  2. Logs de la app por agotamiento de pools, timeouts, fallos de auth.
  3. Límites y uso de FD (/proc/1/limits, lsof si está disponible).
  4. Espacio en disco y estado de montajes (dentro del contenedor si escribe en volúmenes).

Cuándo reiniciar vs. cuándo mantener la calma

  • Reiniciar ayuda si el proceso está bloqueado (deadlock) y tu sistema tolera la pérdida de trabajo en vuelo.
  • Reiniciar perjudica si la dependencia está caída y la app necesita caches calientes, migraciones o lógica de backoff. Amplificarás la carga y alargarás la recuperación.

Errores comunes: síntomas → causa raíz → solución

Error 1: El healthcheck es “ps | grep”

Síntomas: La salud siempre aparece healthy hasta que el proceso se cae, pero los usuarios ven errores durante minutos/horas.

Causa raíz: Compruebas existencia, no funcionalidad.

Solución: Sondea la interfaz real del servicio (HTTP/TCP) con un timeout estricto; opcionalmente incluye sampling de dependencias.

Error 2: El healthcheck toca un endpoint “OK” estático

Síntomas: La salud permanece verde durante caídas de DB; las tasas de error se disparan.

Causa raíz: El endpoint no valida dependencias críticas.

Solución: Haz que /healthz incluya checks baratos de dependencias (tomar conexión DB, ping a cache). Manténlo rápido y acotado.

Error 3: Sin timeouts, las comprobaciones se cuelgan y se acumulan

Síntomas: Muchos procesos curl atascados; CPU sube; el contenedor se vuelve inestable.

Causa raíz: El comando del healthcheck bloquea indefinidamente; Docker sigue programándolo.

Solución: Usa curl --max-time o timeout; mantén timeout del comando menor que el timeout del healthcheck de Docker.

Error 4: El healthcheck es demasiado estricto y provoca flapping

Síntomas: El contenedor oscila healthy/unhealthy; ocurren reinicios; el tráfico cambia constantemente.

Causa raíz: Umbrales demasiado sensibles; intervalo muy corto; retries bajos; start_period pequeño.

Solución: Incrementa retries, alarga el intervalo, añade start_period. También arregla las puntas de latencia subyacentes—no solo acolches timeouts.

Error 5: El healthcheck usa nombres DNS externos no resolubles dentro del contenedor

Síntomas: curl: (6) Could not resolve host; intermitente, específico del entorno.

Causa raíz: DNS del contenedor difiere del host; red faltante; dominios de búsqueda incorrectos.

Solución: Usa nombres de servicio en la red de Docker; valida con getent hosts y configura DNS explícitamente si es necesario.

Error 6: El éxito del healthcheck no afecta al tráfico

Síntomas: El contenedor está unhealthy, pero las peticiones siguen llegando; los usuarios ven errores.

Causa raíz: El proxy/balanceador no está conectado al estado de salud del contenedor.

Solución: Configura el proxy para usar sus propios checks upstream o intégralo con el comportamiento del orquestador; no asumas que el estado de Docker cambia el ruteo.

Error 7: El healthcheck incluye consultas DB costosas

Síntomas: La carga en DB aumenta con el número de réplicas; los healthchecks fallan bajo carga y empeoran el incidente.

Causa raíz: Los healthchecks se convirtieron en un mini test de carga; compiten con el tráfico de producción.

Solución: Usa consultas de tiempo constante y baratas; cachea resultados brevemente en la app si hace falta; reduce frecuencia.

Error 8: El healthcheck depende de herramientas no presentes en imágenes mínimas

Síntomas: El healthcheck siempre falla con “command not found”.

Causa raíz: Usaste curl, bash o nc pero distribuiste scratch/distroless sin ellas.

Solución: Añade un pequeño binario probe preparado, usa el endpoint nativo de la app o incluye intencionalmente la herramienta mínima.

Listas de verificación / plan paso a paso

Paso a paso: escribe un healthcheck que capture la realidad

  1. Elige el contrato visible al usuario. “¿Puede servir HTTP en menos de 500ms y alcanzar la DB?” Escríbelo.
  2. Decide comportamiento readiness vs liveness. En Docker puro, por defecto a estilo readiness.
  3. Elige el método de sondeo. Prefiere endpoint de la app; si no, TCP/HTTP local con timeouts acotados.
  4. Añade timeouts estrictos. Cada check debe terminar rápido, incluso cuando el sistema está enfermo.
  5. Maneja el warm-up correctamente. Usa start_period para evitar falsos negativos durante migraciones y calentamiento de caché.
  6. Manténlo barato. Un ping a DB, no una consulta de informe. Una petición HTTP, no un flujo de login completo.
  7. Haz que las fallas se expliquen solas. Salida de una sola línea como db timeout o http 503.
  8. Prueba tu endpoint de salud bajo carga. Debe mantenerse rápido; si se enlentece primero, no es una señal fiable.
  9. Conecta la salud a decisiones de tráfico. Si tu proxy la ignora, arregla eso o no finjas que los healthchecks controlan ruteo.
  10. Revisa tras incidentes. Cada caída te enseña qué no captó tu healthcheck.

Lista de verificación: no envíes hasta que esto sea cierto

  • El comando del healthcheck tiene un timeout duro más corto que el timeout de Docker.
  • El healthcheck se prueba dentro de la imagen en ejecución (no en tu portátil).
  • El endpoint de salud comprueba al menos una dependencia crítica si el servicio no puede funcionar sin ella.
  • El start period cubre el peor tiempo de arranque en producción (migraciones, cachés fríos).
  • Las fallas son accionables a partir de la última línea del log de salud.
  • Entiendes qué hace “unhealthy” en tu despliegue (¿reinicio? ¿fuera del tráfico? ¿nada?).

Tres micro-historias corporativas desde el terreno

Micro-historia 1: El incidente causado por una suposición errónea

Una fintech mediana ejecutaba una pila Docker Compose detrás de un proxy reverso. El equipo añadió healthchecks y se felicitó:
la API esperaba a la base de datos usando depends_on, así que la secuencia de arranque estaba “resuelta”.

Luego un parche rutinario de la base de datos reinició el host de DB. Los contenedores de API se mantuvieron arriba, siguieron aceptando peticiones y empezaron
a devolver 500. Los healthchecks permanecieron verdes porque /health era un endpoint estático “OK”. El proxy siguió enviando tráfico
a todas las réplicas, que ahora eran fábricas de errores.

El de guardia asumió que Docker dejaría de enrutar a contenedores unhealthy, porque eso es lo que “salud” significa en lenguaje humano.
Pero su proxy no leía el estado de salud de Docker; solo comprobaba si el puerto TCP upstream estaba abierto.

La solución no fue dramática. Cambiaron el endpoint para incluir un ping barato a la DB y configuraron el proxy para hacer su propio
chequeo upstream contra ese endpoint. También documentaron, en lenguaje llano, qué afecta el estado de salud en su stack.
Lo importante no fue el código. Fue eliminar la suposición equivocada antes de que les arruinara el sábado.

Micro-historia 2: La optimización que salió mal

Una compañía de medios tenía una gran flota de contenedores API sin estado. Alguien notó que los healthchecks generaban “tráfico DB innecesario”:
una consulta rápida cada 5 segundos por contenedor suma. El equipo “optimizó” cambiando el healthcheck para verificar solo que el proceso corría y el puerto estaba abierto.

Un mes después tuvieron un incidente donde la configuración del pool de conexiones DB había regresado. Bajo carga, la API aceptaba
peticiones y luego se bloqueaba esperando una conexión DB hasta que los timeouts de los clientes disparaban. El puerto seguía abierto y el proceso vivo.
Los healthchecks estaban felices. Los usuarios no.

El outage fue largo porque los síntomas eran confusos: la CPU no estaba al máximo, la memoria parecía bien y los logs de error eran ruidosos pero no definitivos.
La “optimización” eliminó la única señal automatizada que hubiera clasificado el fallo rápidamente: la saturación de dependencias.

Reintrodujeron un healthcheck consciente de dependencias, pero con presupuesto más inteligente: menor frecuencia, un ping DB barato y algo de cache
dentro del endpoint de salud para evitar stampedes al DB. También aprendieron una verdad aburrida: si optimizas fuera la observabilidad, pagarás después con intereses.

Micro-historia 3: La práctica aburrida pero correcta que salvó el día

Un proveedor SaaS empresarial tenía la costumbre, que parecía tediosa: cada servicio tenía un “contrato de salud” escrito en el repo.
Especificaba qué comprueba /healthz, qué no y qué presupuesto de tiempo debe respetar. También incluía un snippet del runbook:
el comando exacto del healthcheck y cómo reproducirlo dentro del contenedor.

Durante una rotación de certificados, un subconjunto de contenedores empezó a fallar en TLS saliente hacia una dependencia. El endpoint de salud
hacía una llamada mínima a la dependencia y empezó a devolver 503 con una razón de una línea: tls handshake failed.

El de guardia no tuvo que adivinar si era DNS, enrutamiento o código. Ejecutó el comando documentado dentro de un contenedor afectado,
vio el mismo error TLS y comparó diferencias de entorno. El problema se rastreó a un bundle CA anticuado en una imagen base usada
por una línea de servicios. La solución fue reconstruir con el bundle CA correcto y redeployar.

Nadie escribió un postmortem heroico porque no fue heroico. Fue predecible, rápidamente clasificado y solucionado. La práctica aburrida—contratos estándar y checks reproducibles—impidió que el incidente se convirtiera en una historia folclórica de la empresa.

Preguntas frecuentes

1) ¿Debería mi healthcheck de Docker reiniciar el contenedor automáticamente?

Los healthchecks de Docker por sí solos no reinician contenedores. Los reinicios vienen de políticas de restart o de orquestadores. Decide según el modo de fallo:
reinicia por deadlocks/crashes; evita bucles de reinicio cuando las dependencias están caídas.

2) ¿Cuál es la diferencia entre healthchecks de Docker y liveness/readiness de Kubernetes?

Kubernetes separa “¿debo reiniciar?” (liveness) de “¿debo enviar tráfico?” (readiness). Docker te da un único estado de salud,
así que debes elegir qué representa y asegurarte de que tu capa de ruteo lo respete.

3) ¿Qué tan estrictos deben ser los timeouts?

Lo bastante estrictos para detectar stalls, pero no tan estrictos que la jitter normal te marque como unhealthy. Un patrón común es timeout de 1–2 segundos,
intervalo de 10 segundos, 3 retries, con start_period acorde al peor arranque.

4) ¿Deben los healthchecks incluir consultas a la base de datos?

Si el servicio no puede funcionar sin la DB, sí—usa una consulta o ping barato con timeout estricto. Si el servicio puede degradarse con gracia, considera un check más ligero y expón el estado de dependencias por separado para alertas.

5) Mi endpoint de salud causa carga. ¿Qué hago?

Haz el trabajo de tiempo constante, cachea resultados brevemente y reduce la frecuencia. No elimines checks de dependencias por completo; ajústalos.
Asegúrate también de que los endpoints de salud sean baratos (sin auth pesada, sin serialización JSON gigante).

6) ¿Por qué mi healthcheck pasa en docker exec pero falla en el estado de Docker?

Causas comunes: shell diferente, diferencias en PATH, variables de entorno faltantes o temporización (tu prueba manual ocurre en un buen momento).
Compara el comando exacto de .Config.Healthcheck.Test y ejecútalo con sh -lc.

7) ¿Debo usar CMD o CMD-SHELL para healthchecks?

Prefiere CMD (forma exec) cuando sea posible porque evita problemas de quoting en shell. Usa CMD-SHELL cuando necesites pipes,
condicionales o múltiples checks. Si usas shell, sé explícito y cuidadoso.

8) ¿Cómo evito flapping durante migraciones de inicio?

Usa start_period lo bastante largo para cubrir peores casos de migraciones y arranques fríos. También haz que el endpoint de salud devuelva
una razón clara de “starting” durante el calentamiento si es posible.

9) ¿Puedo hacer que los healthchecks validen espacio en disco o montajes de volúmenes?

Sí, y deberías hacerlo para cualquier cosa que escriba en volúmenes. Comprueba que la ruta es escribible y que no está llena. Hazlo barato y no muy frecuente.

10) ¿Está bien que un healthcheck llame a otros servicios?

Solo si esos servicios son requisitos estrictos para la corrección. Mantén las llamadas mínimas y acotadas. Evita llamadas a terceros y evita checks “profundos” costosos en la ruta caliente.

Siguientes pasos que puedes hacer esta semana

Si tus healthchecks actuales son “proceso en ejecución”, no estás comprobando salud—estás comprobando si las luces están encendidas.
Sustitúyelos por sondas que reflejen el trabajo mínimo útil que tu servicio debe hacer, con timeouts estrictos y salida de fallo significativa.

Pasos prácticos:

  1. Elige un servicio crítico y reescribe su healthcheck para golpear localhost HTTP con un presupuesto de 1–2 segundos.
  2. Actualiza el endpoint de salud para samplear al menos una dependencia crítica (de forma barata) y devolver texto de fallo accionable.
  3. Añade start_period basado en tiempo real de arranque, no en optimismo.
  4. Verifica qué hace “unhealthy” en tu capa de ruteo; haz que importe, o deja de fingir que importa.
  5. Después del siguiente incidente, actualiza el contrato de salud y el runbook para que el tú del futuro no tenga que redescubrir el mismo fallo a las 3 a.m.
← Anterior
Error de activación de Windows 0xC004F213: solución sin llamar al soporte
Siguiente →
Respaldar distribuciones WSL: exportar/importar de la manera correcta

Deja un comentario