Skip to content

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
Esto solo garantiza que 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