Sobrescribe la Configuración de Docker Compose por Entorno
Cómo usar archivos override de Docker Compose para configuraciones por entorno: desarrollo, test, staging y producción, con profiles y secrets.
Resumen
Docker Compose fusiona dos o más archivos en una sola configuración. Todo parte del docker-compose.yml base, que contiene la estructura de los servicios. Los archivos override la adaptan a cada entorno: puertos o volúmenes distintos en dev, límites de recursos en prod, quizá un Mailhog que solo existe en local. Compose fusiona los archivos en el orden en que los pasas —los posteriores sobrescriben o extienden a los anteriores— así que una sola config base cubre dev, test, staging y producción sin copiarla cuatro veces.
Si lo que necesitas son dos stacks de Compose completamente separados para dev y producción en lugar de una base más diferencias, la receta de split dev/prod de Docker Compose cubre ese enfoque: los archivos override funcionan mejor cuando los entornos comparten la mayor parte de la configuración y solo cambian en detalles.
Cuándo Usarlo
- Desarrollo local con hot reload y herramientas de debug
- Ejecutar tests de forma aislada con configuración específica
- Staging y producción con ajustes de producción (límites de recursos, sin debug)
- Cuando necesitas servicios distintos por entorno (Mailhog en dev, SES en prod)
- Gestionar aplicaciones multi-servicio en varios entornos
Cuándo NO Usarlo
- Un solo entorno: con un único
docker-compose.ymlbasta - Despliegues de producción: usa Kubernetes o ECS, no Docker Compose
- Cuando las configuraciones difieren drásticamente: archivos separados son más claros que overrides
- Cuando necesitas orquestación compleja: Compose es para dev/test, no para producción
Solución
Compose file base
# docker-compose.yml — Configuración base
services:
app:
build:
context: .
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- LOG_LEVEL=info
depends_on:
db:
condition: service_healthy
restart: unless-stopped
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: myapp
POSTGRES_USER: myapp
POSTGRES_PASSWORD: ${DB_PASSWORD:-changeme}
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U myapp"]
interval: 5s
timeout: 3s
retries: 5
restart: unless-stopped
redis:
image: redis:7-alpine
restart: unless-stopped
volumes:
db-data:
Override de desarrollo
# docker-compose.override.yml — Se usa por defecto con `docker compose up`
services:
app:
build:
target: dev # Target dev del multi-stage build
ports:
- "3000:3000"
- "9229:9229" # Debugger de Node.js
environment:
- NODE_ENV=development
- LOG_LEVEL=debug
volumes:
- .:/app # Hot reload
- /app/node_modules # Evita que el host sobrescribe el directorio
command: npm run dev # Sobrescribe el entrypoint
db:
ports:
- "5432:5432" # Expone el puerto para herramientas locales
environment:
POSTGRES_PASSWORD: devpassword
# Servicios solo de dev
mailhog:
image: mailhog/mailhog:latest
ports:
- "1025:1025" # SMTP
- "8025:8025" # Web UI
adminer:
image: adminer:latest
ports:
- "8080:8080"
depends_on:
- db
Override de producción
# docker-compose.prod.yml
services:
app:
build:
target: production
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- LOG_LEVEL=warn
- DATABASE_URL=postgres://myapp:${DB_PASSWORD}@db:5432/myapp
deploy:
replicas: 3
resources:
limits:
cpus: "1.0"
memory: 512M
reservations:
cpus: "0.5"
memory: 256M
restart_policy:
condition: on-failure
max_attempts: 3
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
db:
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- db-data:/var/lib/postgresql/data
deploy:
resources:
limits:
cpus: "2.0"
memory: 2G
redis:
deploy:
resources:
limits:
cpus: "0.5"
memory: 256M
Override de test
# docker-compose.test.yml
services:
app:
build:
target: test
environment:
- NODE_ENV=test
- DATABASE_URL=postgres://myapp:testpass@db:5432/testdb
command: npm test
depends_on:
db:
condition: service_healthy
db:
environment:
POSTGRES_DB: testdb
POSTGRES_PASSWORD: testpass
# Sin volumen persistente: la base de datos de test es efímera
volumes: !reset []
# redis no existe en el entorno de test
redis: !reset null
Ejecutar con distintos overrides
# Desarrollo (por defecto: docker-compose.yml + docker-compose.override.yml)
docker compose up
# Producción
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
# Test
docker compose -f docker-compose.yml -f docker-compose.test.yml up --abort-on-container-exit
# Compilar las imágenes de producción
docker compose -f docker-compose.yml -f docker-compose.prod.yml build
# Levantar un servicio concreto
docker compose -f docker-compose.yml -f docker-compose.prod.yml up app
# Ver la configuración ya fusionada
docker compose -f docker-compose.yml -f docker-compose.prod.yml config
Usar la variable COMPOSE_FILE
# .env
COMPOSE_FILE=docker-compose.yml:docker-compose.prod.yml
# Ahora `docker compose up` usa ambos archivos
docker compose up -d
Compose profiles
# docker-compose.yml
services:
app:
# Siempre se inicia
image: my-app
db:
# Siempre se inicia
image: postgres:16
debug-tools:
# Solo se inicia con --profile debug
profiles:
- debug
image: nicolaka/netshoot
network_mode: "service:app"
load-test:
# Solo se inicia con --profile loadtest
profiles:
- loadtest
image: grafana/k6
volumes:
- ./tests:/scripts
# Arrancar sin profiles
docker compose up -d # Solo app y db
# Arrancar con el profile debug
docker compose --profile debug up -d
# Arrancar con el profile de load test
docker compose --profile loadtest up
# Arrancar con varios profiles
docker compose --profile debug --profile loadtest up
Gestión de secrets
# docker-compose.prod.yml
services:
app:
secrets:
- db-password
- api-key
environment:
- DB_PASSWORD_FILE=/run/secrets/db-password
- API_KEY_FILE=/run/secrets/api-key
db:
secrets:
- db-password
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db-password
secrets:
db-password:
file: ./secrets/db-password.txt
api-key:
file: ./secrets/api-key.txt
Variables de entorno desde .env
# .env
DB_PASSWORD=supersecret
REDIS_URL=redis://redis:6379
JWT_SECRET=my-jwt-secret
# docker-compose.yml
services:
app:
environment:
- DB_PASSWORD=${DB_PASSWORD}
- REDIS_URL=${REDIS_URL}
- JWT_SECRET=${JWT_SECRET}
Archivos .env por entorno
# .env.dev
DB_PASSWORD=devpassword
LOG_LEVEL=debug
# .env.prod
DB_PASSWORD=supersecret
LOG_LEVEL=warn
# Elige qué archivo env cargar: combina bien con los -f de override
docker compose --env-file .env.dev up
docker compose --env-file .env.prod -f docker-compose.yml -f docker-compose.prod.yml up -d
# O fija un archivo env por servicio: forma parte de la configuración,
# así que un override puede apuntarlo a otro archivo por entorno
services:
app:
env_file:
- .env.dev
Comportamiento del merge entre archivos
# docker-compose.yml (base)
services:
app:
image: my-app:latest
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- LOG_LEVEL=info
# docker-compose.override.yml
services:
app:
# Los puertos se fusionan (no se reemplazan)
ports:
- "9229:9229" # Se suma a 3000:3000
# Las variables de entorno se fusionan
environment:
- NODE_ENV=development # Sobrescribe production
- DEBUG=true # Se añade
Cómo Funciona el Merge
Compose fusiona los archivos de izquierda a derecha, con reglas distintas por tipo de atributo:
| Tipo de atributo | Comportamiento | Ejemplo |
|---|---|---|
Secuencias (ports, volumes, secrets) | Se concatenan: el override añade, no reemplaza | Base 3000:3000 + override 9229:9229 → ambos |
Mapas (environment, labels, sysctls) | Se fusionan por clave: gana el último valor de la misma clave | NODE_ENV reemplazado, DEBUG añadido |
Escalares (image, command, restart) | Se reemplazan por completo con el archivo más reciente | command: npm run dev gana |
depends_on, networks | Se fusionan por nombre de servicio/red | El override añade condiciones |
La Compose Specification ofrece dos mecanismos para salir de estas reglas:
!reset <valor>— elimina por completo el valor heredado.volumes: !reset []descarta todos los volúmenes del base;redis: !reset nullquita el servicio de la configuración fusionada (así es como el override de test elimina redis, más limpio que un profile falso).!override— reemplaza un atributo por completo en lugar de fusionarlo, útil enportscuando no quieres que los mapeos del base se filtren a producción.
Tres detalles que suelen sorprender:
- Todas las rutas relativas se resuelven respecto al directorio del archivo base, no del override. Si tu override vive en otra carpeta, mantén las rutas relativas al base o usa rutas absolutas.
- Ojo con los nombres de archivo si dependes de la carga automática. Compose v2 encuentra por sí solo
compose.yaml,compose.yml,docker-compose.yamlodocker-compose.yml, más el.overrideque corresponda al que haya encontrado. Los nombresdocker-compose.*siguen funcionando;compose.*es la convención nueva. Cualquier otro nombre solo carga con-fexplícitos. - El orden de los
-fes el orden de override.-f a.yml -f b.ymlsignifica que b gana en los conflictos, y por eso la gente suele apilar base → entorno → un archivo local sin trackear: los ajustes personales siempre quedan últimos.
Verifica qué fusionó Compose realmente antes de depurar cualquier otra cosa:
docker compose -f docker-compose.yml -f docker-compose.prod.yml config
Eso imprime la configuración resuelta: la fuente de verdad para “por qué está este puerto/variable/volumen aquí”.
Variantes
Usar extends para reutilizar definiciones de servicios
# docker-compose.yml
services:
web:
extends:
file: docker-compose.base.yml
service: app
environment:
- APP_ROLE=web
worker:
extends:
file: docker-compose.base.yml
service: app
environment:
- APP_ROLE=worker
ports: [] # Sin puertos para worker
Docker Compose con dependencias por healthcheck
services:
app:
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
db:
image: postgres:16
healthcheck:
test: ["CMD-SHELL", "pg_isready"]
interval: 5s
timeout: 3s
retries: 10
start_period: 10s
Watch mode para desarrollo
# docker-compose.override.yml
services:
app:
develop:
watch:
- action: sync
path: ./src
target: /app/src
- action: rebuild
path: package.json
docker compose watch # Sincroniza cambios y reconstruye automáticamente
Buenas Prácticas
- Mantén
docker-compose.ymlcomo base: no pongas configuración específica de entorno en él - Usa
docker-compose.override.ymlpara dev: Compose lo carga automáticamente - Usa
-fexplícitos en todos los entornos que no sean dev: evita overrides accidentales de desarrollo - Guarda los secrets en
.envo Docker secrets, nunca hardcodeados en el YAML - Marca los servicios opcionales con
profilespara quedocker compose upno lo levante todo - Combina
depends_onconconditiony healthchecks: la app conecta solo cuando la base de datos está realmente lista - Ejecuta
docker compose configpara verificar el resultado fusionado: detecta problemas de merge antes de levantar el stack
Errores Comunes
- Olvidar que override.yml es automático:
docker compose upfusionadocker-compose.override.ymlsiempre que existe — usa-fcuando quieras control sobre qué se carga. - Hardcodear secrets en el YAML: las contraseñas escritas en
docker-compose.ymlterminan en git. Guárdalas en.envo en Docker secrets. - No usar healthchecks: la app arranca antes de que la base de datos esté lista y llegan los errores de conexión. Pon healthchecks en todo lo que tus servicios esperan.
- Usar Compose en producción: Compose está pensado para dev/test — la orquestación de producción de verdad significa Kubernetes, ECS o Swarm.
- No limpiar los volúmenes:
docker compose downconserva los volúmenes. Si quieres los datos fuera, esdocker compose down -v.
Lectura Adicional
- Working with multiple Compose files — Docker docs — guía oficial sobre merges, overrides y el flag
-f. - Merge and override — Compose Specification — las reglas de merge canónicas, incluidos
!resety!override. - Using profiles with Compose — Docker docs — autoarranque y servicios opcionales.
- Environment variables precedence — Docker docs — cómo encajan
.env,env_filey--env-file.
Preguntas frecuentes
¿Cómo fusiona Docker Compose los archivos?
Hace deep-merge: las listas (ports, volumes) se concatenan, los mapas (environment, labels) se fusionan por clave y ganan los valores del archivo posterior, y los escalares (image, command) se reemplazan por completo.
¿Cuál es el archivo override por defecto?
docker-compose.override.yml. Si existe en el mismo directorio, Compose lo fusiona automáticamente con docker-compose.yml al ejecutar docker compose up.
¿Puedo usar varios archivos override?
Sí. Encadena flags -f — los archivos posteriores ganan en los conflictos: docker compose -f docker-compose.yml -f docker-compose.prod.yml -f docker-compose.local.yml up. Un orden típico es base → entorno → archivo local sin trackear.
¿Qué diferencia hay entre compose.yaml y docker-compose.yml?
Compose v2 busca primero compose.yaml, luego compose.yml, docker-compose.yaml y por último docker-compose.yml, más el .override correspondiente al que encuentre. Los cuatro nombres funcionan; compose.yaml es la convención más nueva. Cualquier otro nombre solo se carga con -f explícitos.
¿Cómo elimino una clave o un servicio que define el archivo base?
Usa !reset en el override: volumes: !reset [] vacía una lista heredada y service_name: !reset null elimina un servicio completo de la configuración fusionada. Para campos de secuencia que quieras reemplazar en lugar de ampliar, !override cambia todo el atributo de una vez.
¿Cómo uso un .env distinto por entorno?
Pasa --env-file .env.prod por línea de comandos, o pon env_file: dentro del servicio para que un override lo repunte. Sin ninguna de las dos, Compose lee el .env del directorio de trabajo: un único archivo para todos los entornos.
¿Qué son los Compose profiles?
Los profiles marcan servicios como opcionales: un servicio con lista profiles solo arranca cuando pasas --profile <nombre>. Tus herramientas de debug y test pueden vivir en el mismo archivo sin levantarse cada vez que corres el stack.
¿Cómo paso variables de entorno?
Deja un .env junto a tu compose file: Compose lo detecta solo. Después referencia las variables con ${VAR_NAME} dentro del YAML.
Recursos Relacionados
Docker Compose Dev/Prod: Separación de Entornos
Separa configs de Docker Compose para desarrollo y producción con overrides
RecipeImágenes de Producción Slim con Multi-Stage Docker
Cómo construir imágenes Docker de producción minimales usando multi-stage builds con distroless base images, cubriendo Go, Node.js, Python y Java con reducción de tamaño.
RecipeMontar Configs y Secrets en Pods de Kubernetes
Cómo montar ConfigMaps y Secrets en pods de Kubernetes usando env vars, volumes, projected volumes y secret management con external secrets.
RecipeCompartir Lógica de Workflows con GitHub Actions
Cómo crear y consumir reusable workflows en GitHub Actions, cubriendo inputs, secrets, jobs condicionales, matrix strategy y sharing a nivel organización.
RecipeTestear a Través de Múltiples OS y Versiones con GitHub
Cómo usar GitHub Actions matrix strategy para testear a través de múltiples sistemas operativos, versiones de lenguajes y configuraciones con include, exclude y matrices dinámicas.