Ejecutar aplicaciones multi-contenedor en producción requiere mucho más que un archivo docker-compose.yml básico. Aunque Docker Compose se asocia comúnmente con el desarrollo local, es una herramienta poderosa para desplegar cargas de trabajo en producción en entornos de un solo host y clústeres pequeños cuando se configura correctamente. Esta guía te lleva paso a paso a endurecer tu configuración de Docker Compose para producción con health checks, políticas de reinicio, límites de recursos, gestión de secretos, logging centralizado y proxy inverso Nginx con terminación SSL.
Requisitos Previos
- Servidor Linux con Docker Engine 24+ y Docker Compose v2 instalados
- Nombre de dominio apuntando a la IP pública de tu servidor
- Familiaridad básica con conceptos de Docker (imágenes, contenedores, volúmenes, redes)
- Certificado SSL o disposición para usar Let’s Encrypt con Certbot
- Acceso SSH a tu servidor de producción
- Al menos 2 GB de RAM y 2 núcleos de CPU disponibles para tu stack
Configuración de Docker Compose para Producción
Un docker-compose.yml listo para producción difiere significativamente de una configuración de desarrollo. Necesitas políticas de reinicio explícitas, health checks, restricciones de recursos y sistemas de archivos de solo lectura donde sea posible.
Políticas de Reinicio
Las políticas de reinicio aseguran que tus contenedores se recuperen automáticamente de crashes, OOM kills o reinicios del host:
services:
web:
image: myapp:latest
restart: unless-stopped
deploy:
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
window: 120s
Usa unless-stopped para la mayoría de los servicios. Esto reinicia contenedores después de crashes y reinicios del host, pero respeta las paradas manuales. Para servicios críticos que deben ejecutarse siempre, usa always. Evita no en producción — deja los contenedores caídos muertos hasta la intervención manual.
Health Checks
Los health checks permiten a Docker monitorear si tu aplicación realmente está funcionando, no solo si el proceso está ejecutándose:
services:
api:
image: myapp-api:latest
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
El start_period da a tu aplicación tiempo para inicializarse antes de que Docker comience a contar las verificaciones fallidas. Configúralo más alto que el tiempo promedio de arranque de tu aplicación. Usa endpoints de salud específicos en lugar de verificar si un puerto está abierto — un proceso puede escuchar en un puerto mientras está en un estado roto.
Límites de Recursos
Sin límites de recursos, un solo contenedor descontrolado puede consumir toda la memoria del sistema y hacer caer todo:
services:
api:
image: myapp-api:latest
deploy:
resources:
limits:
cpus: "1.0"
memory: 512M
reservations:
cpus: "0.25"
memory: 128M
Configura limits al máximo que un servicio debería usar y reservations para garantizar recursos mínimos. Monitorea tus contenedores con docker stats durante una semana antes de establecer los límites finales. Límites demasiado ajustados causan OOM kills y rendimiento degradado.
Sistemas de Archivos de Solo Lectura
Minimiza la superficie de ataque ejecutando contenedores con sistemas de archivos raíz de solo lectura:
services:
api:
image: myapp-api:latest
read_only: true
tmpfs:
- /tmp
- /var/run
volumes:
- app-data:/app/data
Esto previene que procesos maliciosos escriban en el sistema de archivos del contenedor. Usa tmpfs para directorios que necesitan acceso temporal de escritura y volúmenes nombrados para datos persistentes.
Gestión de Secretos y Variables de Entorno
Las credenciales codificadas directamente en tu archivo compose son un riesgo de seguridad. Docker Compose soporta múltiples enfoques para la gestión de secretos.
Usando Archivos .env
Crea un archivo .env junto a tu archivo compose:
# .env - NUNCA hagas commit de este archivo
POSTGRES_PASSWORD=tu_contraseña_segura_aqui
API_SECRET_KEY=otro_valor_seguro
REDIS_PASSWORD=contraseña_redis_aqui
Referéncialos en tu archivo compose:
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
Agrega .env a tu .gitignore inmediatamente. Para pipelines CI/CD, inyecta variables de entorno desde tu gestor de secretos (GitHub Secrets, AWS Secrets Manager o HashiCorp Vault).
Docker Secrets
Para un manejo más seguro, usa Docker secrets que se montan como archivos en lugar de variables de entorno:
secrets:
db_password:
file: ./secrets/db_password.txt
services:
db:
image: postgres:16
secrets:
- db_password
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
Muchas imágenes oficiales de Docker soportan la convención del sufijo _FILE, leyendo el secreto desde un archivo montado en lugar de una variable de entorno. Esto mantiene las credenciales fuera de la salida de docker inspect y los listados de variables de entorno del proceso.
Proxy Inverso y SSL con Nginx
Exponer puertos de aplicación directamente es inseguro e inflexible. Usa Nginx como proxy inverso con terminación SSL:
services:
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- certbot-webroot:/var/www/certbot:ro
- certbot-certs:/etc/letsencrypt:ro
depends_on:
api:
condition: service_healthy
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/health"]
interval: 30s
timeout: 5s
retries: 3
certbot:
image: certbot/certbot
volumes:
- certbot-webroot:/var/www/certbot
- certbot-certs:/etc/letsencrypt
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done'"
volumes:
certbot-webroot:
certbot-certs:
La configuración de Nginx debe hacer proxy hacia tus servicios internos:
upstream api_backend {
server api:8080;
}
server {
listen 80;
server_name example.com;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
location / {
proxy_pass http://api_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Observa que solo el servicio Nginx expone puertos al host. Todos los demás servicios se comunican a través de la red interna de Docker, reduciendo significativamente la superficie de ataque.
Logging y Monitoreo
Los despliegues en producción necesitan logging estructurado con rotación para prevenir el agotamiento del disco.
Logging JSON File con Rotación
services:
api:
image: myapp-api:latest
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"
tag: "{{.Name}}"
Sin max-size y max-file, los logs de Docker crecen indefinidamente y pueden llenar tu disco. El driver json-file es el predeterminado y funciona bien para la mayoría de las configuraciones. Para entornos multi-host, considera reenviar logs a un sistema centralizado.
Reenvío a Sistemas Externos
Para observabilidad en producción, reenvía logs a un servicio de agregación:
services:
api:
image: myapp-api:latest
logging:
driver: syslog
options:
syslog-address: "tcp://logserver:514"
tag: "myapp-api"
Las alternativas incluyen los drivers fluentd y gelf para stacks ELK o Graylog. Sea cual sea tu elección, siempre configura la rotación de logs a nivel del daemon de Docker como red de seguridad en /etc/docker/daemon.json:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
Monitoreo Básico
Agrega monitoreo de contenedores con un stack ligero:
services:
prometheus:
image: prom/prometheus:latest
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus-data:/prometheus
deploy:
resources:
limits:
memory: 256M
cadvisor:
image: gcr.io/cadvisor/cadvisor:latest
volumes:
- /:/rootfs:ro
- /var/run:/var/run:ro
- /sys:/sys:ro
- /var/lib/docker/:/var/lib/docker:ro
deploy:
resources:
limits:
memory: 128M
Comparativa: Docker Compose vs Kubernetes vs Docker Swarm
| Característica | Docker Compose | Kubernetes | Docker Swarm |
|---|---|---|---|
| Complejidad de configuración | Baja | Alta | Media |
| Soporte multi-host | No (un solo host) | Sí (clúster) | Sí (clúster) |
| Auto-escalado | No | Sí (HPA) | Limitado |
| Descubrimiento de servicios | Basado en DNS | DNS + Ingress | Basado en DNS |
| Gestión de secretos | Basada en archivos | Integrada (etcd) | Integrada |
| Actualizaciones progresivas | Manual | Integrada | Integrada |
| Health checks | Sí | Sí (liveness/readiness) | Sí |
| Límites de recursos | Sí | Sí (requests/limits) | Sí |
| Curva de aprendizaje | Suave | Pronunciada | Moderada |
| Ideal para | Un solo host, equipos pequeños | Gran escala, multi-equipo | Clústeres pequeños |
Docker Compose es la elección correcta cuando despliegas en un solo servidor o un número pequeño de servidores, tu equipo es pequeño y no necesitas auto-escalado. Muchos productos SaaS exitosos se ejecutan completamente sobre Docker Compose en producción.
Escenario del Mundo Real
Estás desplegando una aplicación web compuesta por una API Node.js, una base de datos PostgreSQL, una caché Redis y un proxy inverso Nginx. El servidor tiene 4 GB de RAM y 2 núcleos de CPU.
Aquí está el archivo compose completo de producción:
services:
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- certbot-certs:/etc/letsencrypt:ro
depends_on:
api:
condition: service_healthy
restart: unless-stopped
deploy:
resources:
limits:
cpus: "0.25"
memory: 64M
logging:
driver: json-file
options:
max-size: "5m"
max-file: "3"
api:
image: myapp-api:1.2.3
env_file: .env
secrets:
- db_password
- api_key
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
restart: unless-stopped
read_only: true
tmpfs:
- /tmp
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
deploy:
resources:
limits:
cpus: "1.0"
memory: 512M
reservations:
cpus: "0.25"
memory: 128M
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"
db:
image: postgres:16-alpine
volumes:
- pgdata:/var/lib/postgresql/data
secrets:
- db_password
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
POSTGRES_DB: myapp
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
deploy:
resources:
limits:
cpus: "0.5"
memory: 1G
reservations:
memory: 256M
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
redis:
image: redis:7-alpine
command: redis-server --requirepass ${REDIS_PASSWORD}
volumes:
- redisdata:/data
restart: unless-stopped
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
interval: 10s
timeout: 5s
retries: 3
deploy:
resources:
limits:
cpus: "0.25"
memory: 256M
logging:
driver: json-file
options:
max-size: "5m"
max-file: "3"
secrets:
db_password:
file: ./secrets/db_password.txt
api_key:
file: ./secrets/api_key.txt
volumes:
pgdata:
redisdata:
certbot-certs:
Despliega con:
docker compose -f docker-compose.prod.yml up -d
docker compose -f docker-compose.prod.yml ps
docker compose -f docker-compose.prod.yml logs --tail=50
Esta configuración asigna aproximadamente 1.8 GB de los 4 GB disponibles, dejando margen para el sistema operativo y picos de uso.
Errores Comunes y Casos Especiales
El orden de arranque de contenedores no garantiza disponibilidad. Usar depends_on solo espera a que el contenedor se inicie, no a que el servicio interno esté listo. Siempre combina depends_on con condition: service_healthy y health checks adecuados.
Los contenedores huérfanos se acumulan. Cuando eliminas un servicio de tu archivo compose, el contenedor antiguo sigue ejecutándose. Siempre ejecuta docker compose up -d --remove-orphans para limpiar contenedores obsoletos.
Los permisos de volúmenes causan fallos silenciosos. Si tu contenedor se ejecuta como usuario no-root, el volumen montado puede no ser escribible. Pre-crea volúmenes con la propiedad correcta o usa un patrón de contenedor init.
Docker Compose no descarga nuevas imágenes por defecto al reconstruir. Ejecutar docker compose up -d reutiliza imágenes en caché. Usa docker compose pull && docker compose up -d para asegurar que despliegas la última versión.
La expansión de variables del archivo .env puede generar conflictos. Docker Compose interpola ${VAR} en el archivo compose antes de pasarlo al contenedor. Si la configuración de tu aplicación usa caracteres $, escápalos con $$ en el archivo compose.
La rotación de logs debe configurarse por servicio y globalmente. Las opciones de log a nivel de servicio no anulan los valores predeterminados del daemon para otros contenedores. Configura ambos para cobertura completa.
Las redes bridge tienen problemas de resolución DNS con guiones bajos. Los nombres de servicio con guiones bajos pueden no resolverse correctamente en versiones antiguas de Docker. Usa guiones en los nombres de servicio para máxima compatibilidad.
Resumen
- Usa
restart: unless-stoppedy health checks en cada servicio de producción - Configura límites de memoria y CPU con
deploy.resourcespara prevenir contenedores descontrolados - Gestiona secretos con Docker secrets o archivos
.env— nunca codifiques credenciales directamente - Coloca Nginx como proxy inverso frente a los servicios de aplicación con terminación SSL
- Configura rotación de logs con
max-sizeymax-filepara prevenir agotamiento de disco - Usa
depends_onconcondition: service_healthypara un ordenamiento confiable de arranque - Ejecuta
docker compose up -d --remove-orphanspara limpiar contenedores obsoletos en cada despliegue - Docker Compose está listo para producción en despliegues de un solo host y equipos pequeños