Docker health checks: cómo detectar contenedores rotos antes de que te afecten
Etiquetas: docker,docker-compose,devops,linux,automatización,infraestructuraUn contenedor en estado running no significa que el servicio dentro funcione correctamente. He tenido casos en los que Nginx arrancaba, Docker lo veía verde, pero la aplicación devolvía 502 en cada petición. Sin health checks, eso pasa desapercibido hasta que lo detecta un usuario. Con health checks, Docker lo detecta en segundos y puede reiniciar el contenedor automáticamente.
El problema: running no es healthy
Por defecto, Docker solo sabe si el proceso principal del contenedor sigue vivo. Si tu aplicación arranca, entra en un bucle de error interno y sigue corriendo, Docker no tiene forma de saberlo.
$ docker ps
CONTAINER ID IMAGE STATUS PORTS
a3f9c2b1d4e5 mi-app:1.0 Up 3 minutes 0.0.0.0:8080->8080/tcp
Up 3 minutes solo dice que el proceso no ha muerto. No dice nada sobre si responde peticiones HTTP, si la base de datos está accesible desde dentro del contenedor, o si la aplicación está en medio de un deadlock.
HEALTHCHECK en el Dockerfile
La forma más portable de definir un health check es directamente en el Dockerfile. Así el check viaja con la imagen y no depende de cómo se despliegue:
FROM python:3.12-slim
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=10s --start-period=20s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
CMD ["python", "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]
Los parámetros que importan:
| Parámetro | Valor por defecto | Qué hace |
|---|---|---|
--interval |
30s | Cada cuánto ejecuta el check |
--timeout |
30s | Tiempo máximo para que el check responda |
--start-period |
0s | Tiempo de gracia al arrancar (no cuenta fallos) |
--retries |
3 | Fallos consecutivos antes de marcar como unhealthy |
El --start-period es importante: si tu aplicación tarda en arrancar (carga de modelos, migraciones de base de datos, precalentamiento de caché), necesitas darle ese margen o Docker la marcará como unhealthy antes de que esté lista.
El endpoint /health
Para que el health check sea útil, tu aplicación debería exponer un endpoint específico que compruebe más que «estoy vivo». Un /health mínimo en FastAPI:
from fastapi import FastAPI
from sqlalchemy import text
app = FastAPI()
@app.get("/health")
async def health_check():
# Comprueba que la base de datos responde
try:
db.execute(text("SELECT 1"))
except Exception:
return {"status": "unhealthy", "detail": "db unreachable"}, 503
return {"status": "ok"}
Si el endpoint devuelve cualquier código que no sea 2xx, curl -f falla y el check falla.
HEALTHCHECK en docker-compose.yml
Si no controlas el Dockerfile (imagen de terceros) o quieres sobrescribir el health check definido en la imagen, puedes hacerlo directamente en docker-compose.yml:
services:
servidor-web:
image: nginx:alpine
ports:
- "80:80"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/nginx_status"]
interval: 30s
timeout: 5s
start_period: 10s
retries: 3
base-de-datos:
image: postgres:16
environment:
POSTGRES_DB: mi_base_datos
POSTGRES_USER: usuario
POSTGRES_PASSWORD: TU_PASSWORD_AQUI
healthcheck:
test: ["CMD-SHELL", "pg_isready -U usuario -d mi_base_datos"]
interval: 10s
timeout: 5s
start_period: 30s
retries: 5
Nota la diferencia entre CMD y CMD-SHELL:
CMD: ejecuta el comando directamente, sin shell. Más seguro y predecible.CMD-SHELL: ejecuta el comando a través de/bin/sh -c. Necesario cuando usas tuberías, variables o lógica shell.
depends_on con condición de salud
Aquí está el verdadero poder de los health checks. Si tu aplicación necesita la base de datos para arrancar, puedes indicarle a Docker Compose que espere a que la base de datos esté healthy antes de iniciar la aplicación:
services:
base-de-datos:
image: postgres:16
environment:
POSTGRES_DB: mi_app_db
POSTGRES_USER: usuario
POSTGRES_PASSWORD: TU_PASSWORD_AQUI
healthcheck:
test: ["CMD-SHELL", "pg_isready -U usuario -d mi_app_db"]
interval: 10s
timeout: 5s
start_period: 30s
retries: 5
mi-aplicacion:
image: mi-app:latest
ports:
- "8080:8080"
depends_on:
base-de-datos:
condition: service_healthy
environment:
DATABASE_URL: postgresql://usuario:TU_PASSWORD_AQUI@base-de-datos:5432/mi_app_db
Con condition: service_healthy, Docker Compose no arrancará mi-aplicacion hasta que base-de-datos devuelva un health check exitoso. Sin esto, si la base de datos tarda 20 segundos en estar lista, la aplicación puede fallar al arrancar porque intenta conectarse antes de tiempo.
Las tres condiciones disponibles son:
service_started→ el contenedor simplemente está en marcha (comportamiento por defecto)service_healthy→ el health check pasaservice_completed_successfully→ el contenedor ha terminado con código de salida 0 (para tareas de inicialización)
Ver el estado de salud
Una vez que los contenedores tienen health check configurado, docker ps muestra el estado:
$ docker ps
CONTAINER ID IMAGE STATUS PORTS
a3f9c2b1d4e5 mi-app:1.0 Up 5 minutes (healthy) 0.0.0.0:8080->8080/tcp
b1c4d5e6f7a8 postgres:16 Up 5 minutes (healthy) 5432/tcp
c9d0e1f2a3b4 nginx:alpine Up 2 minutes (unhealthy) 0.0.0.0:80->80/tcp
Para ver el historial de los últimos checks de un contenedor:
docker inspect --format='{{json .State.Health}}' mi-aplicacion | python3 -m json.tool
Esto muestra los últimos 5 resultados con timestamp, código de salida y salida del comando. Muy útil para diagnosticar por qué un contenedor está marcado como unhealthy.
Reinicio automático con restart policies
Los health checks cobran su máximo valor combinados con la política de reinicio:
services:
mi-aplicacion:
image: mi-app:latest
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
start_period: 20s
retries: 3
Con restart: unless-stopped, Docker reiniciará el contenedor si el proceso muere. Pero no lo reinicia automáticamente si el health check falla. Para eso necesitas una herramienta adicional como Autoheal:
services:
autoheal:
image: willfarrell/autoheal:latest
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
AUTOHEAL_CONTAINER_LABEL: autoheal
AUTOHEAL_INTERVAL: 10
AUTOHEAL_START_PERIOD: 0
mi-aplicacion:
image: mi-app:latest
restart: unless-stopped
labels:
- "autoheal=true"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
start_period: 20s
retries: 3
Autoheal monitoriza los contenedores marcados con la etiqueta autoheal=true y los reinicia cuando pasan a estado unhealthy.
Health checks para servicios sin HTTP
No todo es HTTP. Algunos ejemplos habituales:
Redis:
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
MySQL/MariaDB:
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "--password=TU_PASSWORD_AQUI"]
interval: 10s
timeout: 5s
start_period: 30s
retries: 5
Verificación de puerto con netcat (cuando no hay cliente disponible en la imagen):
healthcheck:
test: ["CMD-SHELL", "nc -z localhost 8080 || exit 1"]
interval: 15s
timeout: 5s
retries: 3
Script personalizado (para lógica compleja):
healthcheck:
test: ["CMD", "/usr/local/bin/check-health.sh"]
interval: 30s
timeout: 15s
retries: 3
El script puede hacer múltiples comprobaciones: conectividad de red, disponibilidad de ficheros, espacio en disco, o cualquier condición que defina «sano» para ese servicio específico.
Deshabilitar el health check de una imagen
Si una imagen base define un health check que no quieres, puedes desactivarlo:
healthcheck:
disable: true
Útil cuando heredas una imagen con un health check que no aplica a tu caso de uso o que genera falsos positivos.
Qué he aprendido en producción
start_perioddemasiado corto: el error más común. Una base de datos con muchos datos puede tardar más de lo esperado en arrancar. Empieza con un valor generoso y ajusta observando los logs.- Health checks que consumen demasiado: un
curla un endpoint ligero cuesta muy poco. Un health check que lanza una query pesada cada 10 segundos puede añadir carga innecesaria. Mantén los checks simples y rápidos. - No confundir readiness con liveness: en Kubernetes existe la distinción entre readiness (¿listo para recibir tráfico?) y liveness (¿sigue vivo?). En Docker puro, el health check cubre ambos, así que diseña el endpoint
/healthpensando en ambas preguntas. - Logs del health check: cuando un contenedor está
unhealthyy no sabes por qué,docker inspectcon el filtro deHealthte da los últimos resultados con la salida exacta del comando. Es la primera herramienta que busco.
Los health checks son una de esas cosas que tardas diez minutos en configurar y te ahorran horas de diagnóstico. Si tienes servicios en producción sin ellos, este fin de semana es buen momento para añadirlos.