Tema:
Docker Compose - Mejores Prácticas
Introducción
Una vez que comprendes los fundamentos de Docker Compose y puedes ejecutar aplicaciones multicontenedor básicas, el siguiente paso es aprender a configurarlas de manera robusta, segura y eficiente. Las mejores prácticas en Docker Compose no solo mejoran la confiabilidad de tus aplicaciones, sino que también facilitan el mantenimiento, la escalabilidad y el despliegue en diferentes entornos.
Este módulo cubre técnicas avanzadas como healthchecks para verificar el estado de los servicios, gestión de dependencias entre contenedores, límites de recursos para optimizar el rendimiento, manejo seguro de secretos, y configuraciones específicas para diferentes entornos. Aplicar estas prácticas desde el inicio te permitirá crear aplicaciones profesionales listas para producción.
Objetivo
Objetivo General:
- Capacitar a los participantes en la implementación de mejores prácticas para Docker Compose, incluyendo healthchecks, gestión de dependencias, límites de recursos, políticas de reinicio, manejo seguro de secretos y configuraciones avanzadas. Este módulo busca desarrollar habilidades para crear aplicaciones multicontenedor robustas, seguras y listas para entornos de producción.
1. Healthchecks: Verificando el Estado de los Servicios
¿Qué son los Healthchecks?
Los healthchecks son verificaciones automáticas que Docker ejecuta periódicamente para determinar si un contenedor está funcionando correctamente. A diferencia del estado "running", un healthcheck verifica que el servicio dentro del contenedor realmente está operativo y puede responder a solicitudes.
Beneficios: - Detecta servicios que están "running" pero no funcionan correctamente - Permite que otros servicios esperen hasta que las dependencias estén saludables - Facilita el auto-healing en orquestadores como Docker Swarm o Kubernetes - Mejora la confiabilidad de la aplicación
Sintaxis Básica
services:
web:
image: nginx:alpine
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
Parámetros explicados:
- test: Comando a ejecutar para verificar salud
- interval: Cada cuánto tiempo ejecutar el check (30 segundos)
- timeout: Tiempo máximo de espera para el comando (10 segundos)
- retries: Intentos fallidos antes de marcar como unhealthy (3 veces)
- start_period: Tiempo de gracia al inicio (40 segundos)
Ejemplo Práctico: Web + Database con Healthchecks
services:
db:
image: postgres:15-alpine
environment:
POSTGRES_PASSWORD: example_password
POSTGRES_DB: myapp
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
volumes:
- db-data:/var/lib/postgresql/data
web:
image: nginx:alpine
ports:
- "8080:80"
depends_on:
db:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost"]
interval: 30s
timeout: 10s
retries: 3
volumes:
db-data:
Nota importante: depends_on con condition: service_healthy asegura que web no inicie hasta que db esté saludable.
2. Gestión de Dependencias entre Servicios
depends_on Básico vs Avanzado
Básico (solo orden de inicio):
services:
web:
image: myapp
depends_on:
- db
db inicie antes, no que esté listo.
Avanzado (con condiciones):
services:
web:
image: myapp
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
Condiciones disponibles:
- service_started: El servicio ha iniciado (estado por defecto)
- service_healthy: El servicio está saludable (requiere healthcheck)
- service_completed_successfully: El servicio terminó exitosamente (para tareas one-time)
Ejemplo Completo: Aplicación con Múltiples Dependencias
services:
# Base de datos
postgres:
image: postgres:15-alpine
environment:
POSTGRES_PASSWORD: secure_password
POSTGRES_DB: appdb
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
volumes:
- postgres-data:/var/lib/postgresql/data
# Cache
redis:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
# Migraciones de base de datos (tarea one-time)
migrations:
image: myapp:latest
command: ["python", "manage.py", "migrate"]
depends_on:
postgres:
condition: service_healthy
# Aplicación web
web:
image: myapp:latest
ports:
- "8000:8000"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
migrations:
condition: service_completed_successfully
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
volumes:
postgres-data:
3. Límites de Recursos: Optimizando el Rendimiento
¿Por qué Limitar Recursos?
Sin límites de recursos, un contenedor puede consumir toda la CPU y memoria del host, afectando a otros servicios. Establecer límites garantiza: - Estabilidad del sistema - Prevención de "noisy neighbors" - Mejor planeación de capacidad - Comportamiento predecible
Sintaxis de Recursos
services:
web:
image: myapp
deploy:
resources:
limits:
cpus: '0.50' # Máximo 50% de un CPU core
memory: 512M # Máximo 512MB de RAM
reservations:
cpus: '0.25' # Mínimo garantizado: 25% de un CPU core
memory: 256M # Mínimo garantizado: 256MB de RAM
Diferencia entre limits y reservations: - limits: Valor máximo que el contenedor puede usar - reservations: Recursos garantizados que el contenedor siempre tendrá disponibles
Ejemplo Práctico con Diferentes Perfiles de Recursos
services:
# Base de datos - Recursos altos
database:
image: postgres:15-alpine
deploy:
resources:
limits:
cpus: '2.0'
memory: 2G
reservations:
cpus: '1.0'
memory: 1G
environment:
POSTGRES_PASSWORD: example
volumes:
- db-data:/var/lib/postgresql/data
# Aplicación web - Recursos medios
web:
image: nginx:alpine
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
reservations:
cpus: '0.5'
memory: 256M
ports:
- "80:80"
# Worker de tareas - Recursos bajos
worker:
image: myapp-worker
deploy:
resources:
limits:
cpus: '0.5'
memory: 256M
reservations:
cpus: '0.25'
memory: 128M
volumes:
db-data:
4. Restart Policies: Control de Reinicio
Políticas Disponibles
Docker Compose ofrece diferentes políticas de reinicio para manejar fallos:
| Política | Descripción | Uso Recomendado |
|---|---|---|
no |
Nunca reiniciar automáticamente | Desarrollo/debugging |
always |
Siempre reiniciar, incluso después de detenerlo manualmente | Servicios críticos |
on-failure |
Reiniciar solo si el contenedor falla (exit code != 0) | Aplicaciones que pueden fallar temporalmente |
unless-stopped |
Siempre reiniciar, excepto si se detuvo manualmente | Recomendado para producción |
Ejemplos de Uso
services:
# Servicio crítico que siempre debe estar corriendo
nginx:
image: nginx:alpine
restart: always
ports:
- "80:80"
# Base de datos que debe reiniciar excepto si se detiene manualmente
postgres:
image: postgres:15-alpine
restart: unless-stopped
environment:
POSTGRES_PASSWORD: example
volumes:
- db-data:/var/lib/postgresql/data
# Tarea que debe reintentar si falla
worker:
image: myapp-worker
restart: on-failure:5 # Reintentar máximo 5 veces
depends_on:
- postgres
# Contenedor de desarrollo, sin reinicio automático
dev-tools:
image: mydev-tools
restart: "no"
profiles:
- development
volumes:
db-data:
Nota sobre on-failure:5: El número indica el máximo de intentos de reinicio.
5. Manejo Seguro de Secretos y Variables
Variables de Entorno con archivo .env
El archivo .env permite centralizar configuraciones sin hardcodearlas en el compose.yaml.
Estructura del proyecto:
proyecto/
├── compose.yaml
├── .env
└── .gitignore
Archivo: .env
# Database
POSTGRES_VERSION=15-alpine
POSTGRES_PASSWORD=my_secure_password
POSTGRES_DB=myapp_db
# Application
APP_ENV=production
APP_DEBUG=false
APP_PORT=8000
# Redis
REDIS_VERSION=7-alpine
Archivo: compose.yaml
services:
db:
image: postgres:${POSTGRES_VERSION}
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- postgres-data:/var/lib/postgresql/data
redis:
image: redis:${REDIS_VERSION}
web:
build: .
ports:
- "${APP_PORT}:8000"
environment:
APP_ENV: ${APP_ENV}
APP_DEBUG: ${APP_DEBUG}
DATABASE_URL: postgresql://postgres:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
depends_on:
- db
- redis
volumes:
postgres-data:
Archivo: .gitignore
.env
Docker Secrets (Más Seguro)
Para información realmente sensible, usa Docker secrets en lugar de variables de entorno.
services:
db:
image: postgres:15-alpine
secrets:
- db_password
- db_user
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
POSTGRES_USER_FILE: /run/secrets/db_user
volumes:
- db-data:/var/lib/postgresql/data
web:
image: myapp
secrets:
- api_key
environment:
API_KEY_FILE: /run/secrets/api_key
secrets:
db_password:
file: ./secrets/db_password.txt
db_user:
file: ./secrets/db_user.txt
api_key:
file: ./secrets/api_key.txt
volumes:
db-data:
Ventajas de secrets sobre environment:
- No aparecen en docker inspect
- No se almacenan en variables de entorno (más seguras)
- Se montan como archivos en /run/secrets/
- Mejor para producción
6. Configuración de Logging
Logging Drivers y Rotación
Configurar el logging correctamente previene que los logs llenen el disco.
services:
web:
image: nginx:alpine
logging:
driver: "json-file"
options:
max-size: "10m" # Máximo 10MB por archivo
max-file: "3" # Mantener 3 archivos
labels: "service,environment"
labels:
service: "web"
environment: "production"
app:
image: myapp
logging:
driver: "json-file"
options:
max-size: "5m"
max-file: "5"
compress: "true" # Comprimir logs antiguos
# Enviar logs a syslog
monitoring:
image: monitoring-app
logging:
driver: "syslog"
options:
syslog-address: "tcp://192.168.0.42:514"
tag: "monitoring"
Drivers de Logging Disponibles
| Driver | Descripción | Uso |
|---|---|---|
json-file |
Por defecto, logs en JSON | General |
syslog |
Envía a syslog | Integración con sistemas Unix |
journald |
Integración con systemd | Sistemas con systemd |
local |
Optimizado para performance | Alto volumen de logs |
none |
Deshabilita logging | Cuando no necesitas logs |
7. Redes y Volúmenes Avanzados
Named Volumes vs Bind Mounts
Named Volumes (Recomendado para producción):
services:
db:
image: postgres:15-alpine
volumes:
- postgres-data:/var/lib/postgresql/data # Named volume
- postgres-config:/etc/postgresql # Named volume
volumes:
postgres-data:
driver: local
postgres-config:
driver: local
Bind Mounts (Útil para desarrollo):
services:
web:
image: nginx:alpine
volumes:
- ./html:/usr/share/nginx/html:ro # Read-only
- ./nginx.conf:/etc/nginx/nginx.conf:ro
Redes Personalizadas
services:
# Frontend en red pública
nginx:
image: nginx:alpine
networks:
- frontend
ports:
- "80:80"
# API en ambas redes
api:
image: myapi
networks:
- frontend
- backend
# Base de datos solo en red privada
database:
image: postgres:15-alpine
networks:
- backend
environment:
POSTGRES_PASSWORD: example
# Redis solo en red privada
redis:
image: redis:7-alpine
networks:
- backend
networks:
frontend:
driver: bridge
backend:
driver: bridge
internal: true # Red interna, sin acceso a internet
Ventajas de redes separadas: - Aislamiento de seguridad - Control de comunicación entre servicios - Mejor organización
8. Profiles: Configuraciones para Diferentes Entornos
Los profiles permiten activar/desactivar servicios según el entorno (desarrollo, testing, producción).
services:
# Servicios que siempre están activos
db:
image: postgres:15-alpine
environment:
POSTGRES_PASSWORD: example
volumes:
- db-data:/var/lib/postgresql/data
web:
image: myapp
ports:
- "8000:8000"
depends_on:
- db
# Solo para desarrollo
adminer:
image: adminer
profiles:
- development
ports:
- "8080:8080"
depends_on:
- db
# Solo para desarrollo
mailhog:
image: mailhog/mailhog
profiles:
- development
ports:
- "1025:1025" # SMTP
- "8025:8025" # Web UI
# Solo para testing
test-runner:
image: myapp-tests
profiles:
- testing
depends_on:
- db
command: ["pytest"]
# Solo para producción
nginx-proxy:
image: nginx:alpine
profiles:
- production
ports:
- "443:443"
volumes:
- ./nginx-prod.conf:/etc/nginx/nginx.conf:ro
volumes:
db-data:
Uso de profiles:
# Desarrollo (incluye adminer y mailhog)
docker compose --profile development up
# Testing (incluye test-runner)
docker compose --profile testing up
# Producción (incluye nginx-proxy)
docker compose --profile production up
# Sin profiles (solo servicios base)
docker compose up
9. Extensión y Reutilización con Anchors YAML
Los anchors de YAML permiten reutilizar configuraciones comunes.
# Definir configuraciones comunes
x-common-variables: &common-variables
TZ: America/Bogota
LOG_LEVEL: info
x-healthcheck-defaults: &healthcheck-defaults
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
x-deploy-defaults: &deploy-defaults
resources:
limits:
cpus: '0.50'
memory: 512M
reservations:
cpus: '0.25'
memory: 256M
services:
web1:
image: myapp:latest
environment:
<<: *common-variables
SERVICE_NAME: web1
healthcheck:
<<: *healthcheck-defaults
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
deploy:
<<: *deploy-defaults
web2:
image: myapp:latest
environment:
<<: *common-variables
SERVICE_NAME: web2
healthcheck:
<<: *healthcheck-defaults
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
deploy:
<<: *deploy-defaults
worker:
image: myapp-worker:latest
environment:
<<: *common-variables
SERVICE_NAME: worker
deploy:
<<: *deploy-defaults
Laboratorio Final: Aplicación Completa con Mejores Prácticas
Objetivo
Implementar una aplicación web completa aplicando todas las mejores prácticas aprendidas.
Arquitectura
- Frontend: Nginx
- Backend: API Python/Node
- Base de datos: PostgreSQL
- Cache: Redis
- Monitoring: Healthchecks configurados
Paso 1: Crear estructura del proyecto
mkdir ~/docker-best-practices-lab
cd ~/docker-best-practices-lab
mkdir -p secrets
echo "secure_db_password_123" > secrets/db_password.txt
echo "redis_password_456" > secrets/redis_password.txt
Paso 2: Crear archivo .env
cat > .env << 'EOF'
# Database
POSTGRES_VERSION=15-alpine
POSTGRES_DB=myapp_production
# Redis
REDIS_VERSION=7-alpine
# Application
APP_ENV=production
APP_PORT=8000
EOF
Paso 3: Crear compose.yaml completo
cat > compose.yaml << 'EOF'
services:
# Base de datos con todas las mejores prácticas
postgres:
image: postgres:${POSTGRES_VERSION}
restart: unless-stopped
secrets:
- db_password
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- postgres-data:/var/lib/postgresql/data
networks:
- backend
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
deploy:
resources:
limits:
cpus: '1.0'
memory: 1G
reservations:
cpus: '0.5'
memory: 512M
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
# Redis para cache
redis:
image: redis:${REDIS_VERSION}
restart: unless-stopped
command: redis-server --requirepass $(cat /run/secrets/redis_password)
secrets:
- redis_password
networks:
- backend
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
deploy:
resources:
limits:
cpus: '0.5'
memory: 256M
reservations:
cpus: '0.25'
memory: 128M
logging:
driver: "json-file"
options:
max-size: "5m"
max-file: "3"
# Aplicación backend
api:
image: nginx:alpine # Reemplazar con tu imagen
restart: unless-stopped
environment:
APP_ENV: ${APP_ENV}
networks:
- frontend
- backend
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
reservations:
cpus: '0.5'
memory: 256M
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "5"
# Frontend/Proxy
nginx:
image: nginx:alpine
restart: unless-stopped
ports:
- "${APP_PORT}:80"
networks:
- frontend
depends_on:
api:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost"]
interval: 30s
timeout: 10s
retries: 3
deploy:
resources:
limits:
cpus: '0.5'
memory: 256M
reservations:
cpus: '0.25'
memory: 128M
logging:
driver: "json-file"
options:
max-size: "5m"
max-file: "3"
# Herramienta de desarrollo (solo con profile)
adminer:
image: adminer
restart: unless-stopped
profiles:
- development
ports:
- "8080:8080"
networks:
- backend
depends_on:
postgres:
condition: service_healthy
networks:
frontend:
driver: bridge
backend:
driver: bridge
internal: true
volumes:
postgres-data:
driver: local
secrets:
db_password:
file: ./secrets/db_password.txt
redis_password:
file: ./secrets/redis_password.txt
EOF
Paso 4: Ejecutar la aplicación
# Modo producción (sin herramientas de desarrollo)
docker compose up -d
# Verificar estado de los servicios
docker compose ps
# Ver logs de todos los servicios
docker compose logs
# Ver solo logs de postgres
docker compose logs postgres
# Verificar healthchecks
docker compose ps --format "table {{.Service}}\t{{.Status}}\t{{.Health}}"
Paso 5: Modo desarrollo (con Adminer)
docker compose --profile development up -d
Ahora puedes acceder a Adminer en http://localhost:8080 para administrar la base de datos.
Paso 6: Verificar recursos
# Ver uso de recursos
docker stats
Paso 7: Probar restart policy
# Detener un contenedor y ver si se reinicia
docker stop docker-best-practices-lab-nginx-1
Espera unos segundos y verifica:
docker compose ps
El contenedor debería reiniciarse automáticamente debido a restart: unless-stopped.
Paso 8: Limpieza
docker compose down
Para limpiar completamente incluyendo volúmenes:
docker compose down -v
Comandos Útiles de Docker Compose
# Ver estado detallado incluyendo health
docker compose ps --format "table {{.Service}}\t{{.Status}}\t{{.Health}}"
# Ver configuración final (con variables resueltas)
docker compose config
# Validar sintaxis del compose file
docker compose config --quiet
# Ver logs en tiempo real de todos los servicios
docker compose logs -f
# Ver logs de un servicio específico
docker compose logs -f postgres
# Escalar un servicio (crear múltiples réplicas)
docker compose up -d --scale worker=3
# Recrear un servicio específico
docker compose up -d --force-recreate postgres
# Ver uso de recursos
docker compose top
# Ejecutar comando en un servicio
docker compose exec postgres psql -U postgres
# Detener sin eliminar
docker compose stop
# Iniciar servicios detenidos
docker compose start
# Reiniciar servicios
docker compose restart
# Ver eventos en tiempo real
docker compose events
# Eliminar servicios detenidos
docker compose rm
# Eliminar todo incluyendo volúmenes
docker compose down -v
# Eliminar todo incluyendo imágenes
docker compose down --rmi all -v