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

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 reiniciaron-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:
- Tras
retriesfallos seguidos → contenedorunhealthy - Con
restart_policy, Docker intenta reiniciar - Tras el reinicio, el health check vuelve a contar
- 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:
- 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.
- Ejecuta el comando del health check a mano:
docker exec <container> curl -f http://localhost:8080/health
- 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:
- Motivo de salida:
docker inspect --format='{{.State.ExitCode}}' <container>
docker inspect --format='{{.State.Error}}' <container>
Códigos: 1 = error general, 137 = OOM, 139 = segfault.
- Contador de reinicios:
docker inspect --format='{{.RestartCount}}' <container>
- Logs recientes:
docker logs --tail 100 <container>
Disco lleno por logs
Síntoma: alerta de disco; /var/lib/docker/containers enorme.
Pasos:
- Archivos más grandes:
du -sh /var/lib/docker/containers/*/*-json.log | sort -rh | head -5
- ¿Rotación activa?
docker inspect --format='{{.HostConfig.LogConfig}}' <container>
Si muestra Config: {}, no hay rotación.
- 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.
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:
- Revisa tu docker-compose.yml actual; si faltan health check y rotación de logs, añádelos ya.
- Despliega la plantilla en un entorno de prueba y comprueba health checks y rotación.
- 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?
¿Es mejor restart: always u on-failure?
¿Qué tamaño y cuántos archivos de log son adecuados?
El health check falla siempre pero la app funciona bien, ¿qué hago?
¿Para qué sirve depends_on con condition: service_healthy?
¿Cómo ver rápido cuánto disco ocupan los logs de contenedores?
9 min de lectura · Publicado el: 12 abr 2026 · Actualizado el: 21 ago 2026
Guía práctica de Docker
Si llegaste desde búsqueda, lo más rápido es ir al artículo anterior o siguiente de esta misma serie.
Anterior
Guía de solución de errores en Docker Compose: 5 fallos frecuentes y cómo resolverlos rápido
¿Errores al ejecutar Docker Compose? Este artículo recopila 5 tipos habituales — conflictos de puertos, problemas de red, fallos de build, salida del contenedor y errores de permisos — con un flujo de diagnóstico sistemático para localizar el problema en 5 minutos.
Parte 10 de 38
Siguiente
Tres pilares del despliegue en producción con Docker Compose: health checks, reinicio y límites de recursos
Guía detallada de los tres pilares del despliegue en producción con Docker Compose: health checks para saber cuándo un servicio está listo, políticas de reinicio para autorrecuperación y límites de recursos para evitar desbordes. Incluye plantilla YAML completa para construir aplicaciones containerizadas estables.
Parte 12 de 38



Comentarios
Inicia sesión con GitHub para dejar un comentario