Health Checks y Reinicio Automático

Lectura
20 min~8 min lectura
CONCEPTO CLAVE: Los health checks permiten a Docker monitorear el estado de salud de un container y tomar acciones automáticas cuando algo falla. Combinados con políticas de reinicio, garantizan alta disponibilidad y resiliencia en entornos de producción.

¿Por qué necesitas Health Checks?

En un entorno de producción, los containers pueden fallar por múltiples razones: errores en la aplicación, agotamiento de memoria, conexiones perdidas a bases de datos, o simplemente procesos zombies. Sin mecanismos de supervisión, un container caído permanece caído hasta que alguien lo descubre manualmente.

Los health checks resuelven este problema permitiendo que Docker interrogue periódicamente al container sobre su estado real de salud. Si el container no responde correctamente, Docker puede reiniciarlo automáticamente según la política configurada.

📌 Nota importante: Un container que está "running" no significa necesariamente que esté funcionando correctamente. Un proceso puede estar activo pero colgado, sin aceptar conexiones, o en un estado de error no detectado.

Configurando Health Checks

Docker proporciona la instrucción HEALTHCHECK en el Dockerfile para definir cómo verificar que un container está saludable. Esta instrucción acepta varios parámetros:

HEALTHCHECK [--interval=30s] [--timeout=10s] [--retries=3] [--start-period=40s] CMD comando

Parámetros del HEALTHCHECK

Parámetro Descripción Valor por defecto
--interval Tiempo entre verificaciones de salud 30 segundos
--timeout Tiempo máximo para considerar la verificación exitosa 30 segundos
--retries Intentos fallidos consecutivos antes de marcar como unhealthy 3
--start-period Tiempo de gracia inicial durante el cual los fallos no cuentan 0 segundos

Ejemplo práctico: API REST con Node.js

FROM node:18-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .

EXPOSE 3000

# Health check: verifica que el endpoint /health responde correctamente
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
  CMD node -e "
    const http = require('http');
    http.get('http://localhost:3000/health', (res) => {
      process.exit(res.statusCode === 200 ? 0 : 1);
    }).on('error', () => process.exit(1));
  "

CMD ["node", "server.js"]

Ejemplo: Base de datos PostgreSQL

FROM postgres:15-alpine

ENV POSTGRES_DB=mydb
ENV POSTGRES_USER=myuser
ENV POSTGRES_PASSWORD=mypassword

# Verificar que PostgreSQL responde a conexiones
HEALTHCHECK --interval=10s --timeout=5s --retries=5 \
  CMD pg_isready -U myuser -d mydb || exit 1

EXPOSE 5432
⚠️ Advertencia: El comando HEALTHCHECK debe terminar con código de salida 0 para indicar éxito y cualquier otro código para indicar fallo. Un health check que nunca termina correctamente hará que el container sea marcado como unhealthy permanentemente.

Políticas de Reinicio Automático

Docker ofrece múltiples políticas de reinicio que determinan qué sucede cuando un container se detiene. Estas políticas se configuran con la bandera --restart al crear el container.

Políticas disponibles

Política Comportamiento Casos de uso
no No reinicia automáticamente (comportamiento por defecto) Containers de desarrollo, tareas únicas
on-failure[:max-retries] Reinicia solo si el container sale con código de error Procesos que pueden fallar recoverablemente
unless-stopped Reinicia siempre excepto si se detuvo manualmente Servicios en producción (recomendado)
always Reinicia siempre, incluso si se detuvo manualmente Servicios críticos, demonios

Ejemplos de uso

# Reiniciar siempre (para demonios y servicios críticos)
docker run -d --name mi-nginx --restart always -p 80:80 nginx

# Reiniciar solo en caso de fallo, máximo 5 intentos
docker run -d --name mi-app --restart on-failure:5 mi-imagen

# Reiniciar siempre excepto si se detuvo manualmente (PRODUCCIÓN)
docker run -d --name mi-api --restart unless-stopped -p 8080:8080 mi-api:latest
💡 Tip profesional: Para servicios en producción, usa unless-stopped en lugar de always. La diferencia es sutil pero importante: si detienes manualmente un container con always, se reiniciará al reiniciar Docker. Con unless-stopped, respetará tu decisión de mantenerlo detenido.

Combinando Health Checks y Reinicio Automático

La verdadera potencia surge cuando combinas ambas características. Cuando un health check falla consecutivamente el número de retries configurado, Docker marca el container como unhealthy. Con la política de reinicio apropiada, Docker puede automáticamente reiniciar el container para intentar recuperarlo.

📌 Flujo de operación: Docker ejecuta el health check → Si falla, incrementa el contador → Si supera el límite de fallos, marca como unhealthy → Si hay política de reinicio, reinicia el container → El ciclo se repite si es necesario.

Ejemplo completo: Aplicación web con base de datos

version: '3.8'

services:
  api:
    build: ./api
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    depends_on:
      db:
        condition: service_healthy
    ports:
      - "3000:3000"

  db:
    image: postgres:15-alpine
    restart: unless-stopped
    environment:
      POSTGRES_DB: appdb
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: secretpassword
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U appuser -d appdb"]
      interval: 10s
      timeout: 5s
      retries: 5
    
volumes:
  pgdata:

Verificando el estado de salud

# Ver estado de todos los containers incluyendo health
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Health}}"

# Ver detalles del health check de un container específico
docker inspect --format='{{json .State.Health}}' mi-container | jq

# Ver historial de health checks
docker inspect --format='{{range .State.Health.Log}} {{.End}} - Exit: {{.ExitCode}} - Output: {{.Output}} {{end}}' mi-container

Códigos de estado del Health

Un container puede tener uno de estos estados de salud:

Estado Descripción
starting El container está en su período de gracia inicial (start-period)
healthy Todos los health checks han pasado exitosamente
unhealthy El número de fallos consecutivos alcanzó el límite de retries

Casos de uso avanzados

Health check para aplicaciones con autenticación

# Verificar salud sin exponer endpoints de métricas públicos
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:8080/api/health \
    -O /dev/null || exit 1

Health check con verificación de dependencias

Ver más: Verificación de servicios dependientes
#!/bin/sh
set -e

# Verificar que el servicio responde
curl -f http://localhost:8080/health || exit 1

# Verificar conexión a base de datos
nc -z db 5432 || exit 1

echo "All dependencies healthy"
exit 0

Este script verifica tanto la aplicación como sus dependencias externas antes de reportar salud.

Reinicio con límite de intentos

# Detener después de 10 intentos fallidos para evitar loops infinitos
docker run -d \
  --name servicio-critico \
  --restart on-failure:10 \
  --health-cmd="pg_isready -U postgres" \
  --health-interval=30s \
  --health-retries=3 \
  mi-base-datos
⚠️ Importante: Sin límite en on-failure, Docker intentará reiniciar indefinidamente. En casos de bugs críticos que causan crashes constantes, esto puede generar un loop de reinicios que satura los recursos del sistema. Establece un límite máximo de intentos.

Debugging de problemas

  1. Verifica los logs del container: docker logs mi-container para identificar qué está causando los fallos.
  2. Revisa el output del health check: docker inspect --format='{{.State.Health}}' mi-container
  3. Ejecuta el comando manualmente: Entra al container y ejecuta el comando del health check para ver el error directamente.
  4. Verifica recursos: Asegúrate de que el container tiene suficiente CPU y memoria asignados.
  5. Revisa dependencias: Verifica que los servicios externos (bases de datos, APIs) están accesibles.
# Entrar al container y verificar manualmente
docker exec -it mi-container sh

# Dentro del container, ejecutar el health check
curl -f http://localhost:3000/health

# O verificar pg_isready si es PostgreSQL
pg_isready -U postgres

Integración con orquestación

En entornos con Docker Swarm o Kubernetes, los health checks se integran con los mecanismos de orquestación:

📌 Docker Swarm: Los containers marcados como unhealthy no reciben nuevo tráfico en servicios con modo replicated. Swarm puede automáticamente reiniciar containers en nodos que reportan failure.
# En Docker Swarm, crear servicio con health check
docker service create \
  --name mi-servicio \
  --health-cmd="curl -f http://localhost:3000/health || exit 1" \
  --health-interval=30s \
  --health-retries=3 \
  --health-timeout=10s \
  --replicas=3 \
  -p 3000:3000 \
  mi-imagen:latest
"Los sistemas distribuidos tolerantes a fallos asumen que los componentes fallarán eventualmente. Diseña tus servicios asumiendo que necesitarán reiniciarse y asegura que el reinicio los lleve a un estado consistente."
💡 Tip final: Nunca uses exit 0 hardcodeado en tus health checks. Asegúrate de que el comando realmente verifique la funcionalidad de tu aplicación. Un health check que siempre pasa pero no verifica nada es peor que no tener ninguno, porque crea una falsa sensación de seguridad.

Resumen de mejores prácticas

Área Recomendación
Intervalo No más de 30-60 segundos para servicios críticos. Verificaciones muy frecuentes consumen recursos.
Timeout Debe ser menor que el intervalo para permitir reintentos.
Retries 3-5 intentos son suficientes. Muy pocos pueden causar falsos positivos.
Start period Configúralo para aplicaciones que tardan en inicializar.
Reinicio Usa unless-stopped para producción, on-failure:N con límite para evitar loops.
🧠 Quiz: Health Checks y Reinicio Automático

¿Cuál es la diferencia principal entre las políticas de reinicio always y unless-stopped?

  • A) No hay diferencia, ambas reinician siempre
  • B) always reinicia incluso después de un stop manual, mientras unless-stopped respeta los stops manuales hasta el próximo reinicio de Docker
  • C) unless-stopped solo reinicia una vez
  • D) always es para desarrollo y unless-stopped para producción
✅ Respuesta correcta: B) La diferencia clave es que always ignora los stops manuales y reinicia el container al reiniciar el daemon de Docker, mientras unless-stopped respeta la decisión de detenerlo manualmente, manteniendo el container detenido incluso después de reiniciar Docker.
🧠 Quiz: Configuración de Health Checks

¿Qué sucede cuando un health check alcanza el número máximo de fallos configurado en --retries?

  • A) El container se elimina automáticamente
  • B) El container se marca como unhealthy y puede ser reiniciado según la política de reinicio configurada
  • C) Docker envía un email al administrador
  • D) Nada, los retries solo afectan los logs
✅ Respuesta correcta: B) Cuando se alcanzan los fallos consecutivos máximo, Docker marca el container como unhealthy. Lo que sucede después depende de la política de reinicio: si es always o unless-stopped, Docker reiniciará el container. Si es on-failure, también reiniciará porque el exit code será diferente de 0.
# Comando rápido para verificar todos los health checks de tus containers
docker ps --filter "health=unhealthy" --format "table {{.Names}}\t{{.Status}}"