Cambiar tema

Despliegue en producción con Docker Compose: health checks, políticas de reinicio y gestión de logs

Easton editorial illustration: observability control panel

Las alertas del servidor llegaban una tras otra al móvil. Abrí la terminal: disco al 99% — los logs de contenedores ocupaban 50 GB.

Y eso no fue lo peor. En un proyecto del año pasado, el contenedor de la API aparecía como «running», pero la conexión a la base de datos llevaba muerta un rato y todas las peticiones devolvían 500. Tardamos tres horas en localizar el fallo. Según un informe de Last9, depurar un contenedor «zombi» suele costar de media 3,2 horas.

Muchos equipos despliegan Docker Compose en producción por primera vez con un mapeo de puertos y volúmenes, y poco más. Sin health checks, sin rotación de logs, restart: always a lo loco. Resultado: contenedores que parecen vivos pero no lo están; logs que crecen hasta reventar el disco; servicios en bucle de reinicio que se comen CPU y memoria.

Este artículo desglosa tres configuraciones clave en producción — health checks, políticas de reinicio y gestión de logs — con ejemplos, comandos por servicio, pasos de diagnóstico y una plantilla docker-compose.yml lista para copiar.

Health checks — que el contenedor esté realmente vivo

Que el estado sea running no significa que la aplicación funcione. Base de datos caída, puerto sin escuchar, proceso colgado — Docker no lo sabe. El health check es el «monitor de pulso» que comprueba periódicamente si la app responde bien.

Sintaxis de configuración

En docker-compose.yml, el bloque healthcheck se ve así:

healthcheck:
  test: ["CMD-SHELL", "pg_isready -U postgres"]
  interval: 10s      # comprobar cada 10 segundos
  timeout: 5s        # esperar como máximo 5 s por comprobación
  retries: 5         # marcar unhealthy tras 5 fallos seguidos
  start_period: 30s  # 30 s de calentamiento tras el arranque

Hay que combinar bien los parámetros. timeout no puede ser mayor que interval, o la siguiente comprobación empezará antes de que acabe la anterior. No omitas start_period: bases de datos y servicios lentos necesitan tiempo; si es corto, el health check marcará el contenedor como caído por error.

Comandos habituales por servicio

Cada servicio se comprueba distinto. Algunos ejemplos:

PostgreSQL

healthcheck:
  test: ["CMD-SHELL", "pg_isready -U postgres -d mydb"]
  interval: 10s
  timeout: 5s
  retries: 5
  start_period: 30s

pg_isready es la herramienta propia de PostgreSQL para saber si acepta conexiones.

MySQL / MariaDB

healthcheck:
  test: ["CMD-SHELL", "mysqladmin ping -h localhost -u root -p$$MYSQL_ROOT_PASSWORD"]
  interval: 10s
  timeout: 5s
  retries: 5
  start_period: 30s

La contraseña va con $$ para escapar; si no, YAML interpreta $ como variable.

Redis

healthcheck:
  test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
  interval: 10s
  timeout: 3s
  retries: 3

El ping de Redis devuelve PONG; grep confirma el resultado.

Servidor web (comprobación HTTP)

healthcheck:
  test: ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"]
  interval: 30s
  timeout: 10s
  retries: 3
  start_period: 10s

-f hace que curl devuelva código distinto de cero si el HTTP no es 2xx, marcando el fallo.

Trampa habitual: en imágenes Alpine puede no haber curl. Instálalo (apk add curl) o usa wget:

test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1"]

Control del orden de arranque

La base de datos aún no está lista y la API ya arranca: conexión fallida, errores, crash — lo he visto demasiadas veces. depends_on con condition: service_healthy lo evita:

services:
  postgres:
    image: postgres:16
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s

  api:
    build: ./api
    depends_on:
      postgres:
        condition: service_healthy  # esperar a que postgres esté healthy

Docker Compose arranca api solo cuando postgres devuelve healthy. Se acabó el «la base aún no está y la API ya conecta».

Políticas de reinicio — recuperación elegante tras un fallo

¿Qué pasa si el contenedor cae? Reiniciarlo automáticamente suena bien, pero si la causa raíz no se corrige, entras en bucle infinito: CPU y memoria al límite y el fallo real queda oculto.

Sintaxis de configuración

La política de reinicio va en el bloque deploy:

deploy:
  restart_policy:
    condition: on-failure   # solo reiniciar ante fallo
    delay: 5s               # esperar 5 s antes de reiniciar
    max_attempts: 3         # como máximo 3 intentos
    window: 120s            # 120 s sin fallo = recuperación real

condition tiene tres opciones:

  • none: no reiniciar
  • on-failure: solo si la salida es anómala (código ≠ 0)
  • any: reiniciar siempre

Recomendación en producción

Usa on-failure, no always.

¿Por qué? restart: always reinicia en cualquier caso: bug en la app, base caída, config mal escrita… Bucle de crash, logs a reventar, CPU consumida una y otra vez.

Con on-failure y max_attempts, tras unos intentos el contenedor se queda parado y ops puede investigar.

Ajuste de parámetros

delay es el intervalo entre reinicios. Muy corto y el contenedor no termina de limpiarse; muy largo y alargas la caída. 5-10 s suele ir bien.

window define cuánto tiempo debe pasar sin otro fallo para contar el reinicio como exitoso. Con window: 120s, si el contenedor vuelve a caer en ese margen, no se resetea el contador de max_attempts.

Health check y reinicio juntos

No son independientes:

  1. Tras retries fallos seguidos → contenedor unhealthy
  2. Con restart_policy, Docker intenta reiniciar
  3. Tras el reinicio, el health check vuelve a contar
  4. Si pasa, vuelve a la normalidad; si no, reintenta hasta agotar max_attempts

Así tienes recuperación automática con límite al bucle infinito.

Gestión de logs — evitar llenar el disco

La alerta de las tres de la mañana — disco al 99%, logs en 50 GB — me ha pasado más de una vez. El driver json-file por defecto no limpia logs viejos; crecen sin freno. Sin rotación, el disco revienta tarde o temprano.

Configuración de rotación

Añade logging en docker-compose.yml:

logging:
  driver: "json-file"
  options:
    max-size: "10m"      # máximo 10 MB por archivo
    max-file: "3"        # conservar 3 archivos
    compress: "true"     # comprimir los antiguos

Con esto, como mucho 30 MB (10 MB × 3). Al superar 10 MB se crea otro archivo; al pasar de 3, el más viejo se borra o comprime.

Los logs están en /var/lib/docker/containers/<container-id>/<container-id>-json.log. Para ver el uso:

du -sh /var/lib/docker/containers/*/*-json.log

Elección de driver

Docker admite json-file, syslog, fluentd, journald, local, etc. Para la mayoría, json-file o local bastan.

La documentación oficial indica que local es más eficiente que json-file e incluye rotación sin configurar max-size/max-file. Si generas decenas de GB al día, valora local:

logging:
  driver: "local"

Inconveniente: no puedes usar docker logs directamente; en la config conviene mode: "non-blocking" para compatibilidad.

Recolección centralizada (opcional)

En un solo servidor, json-file o local alcanzan. Con decenas de máquinas y cientos de contenedores, conviene centralizar:

  • Fluentd: ligero, clústeres pequeños
  • ELK Stack: potente, costoso de desplegar
  • Loki + Grafana: cloud native, encaja con Prometheus

Configurar estos sistemas es complejo; aquí solo la idea de Fluentd:

logging:
  driver: "fluentd"
  options:
    fluentd-address: "localhost:24224"
    tag: "docker.{{.Name}}"

Fluentd reenvía logs a la dirección indicada para analizarlos en otro servidor.

Plantilla de configuración completa

Combinando health check, reinicio y logs, obtienes un docker-compose.yml de producción. Ejemplo con PostgreSQL, Redis y API:

version: '3.8'

services:
  postgres:
    image: postgres:16
    environment:
      POSTGRES_USER: myuser
      POSTGRES_PASSWORD: mypassword
      POSTGRES_DB: mydb
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U myuser -d mydb"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
    deploy:
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 3
        window: 120s
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"
        compress: "true"

  redis:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
      interval: 10s
      timeout: 3s
      retries: 3
      start_period: 5s
    deploy:
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 3
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

  api:
    build:
      context: ./api
      dockerfile: Dockerfile
    ports:
      - "8080:8080"
    environment:
      DATABASE_URL: postgres://myuser:mypassword@postgres:5432/mydb
      REDIS_URL: redis://redis:6379
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 10s
    deploy:
      restart_policy:
        condition: on-failure
        delay: 10s
        max_attempts: 3
        window: 120s
    logging:
      driver: "json-file"
      options:
        max-size: "50m"
        max-file: "5"
        compress: "true"

volumes:
  postgres_data:

Puntos clave

Orden de arranque: api espera a postgres y redis healthy antes de arrancar.

Tamaño de logs: postgres y redis suelen bastar con 10m × 3; la API puede necesitar 50m × 5. Ajusta según volumen real.

Delay de reinicio: postgres arranca lento — delay 5 s; la API, delay 10 s para dar margen al health check.

start_period: postgres 30 s; redis 5 s; API 10 s.

Copia la plantilla, cambia variables e imágenes. Para MongoDB, MinIO, etc., repite el mismo patrón.

Trampas habituales y diagnóstico

El health check falla siempre

Síntoma: el contenedor sigue unhealthy pero la app parece OK.

Pasos:

  1. Comprueba si existen las herramientas:
   docker exec <container> which curl
   docker exec <container> which pg_isready
   

Alpine suele no traer curl; instálalo o usa wget.

  1. Ejecuta el comando del health check a mano:
   docker exec <container> curl -f http://localhost:8080/health
   
  1. Revisa el historial:
   docker inspect --format='{{json .State.Health}}' <container> | jq
   

Reinicios en bucle

Síntoma: arranca, cae a los pocos segundos, logs llenos de reinicios.

Pasos:

  1. Motivo de salida:
   docker inspect --format='{{.State.ExitCode}}' <container>
   docker inspect --format='{{.State.Error}}' <container>
   

Códigos: 1 = error general, 137 = OOM, 139 = segfault.

  1. Contador de reinicios:
   docker inspect --format='{{.RestartCount}}' <container>
   
  1. Logs recientes:
   docker logs --tail 100 <container>
   

Disco lleno por logs

Síntoma: alerta de disco; /var/lib/docker/containers enorme.

Pasos:

  1. Archivos más grandes:
   du -sh /var/lib/docker/containers/*/*-json.log | sort -rh | head -5
   
  1. ¿Rotación activa?
   docker inspect --format='{{.HostConfig.LogConfig}}' <container>
   

Si muestra Config: {}, no hay rotación.

  1. Limpieza temporal:
   truncate -s 0 /var/lib/docker/containers/<id>/<id>-json.log
   

A largo plazo, configura rotación.

Comandos rápidos

# estado de salud de todos los contenedores
docker ps --format "table {{.Names}}\t{{.Status}}"

# historial de health check
docker inspect --format='{{json .State.Health}}' <container>

# código de salida y reinicios
docker inspect --format='ExitCode: {{.State.ExitCode}}, RestartCount: {{.RestartCount}}' <container>

# tamaño de logs
du -sh /var/lib/docker/containers/*/*-json.log | sort -rh

# últimas 100 líneas de log
docker logs --tail 100 <container>

Resumen

En producción con Docker Compose, estas tres configuraciones no son opcionales: health checks para que el contenedor no solo «parezca» vivo; política de reinicio para recuperación acotada; gestión de logs para no llenar el disco.

3,2 horas
Tiempo medio de diagnóstico de contenedor zombi

Lista de configuración clave:

  • Health check: test + interval + timeout + retries + start_period
  • Reinicio: condition: on-failure + max_attempts: 3
  • Rotación de logs: max-size: 10m + max-file: 3 + compress: true

Plan en tres pasos:

  1. Revisa tu docker-compose.yml actual; si faltan health check y rotación de logs, añádelos ya.
  2. Despliega la plantilla en un entorno de prueba y comprueba health checks y rotación.
  3. Guarda los comandos de diagnóstico; la próxima alerta a las tres de la mañana podrás actuar rápido.

No dejes contenedores «desnudos» en producción. Con estas tres capas de protección, cuando algo falle podrás recuperar, diagnosticar y evitar que el disco reviente.

FAQ

¿Cómo configurar interval y timeout del health check de forma razonable?
interval: 10-30 segundos; timeout: 3-10 segundos. Lo clave es que timeout no supere interval, o la siguiente comprobación empezará antes de que termine la anterior. En bases de datos, usa start_period más largo (30-60 s) para dar tiempo a la inicialización.
¿Es mejor restart: always u on-failure?
En producción conviene on-failure con max_attempts. always reinicia en cualquier situación — errores de config, bugs, etc. — y provoca bucles de crash. on-failure solo reinicia ante salidas anómalas; con max_attempts el equipo de ops puede detectar el problema.
¿Qué tamaño y cuántos archivos de log son adecuados?
Para la mayoría de servicios: max-size: 10m + max-file: 3 (30 MB total). Servicios con mucho log (API) pueden usar 50m × 5. Ajusta según el volumen real y activa compress: true para ahorrar espacio.
El health check falla siempre pero la app funciona bien, ¿qué hago?
Confirma que existen las herramientas del comando (curl, pg_isready, etc.); las imágenes Alpine suelen no traerlas. Ejecuta el comando a mano y revisa el historial con docker inspect para ver el motivo del fallo.
¿Para qué sirve depends_on con condition: service_healthy?
Arranca el contenedor actual solo cuando el servicio dependiente pasa el health check. Más fiable que depends_on simple: evita que la API intente conectar antes de que la base de datos esté lista. La dependencia debe tener healthcheck configurado.
¿Cómo ver rápido cuánto disco ocupan los logs de contenedores?
Ejecuta: du -sh /var/lib/docker/containers/*/*-json.log | sort -rh. Si un contenedor destaca, revisa si la rotación de logs está activa.

9 min de lectura · Publicado el: 12 abr 2026 · Actualizado el: 21 ago 2026

Comentarios

Inicia sesión con GitHub para dejar un comentario

Easton BlogEaston Blog