StackPractices
intermediate Por Mathias Paulenko

Docker Compose Dev/Prod: Separación de Entornos

Separa configs de Docker Compose para desarrollo y producción con overrides

Temas: devops

Visión General

Docker Compose soporta múltiples archivos y patrones de override para separar configuraciones de desarrollo y producción. Esta recipe muestra cómo usar un archivo base, un override de dev y un override de prod para ejecutar la misma app con diferentes settings (hot reload, puertos de debug, TLS, límites de recursos) sin duplicar definiciones de servicios.

Cuándo Usar

  • Necesitas diferentes configs para dev (hot reload, debug) y prod (TLS, límites de recursos)
  • Quieres evitar mantener archivos compose separados con servicios duplicados
  • Usas Docker Compose en CI/CD y necesitas overrides específicos por entorno
  • Quieres defaults sensatos con la capacidad de overridear por entorno

Solución

Archivo compose base

# docker-compose.yml (base — compartido por dev y prod)
services:
    api:
        build:
            context: .
            dockerfile: Dockerfile
        environment:
            - NODE_ENV=${NODE_ENV:-development}
            - DATABASE_URL=postgres://app:app@db:5432/app
        depends_on:
            db:
                condition: service_healthy
        ports:
            - "${API_PORT:-3000}:3000"

    db:
        image: postgres:16-alpine
        environment:
            POSTGRES_USER: app
            POSTGRES_PASSWORD: app
            POSTGRES_DB: app
        volumes:
            - db-data:/var/lib/postgresql/data
        healthcheck:
            test: ["CMD", "pg_isready", "-U", "app"]
            interval: 10s
            timeout: 5s
            retries: 5

volumes:
    db-data:

Override de dev

# docker-compose.dev.yml
services:
    api:
        build:
            target: builder
        environment:
            - NODE_ENV=development
            - DEBUG=app:*
        volumes:
            - .:/app
            - /app/node_modules
        command: npm run dev
        ports:
            - "3000:3000"
            - "9229:9229"  # Debugger de Node.js

    db:
        ports:
            - "5432:5432"
        volumes:
            - db-data-dev:/var/lib/postgresql/data

volumes:
    db-data-dev:

Override de prod

# docker-compose.prod.yml
services:
    api:
        build:
            target: production
        environment:
            - NODE_ENV=production
        deploy:
            replicas: 3
            resources:
                limits:
                    cpus: "1.0"
                    memory: 512M
                reservations:
                    cpus: "0.5"
                    memory: 256M
            restart_policy:
                condition: on-failure
                max_attempts: 3
        ports:
            - "80:3000"
        healthcheck:
            test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
            interval: 30s
            timeout: 5s
            retries: 3
            start_period: 10s

    db:
        environment:
            POSTGRES_PASSWORD: ${DB_PASSWORD}
        volumes:
            - db-data:/var/lib/postgresql/data
        deploy:
            resources:
                limits:
                    cpus: "2.0"
                    memory: 1G

Ejecutando dev y prod

# Desarrollo (hot reload + debugger)
docker compose -f docker-compose.yml -f docker-compose.dev.yml up

# Producción (optimizado + límites de recursos)
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

# Usando COMPOSE_FILE env var (sin flags -f)
export COMPOSE_FILE=docker-compose.yml:docker-compose.dev.yml
docker compose up

Perfiles de Compose para servicios opcionales

# docker-compose.yml
services:
    api:
        build: .
        ports:
            - "3000:3000"

    db:
        image: postgres:16-alpine
        profiles: ["dev", "prod"]

    redis:
        image: redis:7-alpine
        profiles: ["prod"]

    mailhog:
        image: mailhog/mailhog
        profiles: ["dev"]
        ports:
            - "8025:8025"
# Iniciar solo api (sin servicios de perfil)
docker compose up

# Iniciar con perfil dev (api + db + mailhog)
docker compose --profile dev up

# Iniciar con perfil prod (api + db + redis)
docker compose --profile prod up

Archivo .env para variables de entorno

# .env.dev
NODE_ENV=development
API_PORT=3000
DB_PASSWORD=devpassword

# .env.prod
NODE_ENV=production
API_PORT=80
DB_PASSWORD=strong_prod_password
# Usar archivo env específico
docker compose --env-file .env.dev -f docker-compose.yml -f docker-compose.dev.yml up
docker compose --env-file .env.prod -f docker-compose.yml -f docker-compose.prod.yml up -d

Explicación

Docker Compose fusiona múltiples archivos en orden. Los archivos posteriores overridean los anteriores. El archivo base define servicios compartidos, y los archivos de override añaden o modifican settings específicos de cada entorno.

Reglas de merge:

  • Escalares: Los valores posteriores reemplazan los anteriores (ej., NODE_ENV=production overridea NODE_ENV=development).
  • Listas: Las listas posteriores reemplazan las anteriores completamente (no se fusionan items de lista).
  • Mapas: Fusionados clave por clave (ej., añadir una nueva variable de entorno mantiene las existentes).
  • Volúmenes: Los nuevos volúmenes se añaden, no se reemplazan.

El flag -f especifica archivos en orden de merge. La env var COMPOSE_FILE hace lo mismo sin flags. Usa --env-file para cargar variables específicas del entorno.

Los perfiles permiten incluir servicios opcionales solo cuando se necesitan. Los servicios sin perfil siempre arrancan. Los servicios con perfil solo arrancan cuando se pasa --profile <name>.

Variantes

PatrónArchivosUsar Cuando
Base + override3 archivos (base, dev, prod)Split estándar dev/prod
Perfiles1 archivo con perfilesServicios opcionales por entorno
Múltiples .envarchivos env por entornoGestión de secretos sin cambios en compose
COMPOSE_FILEenv varEvitar tipear flags -f

Pautas

  • Mantener las definiciones de servicios compartidos en el archivo base. Solo overridear lo que difiere.
  • Usar target en la sección build para seleccionar targets de multi-stage build por entorno.
  • Montar código fuente como volúmenes en dev para hot reload. Nunca hacer esto en prod.
  • Configurar límites de recursos y restart policies solo en prod.
  • Exponer puertos de debug (9229 para Node.js, 5005 para Java) solo en dev.
  • Usar --env-file para separar secretos por entorno.
  • Usar perfiles para servicios opcionales (MailHog en dev, Redis en prod).
  • Nombrar archivos de override claramente: docker-compose.dev.yml, docker-compose.prod.yml.
  • Usar la env var COMPOSE_FILE en CI/CD para evitar flags -f largos.

Errores Comunes

  • Duplicar todos los servicios en archivos dev y prod en lugar de overridear solo diferencias.
  • Montar código fuente en prod. Esto acopla el contenedor al filesystem del host y rompe reproducibilidad.
  • No configurar límites de recursos en prod. Un solo contenedor puede consumir todos los recursos del host.
  • Exponer puertos de debug en prod. Esto es un riesgo de seguridad.
  • Usar la misma contraseña de base de datos para dev y prod. Los secretos de prod deben venir de env files o secret managers.
  • Olvidar que las listas se reemplazan, no se fusionan. Añadir un puerto en el override elimina todos los puertos base.
  • No usar depends_on con condition: service_healthy para orden de inicio.

Tips de Rendimiento

  1. Usa --build solo cuando sea necesario. Rebuildear en cada up es lento:
# Rápido: usar imágenes cacheadas
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

# Más lento: forzar rebuild
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build
  1. Usa named volumes para mejor I/O. Los named volumes son más rápidos que los bind mounts en producción:
# Lento: bind mount
volumes:
  - ./data:/var/lib/postgresql/data

# Rápido: named volume
volumes:
  - db-data-prod:/var/lib/postgresql/data
  1. Limita el logging de contenedores en prod. Logs sin límite llenan el disco:
services:
  api:
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

Preguntas frecuentes

¿Cómo fusiona Docker Compose múltiples archivos?

Los archivos se fusionan en el orden en que aparecen en la línea de comandos. Para mapas (environment, labels), las claves se fusionan. Para listas (ports, volumes), la lista del archivo posterior reemplaza la anterior completamente. Los escalares se reemplazan.

¿Puedo usar Docker Compose para producción?

Sí, pero solo para deployments pequeños. Para producción a escala, usa Docker Swarm o Kubernetes. Compose está bien para deployments de un solo host, prototipado y CI/CD.

¿Cómo veo la configuración fusionada?
docker compose -f docker-compose.yml -f docker-compose.prod.yml config

Esto genera la configuración final fusionada, útil para debuggear overrides.

¿Cuál es la diferencia entre perfiles y overrides?

Los perfiles controlan qué servicios arrancan. Los overrides controlan cómo se configuran los servicios. Usa perfiles para servicios opcionales (Redis, MailHog) y overrides para settings específicos del entorno (límites de recursos, env vars).

Override de Staging
# docker-compose.staging.yml
services:
  api:
    build:
      target: production
    environment:
      - NODE_ENV=staging
      - SENTRY_DSN=${SENTRY_DSN}
    deploy:
      replicas: 1
      resources:
        limits:
          cpus: "0.5"
          memory: 256M
      restart_policy:
        condition: on-failure
        max_attempts: 3
    ports:
      - "8080:3000"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 5s
      retries: 3

  db:
    environment:
      POSTGRES_PASSWORD: ${STAGING_DB_PASSWORD}
    volumes:
      - db-data-staging:/var/lib/postgresql/data

volumes:
  db-data-staging:
# Ejecutar staging
docker compose --env-file .env.staging \
    -f docker-compose.yml -f docker-compose.staging.yml up -d
Integración CI/CD con GitHub Actions
# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main, staging]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build and deploy
        run: |
          if [ "${{ github.ref }}" = "refs/heads/main" ]; then
            ENV=prod
          else
            ENV=staging
          fi

          docker compose --env-file .env.${ENV} \
            -f docker-compose.yml -f docker-compose.${ENV}.yml \
            up -d --build

      - name: Verify health
        run: |
          sleep 15
          docker compose -f docker-compose.yml -f docker-compose.${ENV}.yml ps
          curl -f http://localhost:3000/health
Makefile para Gestión de Entornos
# Makefile — Simplificar comandos docker compose
.PHONY: dev prod staging down logs ps

dev:
	docker compose --env-file .env.dev \
		-f docker-compose.yml -f docker-compose.dev.yml up

prod:
	docker compose --env-file .env.prod \
		-f docker-compose.yml -f docker-compose.prod.yml up -d

staging:
	docker compose --env-file .env.staging \
		-f docker-compose.yml -f docker-compose.staging.yml up -d

down:
	docker compose -f docker-compose.yml -f docker-compose.dev.yml down
	docker compose -f docker-compose.yml -f docker-compose.prod.yml down

logs:
	docker compose -f docker-compose.yml -f docker-compose.dev.yml logs -f

ps:
	docker compose -f docker-compose.yml -f docker-compose.dev.yml ps
# Uso
make dev       # Iniciar entorno dev
make prod      # Iniciar entorno prod
make staging   # Iniciar entorno staging
make down      # Detener todo
make logs      # Tail logs
Configuración de Logging por Entorno
# docker-compose.prod.yml — Logging de producción
services:
  api:
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"
        labels: "service,environment"

  db:
    logging:
      driver: syslog
      options:
        syslog-address: "tcp://192.168.1.100:514"
        tag: "db-prod"
# docker-compose.dev.yml — Logging de dev (consola)
services:
  api:
    logging:
      driver: json-file
      options:
        max-size: "5m"
        max-file: "2"
Gestión de Secretos con Docker Secrets
# docker-compose.prod.yml — Usando Docker secrets
services:
  db:
    secrets:
      - db-password
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db-password

secrets:
  db-password:
    file: ./secrets/db_password.txt
# Crear archivo de secreto (nunca commitear a git)
echo "my_secure_password" > secrets/db_password.txt
echo "secrets/" >> .gitignore