Tres pilares del despliegue en producción con Docker Compose: health checks, reinicio y límites de recursos

Un SMS de alerta del servidor te saca de la cama.
Abres el portátil: el contenedor aparece como running, con un puntito verde que parece saludable. ¿Pero acceder al servicio? 502 Bad Gateway.
El contenedor de la base de datos aún no había terminado de arrancar y el de la aplicación ya intentaba conectarse. Fallo de conexión, servicio caído. El contenedor sigue en «running», pero el servicio lleva rato muerto.
Así fue mi primera experiencia desplegando Docker Compose en producción.
Que arranque y que sea estable son cosas distintas. docker-compose up levanta todo de un golpe, pero eso no garantiza que aguante una explosión de memoria a las tres de la mañana o un crash de proceso.
Los tres pilares del despliegue en producción con Docker Compose — health checks, políticas de reinicio y límites de recursos — existen para cubrir ese hueco. En este artículo comparto la configuración que saqué de esos tropiezos, con plantillas YAML listas para copiar, para pasar de «funciona» a «funciona de forma fiable».
1. Health check (healthcheck): saber si el contenedor está realmente disponible
El estado running de docker ps solo indica que el proceso del contenedor sigue vivo. ¿Puede atender peticiones con normalidad? No lo sabes.
El health check hace que Docker «revise» el contenedor de forma periódica: una petición HTTP, una conexión a la base de datos o un script, para comprobar si el servicio responde de verdad.
¿Cómo funciona?
Docker ejecuta el comando de comprobación dentro del contenedor según el intervalo que definas. Código de salida 0 = saludable; distinto de 0 = no saludable. Tras varios fallos seguidos, el contenedor pasa a unhealthy.
Lo importante: un health check fallido no reinicia el contenedor automáticamente. Solo expone el estado. Para recuperación automática hay que combinarlo con depends_on condicionado y la política de reinicio.
Cuatro parámetros que debes dominar
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"]
interval: 30s # Cada cuánto comprobar
timeout: 10s # Tiempo máximo por comprobación
retries: 3 # Fallos seguidos antes de marcar unhealthy
start_period: 60s # Periodo de gracia al arrancar; los fallos no cuentan para retries
Yo ignoré start_period al principio: la app tardaba en arrancar, la conexión a la base de datos unos 40 segundos, pero el health check empezaba a los 10. Tres fallos seguidos y unhealthy. Con start_period: 60s la app tiene margen para inicializarse.
Comandos de health check habituales
Servicio web:
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
PostgreSQL:
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
Redis:
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
Arranque condicionado con depends_on
Aquí es donde el health check brilla de verdad: esperar a que las dependencias estén listas antes de arrancar.
services:
app:
depends_on:
db:
condition: service_healthy # Esperar a que pase el health check de la base de datos
redis:
condition: service_healthy # Esperar a que pase el health check de Redis
Antes usaba depends_on: [db, redis]: la app arrancaba mientras la base de datos aún se inicializaba. Sin conexión, error y salida. Con condition: service_healthy la app espera hasta que la base responda a pg_isready. Problema resuelto.
2. Política de reinicio (restart): autorrecuperación del contenedor
¿Quién levanta el contenedor cuando cae?
¿docker restart manual? A las tres de la mañana, con la alarma sonando, no apetece.
La política de reinicio delega en el daemon de Docker: tras salir el contenedor, decide si hay que reiniciarlo.
Comparación de las cuatro políticas
| Política | Comportamiento | Escenario |
|---|---|---|
no | Si cae, cae; no reinicia | Pruebas puntuales, CI/CD |
always | Reinicia siempre, cualquier salida | Servicios críticos |
on-failure | Solo reinicia si la salida es anómala | Contenedores tipo tarea |
unless-stopped | Reinicia siempre salvo stop manual | Opción preferida en producción |
Producción: unless-stopped
restart: unless-stopped
¿Por qué unless-stopped y no always?
La diferencia está en el comportamiento tras un docker stop manual:
always: tras un stop manual, si reinicias el sistema o el servicio Docker, el contenedor vuelve a arrancarunless-stopped: tras un stop manual, se queda parado; no «revive» solo
Imagina que paras un contenedor para mantenimiento y, tras un reinicio del servidor, vuelve a levantarse solo. No es lo que quieres.
Límite de reintentos con on-failure
on-failure admite un tope de reintentos:
restart: on-failure:5 # Como máximo 5 reinicios
Si el contenedor falla al arrancar cinco veces seguidas, Docker se rinde. Sirve cuando el crash se repite por causas externas (base de datos inaccesible, configuración errónea) y quieres evitar un bucle infinito de reinicios.
¿Cómo elegir? En resumen
- Servicios core (Web, API, base de datos):
unless-stopped - Tareas en segundo plano, scripts programados:
on-failure - Desarrollo, ejecución temporal:
no
Trampa habitual: la política de reinicio solo responde a «¿reinicio cuando el contenedor sale?». No responde a «¿el servicio está realmente sano?». Para eso siguen haciendo falta los health checks.
3. Límites de recursos (deploy.resources): evitar que un contenedor se descontrole
¿Te ha pasado? Un contenedor con fuga de memoria se come toda la RAM del servidor y el OOM Killer acaba con el resto.
A mí sí. No tiene gracia.
Los límites de recursos ponen un «techo» a cada contenedor: si lo supera, se lo cargan y proteges al resto.
limits vs reservations
deploy:
resources:
limits:
cpus: '1.0' # Máximo 1 CPU
memory: 512M # Máximo 512 MB de RAM
reservations:
cpus: '0.5' # Reservar al menos 0,5 CPU
memory: 256M # Reservar al menos 256 MB de RAM
- limits: límite duro; si se supera, el proceso muere (OOM)
- reservations: límite blando; le dice al scheduler «este contenedor necesita al menos estos recursos»
En pocas palabras: limits es «no puedes pasar de aquí»; reservations es «garantiza al menos esto».
¿Cómo fijar el límite de CPU?
cpus: '1.0' # Hasta 1 núcleo completo
cpus: '0.5' # Hasta el 50 % de un núcleo
cpus: '2.0' # Hasta 2 núcleos
El límite de CPU es blando: si lo superas, te limitan la velocidad; no te matan el proceso. Mejor pecar de generoso.
¿Cómo fijar el límite de memoria?
memory: 512M # 512 MB
memory: 2G # 2 GB
El límite de memoria es duro. Si lo superas, el OOM Killer mata el contenedor. Sin negociación.
Valores que suelo usar:
- Apps Node.js: al menos
512M; en producción recomiendo1G - Apps Python:
256M - 512M - PostgreSQL:
1G - 4Gsegún conexiones y datos - Redis:
256M - 512M; como caché puede hacer falta más
Configuración de ejemplo
services:
app:
image: myapp:latest
deploy:
resources:
limits:
cpus: '1.0'
memory: 1G
reservations:
cpus: '0.25'
memory: 256M
Con esto la app usa como máximo 1 CPU y 1 GB de RAM, y Docker reserva al menos 0,25 CPU y 256 MB.
Nota: deploy está pensado sobre todo para Docker Swarm. En un solo host con docker-compose up, los límites funcionan con Docker Compose V2 o con docker-compose --compatibility. Alternativa más universal en host único: mem_limit y cpus (obsoletos) o deploy (Compose V2.20+).
4. Plantilla completa: YAML de producción listo para copiar
Cada pilar ayuda por separado; juntos forman una configuración de producción. Abajo, un ejemplo completo con servicio web + PostgreSQL + Redis que puedes copiar y adaptar.
Ejemplo completo
version: '3.8'
services:
# Aplicación web
app:
image: myapp:latest
restart: unless-stopped
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
deploy:
resources:
limits:
cpus: '1.0'
memory: 1G
reservations:
cpus: '0.25'
memory: 256M
logging:
driver: json-file
options:
max-size: "10m" # Máximo 10 MB por archivo de log
max-file: "3" # Conservar como máximo 3 archivos
# Base de datos PostgreSQL
db:
image: postgres:15
restart: unless-stopped
environment:
POSTGRES_USER: appuser
POSTGRES_PASSWORD: apppassword
POSTGRES_DB: appdb
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
deploy:
resources:
limits:
cpus: '2.0'
memory: 2G
reservations:
cpus: '0.5'
memory: 512M
volumes:
- pgdata:/var/lib/postgresql/data
# Caché Redis
redis:
image: redis:7-alpine
restart: unless-stopped
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M
reservations:
memory: 128M
volumes:
pgdata:
Separar configuraciones
Desarrollo y producción suelen diferir. Dos archivos lo resuelven:
# compose.yaml - Entorno de desarrollo
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
restart: "no" # En desarrollo no quiero reinicio automático
# compose.production.yaml - Overrides de producción
version: '3.8'
services:
app:
image: myapp:v1.2.3 # En producción, imagen ya construida
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"]
interval: 30s
timeout: 10s
retries: 3
deploy:
resources:
limits:
memory: 1G
Arrancar producción:
docker-compose -f compose.yaml -f compose.production.yaml up -d
Los archivos se fusionan; compose.production.yaml sobrescribe a compose.yaml.
Gestión de logs: evitar llenar el disco
El driver por defecto json-file crece sin límite. Sin tope, a los meses el disco se llena de logs.
logging:
driver: json-file
options:
max-size: "10m" # Máximo 10 MB por archivo
max-file: "3" # Máximo 3 archivos, 30 MB en total
Como máximo 30 MB de logs por contenedor, con rotación automática. Lo añado a cada servicio para no tener que limpiar a mano más adelante.
Resumen
En resumen, la lógica de los tres pilares es:
El health check detecta el problema → la política de reinicio recupera → los límites evitan que el fallo se propague
Con esta combinación, tu aplicación en Docker Compose puede:
- Esperar a que las dependencias estén listas al arrancar, en lugar de lanzarse a ciegas
- Levantarse sola tras un crash, sin que tengas que levantarte a las tres de la mañana
- Contener un contenedor descontrolado para que no tumbe todo el servidor
Revisa tu docker-compose.yml. ¿Qué te falta? Complétalo.
Si aún no despliegas en producción con Docker Compose, la próxima vez lleva estas tres configuraciones. Te lo agradecerás.
Configurar los tres pilares del despliegue en producción con Docker Compose
Añade health checks, políticas de reinicio y límites de recursos a Docker Compose para un despliegue estable de nivel producción
⏱️ Estimated time: 15 min
- 1
Step 1: Añadir configuración de health check
Añade healthcheck a cada servicio:
• test: comando de comprobación (curl, pg_isready, redis-cli ping, etc.)
• interval: intervalo entre comprobaciones (recomendado 10-30s)
• timeout: tiempo de espera (recomendado 5-10s)
• retries: intentos fallidos (recomendado 3-5)
• start_period: periodo de gracia al arrancar (30-60s según el tiempo de inicio de la app) - 2
Step 2: Configurar arranque condicionado por dependencias
Usa el parámetro condition de depends_on:
• Cambia depends_on: [db] por depends_on: db: condition: service_healthy
• Asegúrate de que el servicio dependiente tenga health check configurado
• El servicio de la app esperará a que la dependencia esté realmente disponible antes de arrancar - 3
Step 3: Definir la política de reinicio
Elige la política según el tipo de servicio:
• Servicios core (Web/API/base de datos): restart: unless-stopped
• Tareas en segundo plano/scripts programados: restart: on-failure:5
• Desarrollo y depuración: restart: "no"
• Evita always (reinicia tras un stop manual si el servidor o Docker se reinician) - 4
Step 4: Configurar límites de recursos
Define limits y reservations en deploy.resources:
• limits: límite duro; si se supera, el OOM Killer actúa
• reservations: límite blando; recursos mínimos que Docker garantiza
• Apps Node.js: limits.memory al menos 512M
• Base de datos: 1G-4G según conexiones y volumen de datos - 5
Step 5: Añadir rotación de logs
Evita que los logs llenen el disco:
• logging.driver: json-file (driver por defecto)
• logging.options.max-size: "10m" (máximo 10 MB por archivo)
• logging.options.max-file: "3" (conservar 3 archivos)
• Tope total de 30 MB con rotación automática
FAQ
¿Se reinicia automáticamente el contenedor si falla el health check?
¿Cuál es la diferencia entre unless-stopped y always?
¿Qué diferencia hay entre limits y reservations en los límites de recursos?
¿Para qué sirve el parámetro start_period?
¿Cómo usar configuraciones distintas en desarrollo y producción?
• compose.yaml: configuración de desarrollo (build: ., restart: "no")
• compose.production.yaml: overrides de producción (image: xxx, restart: unless-stopped)
• Comando de arranque: docker-compose -f compose.yaml -f compose.production.yaml up -d
• El segundo archivo sobrescribe al primero
¿Se puede usar deploy.resources en un despliegue en un solo host?
8 min de lectura · Publicado el: 24 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
Despliegue en producción con Docker Compose: health checks, políticas de reinicio y gestión de logs
Guía práctica de Docker Compose en producción: configuración de health checks, políticas de reinicio y gestión completa de logs. De contenedores zombi a recuperación automática, con configuraciones útiles para evitar que el disco se llene.
Parte 11 de 38
Siguiente
Despliegue PHP con Docker Compose: tutorial completo DNMP (Nginx+MySQL+PHP)
Guía paso a paso para desplegar DNMP (Docker+Nginx+MySQL+PHP) con Docker Compose en un solo comando. Listo en 10 minutos, elimina la inconsistencia de entornos en el equipo, con archivos de configuración completos y guía de errores típicos
Parte 13 de 38



Comentarios
Inicia sesión con GitHub para dejar un comentario