Aurora Libros ya está declarada y parametrizada, pero en la vida real la misma plataforma se levanta de tres maneras distintas: en el portátil de quien programa, en el runner de integración continua y en el servidor. Las diferencias son pocas —construir en vez de descargar, exponer o no ciertos puertos, límites más altos— y la tentación es duplicar el fichero.
Duplicarlo es el peor camino: dos ficheros divergen en una semana. Compose ofrece dos mecanismos complementarios para evitarlo: perfiles, que activan servicios opcionales bajo demanda, y ficheros de override, que se fusionan sobre una base común.
Contenido
- Perfiles: servicios que no siempre quieres
- Perfiles y dependencias: las reglas
- El override automático:
compose.override.yaml - Overrides explícitos con varios
-f - Las reglas de fusión
!resety!override: sustituir en lugar de fusionar- La estrategia de Aurora Libros: base, desarrollo y producción
extends: reutilizar definiciones entre proyectosinclude:: componer ficheros de varios equipos- Convenciones de nombres de fichero y de proyecto
- Perfiles: servicios que no siempre quieres
Un servicio con la clave profiles: no se levanta salvo que su perfil esté activo. Es la forma de tener herramientas opcionales en el mismo fichero sin que molesten.
adminer:
image: adminer:5
profiles: [herramientas]
ports:
- "${ADMINER_PUERTO:-8081}:8080"
environment:
ADMINER_DEFAULT_SERVER: aurora-db
depends_on:
aurora-db: { condition: service_healthy }
networks: [frontal, trasera]
mailhog:
image: mailhog/mailhog:v1.0.1
profiles: [herramientas]
ports:
- "8025:8025" # interfaz web para leer los correos capturados
networks: [frontal]
semillas:
image: auroralibros/aurora-api:${AURORA_API_VERSION:-1.2.0}
profiles: [datos]
command: ["node", "semillas.js", "--catalogo-demo"]
environment:
DB_HOST: aurora-db
DB_USER: ${DB_USUARIO:-aurora}
DB_NAME: ${DB_NOMBRE:-aurora_libros}
depends_on:
aurora-db: { condition: service_healthy }
restart: "no"
networks: [trasera]Un servicio puede pertenecer a varios perfiles (profiles: [herramientas, ci]), y se activa si alguno de ellos está activo.
docker compose up -d # solo los 5 servicios base
docker compose --profile herramientas up -d # base + adminer + mailhog
docker compose --profile herramientas --profile datos up -d # todo
COMPOSE_PROFILES=herramientas,datos docker compose up -d # equivalente
docker compose --profile "*" up -d # todos los perfilesaurora-api
aurora-cache
aurora-db
aurora-migraciones
aurora-web
(con el perfil herramientas)
adminer
aurora-api
aurora-cache
aurora-db
aurora-migraciones
aurora-web
mailhogCon Adminer levantado, http://localhost:8081 te da una interfaz web para explorar el catálogo sin instalar nada en tu máquina. Y COMPOSE_PROFILES=herramientas en tu .env local activa el perfil permanentemente para ti, sin afectar a nadie más.
Un uso muy práctico de un perfil para tareas puntuales:
- Perfiles y dependencias: las reglas
Aquí hay tres comportamientos que conviene tener claros, porque son fuente de sorpresas:
| Situación | Qué hace Compose |
|---|---|
| Servicio con perfil, perfil inactivo | No se crea, ni siquiera si otro lo tiene en depends_on |
| Servicio sin perfil que depende de uno con perfil | Error: depends_on a un servicio no habilitado |
| Servicio con perfil que depende de uno sin perfil | Correcto: la dependencia se levanta automáticamente |
Se nombra el servicio explícitamente (up adminer) |
Su perfil se activa solo, sin --profile |
down sin --profile |
No para los servicios con perfil activo |
Las dos filas importantes: nombrar un servicio con perfil lo activa implícitamente —docker compose up -d adminer funciona sin más—, y docker compose down deja huérfanos los servicios de perfil si no repites el perfil. Es la causa número uno de "he hecho down y sigue habiendo contenedores".
docker compose --profile herramientas down # correcto
docker compose down --remove-orphans # alternativa contundenteRegla de diseño: los servicios base nunca deben depender de un servicio con perfil. La dependencia va siempre en la dirección contraria.
- El override automático:
compose.override.yaml
compose.override.yamlSi existe un fichero llamado compose.override.yaml (o .yml) junto al compose.yaml, Compose lo carga y lo fusiona automáticamente, sin que tengas que indicar nada.
docker compose up -d
# equivale exactamente a:
docker compose -f compose.yaml -f compose.override.yaml up -dEs el mecanismo perfecto para el entorno de desarrollo: el compose.yaml describe la plataforma de forma neutra y el override añade lo que solo tiene sentido en tu máquina. Y como el override se ignora en el servidor —donde se usan otros ficheros explícitos—, no hay riesgo de que un puerto de depuración acabe en producción.
Cuando pases ficheros con -f, el override automático deja de cargarse: mandas tú.
- Overrides explícitos con varios
-f
-fCompose lee los ficheros en el orden indicado y fusiona cada uno sobre el resultado acumulado: el último gana. El orden importa y es una fuente clásica de errores; si inviertes los ficheros, el fichero base sobrescribe al específico.
Las rutas relativas se resuelven respecto al directorio del primer fichero, salvo que uses --project-directory. Si tus overrides viven en un subdirectorio, tenlo presente:
- Las reglas de fusión
Lo que ocurre al fusionar depende del tipo de cada clave:
| Tipo de clave | Ejemplos | Comportamiento |
|---|---|---|
| Escalar | image, restart, user, container_name |
Sustituye: gana el último fichero |
| Mapa | environment (forma mapa), labels, deploy, healthcheck |
Se fusionan clave a clave: gana el último en las coincidentes, se conservan las demás |
| Lista | ports, volumes, dns, env_file, networks |
Se concatenan: aparecen los elementos de ambos |
| Lista tratada como bloque | command, entrypoint, healthcheck.test |
Sustituye entera: no se concatena |
environment en forma de lista |
- CLAVE=valor |
Se fusiona por nombre de variable, no se duplica |
La distinción entre las dos primeras filas y la tercera explica el 90 % de las sorpresas. Un ejemplo:
# compose.yaml
aurora-api:
image: auroralibros/aurora-api:1.2.0
environment:
NODE_ENV: production
LOG_NIVEL: info
ports:
- "3000:3000"environment se ha fusionado (NODE_ENV sobrevive, LOG_NIVEL se sustituye) y ports se ha concatenado (los dos puertos). Que las listas se concatenen es cómodo para añadir, pero significa que no puedes quitar un puerto o un volumen desde un override... salvo con lo que viene ahora.
!reset y !override: sustituir en lugar de fusionar
!reset y !override: sustituir en lugar de fusionarCompose v2.24 introdujo dos etiquetas YAML que resuelven justo ese problema:
# compose.prod.yaml
services:
aurora-api:
ports: !reset [] # elimina TODOS los puertos heredados
volumes: !override # sustituye la lista en vez de concatenarla
- aurora-logs:/app/logs!reset vacía la clave heredada (y con null la elimina por completo), y !override sustituye el valor en lugar de fusionarlo. Son la única forma limpia de quitar en un override lo que la base añade, y evitan el antiguo apaño de mantener dos ficheros base casi idénticos.
- La estrategia de Aurora Libros: base, desarrollo y producción
La estructura recomendada son tres ficheros con responsabilidades bien separadas:
| Fichero | Contenido | Cuándo se usa |
|---|---|---|
compose.yaml |
La plataforma neutra: servicios, redes, volúmenes, dependencias, sondas | Siempre |
compose.override.yaml |
Comodidades de desarrollo | Automático en local |
compose.prod.yaml |
Endurecimiento y ajustes de servidor | Explícito con -f |
El base es el de la lección 04-04, con una regla añadida: nada específico de un entorno. Ni build, ni puertos de depuración, ni NODE_ENV fijado a mano.
# compose.override.yaml — desarrollo local (automático)
services:
aurora-api:
build: # construir desde el código, no descargar
context: ./api
dockerfile: Dockerfile
environment:
NODE_ENV: development
LOG_NIVEL: debug
ports:
- "3000:3000" # atacar la API directamente
- "9229:9229" # inspector de Node (lección 04-07)
volumes:
- ./api/src:/app/src # código montado desde el host
restart: "no" # que un fallo no se esconda en un bucle
aurora-db:
ports:
- "127.0.0.1:5432:5432" # psql desde el host
networks: [trasera, frontal] # necesaria para poder publicar el puerto
adminer:
profiles: [herramientas]
image: adminer:5
ports: ["8081:8080"]
environment: { ADMINER_DEFAULT_SERVER: aurora-db }
networks: [frontal, trasera]El detalle de los bind mounts de código y de la recarga automática es la lección 04-07; aquí solo interesa dónde vive esa configuración: en el override, nunca en la base.
# compose.prod.yaml — servidor
services:
aurora-api:
image: auroralibros/aurora-api:${AURORA_API_VERSION:?fija la versión a desplegar}
build: !reset null # en el servidor no se construye: solo se descarga
environment:
NODE_ENV: production
LOG_NIVEL: warn
ports: !reset [] # sin acceso directo: todo pasa por el proxy
restart: always
deploy:
resources: { limits: { memory: 512M, cpus: "2.0" } }
logging:
driver: json-file
options: { max-size: "50m", max-file: "5" }
aurora-db:
restart: always
deploy:
resources: { limits: { memory: 2G, cpus: "2.0" }, reservations: { memory: 1G } }
aurora-web:
restart: always
ports:
- "80:80"
deploy:
resources: { limits: { memory: 256M, cpus: "1.0" } }Fíjate en tres decisiones: la versión de la imagen es obligatoria (:?), porque desplegar latest es desplegar cualquier cosa; build: !reset null garantiza que el servidor jamás construya; y ports: !reset [] elimina el 3000 heredado, no lo añade.
# Desarrollo: el override se carga solo
docker compose up -d --build
# Producción
docker compose -f compose.yaml -f compose.prod.yaml -p aurora-prod up -d --wait
# CI: ni override de desarrollo ni ajustes de servidor
docker compose -f compose.yaml -f compose.ci.yaml up -d --waitVerifica siempre un despliegue antes de ejecutarlo:
extends: reutilizar definiciones entre proyectos
extends: reutilizar definiciones entre proyectosMientras que los overrides fusionan ficheros completos, extends importa un servicio concreto, incluso desde otro fichero o proyecto:
# comunes/servicios-base.yaml
services:
nodo-base:
image: node:22-alpine
working_dir: /app
user: "1001:1001"
init: true
restart: unless-stopped# compose.yaml
services:
aurora-api:
extends:
file: comunes/servicios-base.yaml
service: nodo-base
image: auroralibros/aurora-api:1.2.0 # lo local gana
environment:
PORT: "3000"| Ventaja | Limitación |
|---|---|
| Comparte definiciones entre proyectos distintos | No importa depends_on, volumes_from ni links |
No exige orden de -f |
Solo un extends por servicio, aunque puede encadenarse |
| Documenta explícitamente el origen | Las rutas relativas se resuelven desde el fichero extendido |
La limitación de depends_on es deliberada: una dependencia solo tiene sentido dentro del proyecto que la define. Usa extends para plantillas de configuración (imagen base, usuario, política de reinicio) y anclas YAML para repetición dentro del mismo fichero.
include:: componer ficheros de varios equipos
include:: componer ficheros de varios equiposinclude: va un paso más allá: incorpora ficheros de Compose completos, con sus servicios, redes y volúmenes, como si estuvieran escritos en el tuyo.
# compose.yaml
include:
- path: ../plataforma-comun/compose.observabilidad.yaml
- path: ./pagos/compose.yaml
env_file: ./pagos/.env # cada fichero incluido resuelve SUS variables
project_directory: ./pagos # y sus rutas relativas
services:
aurora-api:
depends_on:
- coleccionista-metricas # servicio definido en el fichero incluidoLa diferencia con -f es importante: con varios -f los ficheros se fusionan (se espera que hablen de los mismos servicios), mientras que include agrega ficheros independientes que aportan servicios propios. Cada fichero incluido mantiene su propio contexto de rutas y de variables, lo que permite que el equipo de pagos mantenga su compose.yaml sin coordinarse con el de plataforma. A cambio, los nombres de servicio deben ser únicos en el conjunto.
- Convenciones de nombres de fichero y de proyecto
| Fichero | Uso |
|---|---|
compose.yaml |
Base neutra. Siempre |
compose.override.yaml |
Desarrollo local. Automático |
compose.prod.yaml |
Servidor |
compose.ci.yaml |
Integración continua |
compose.pruebas.yaml |
Pruebas de integración con datos efímeros |
Y el nombre de proyecto por entorno, para que dos pilas convivan en la misma máquina sin pisarse:
docker compose -p aurora-dev up -d
docker compose -f compose.yaml -f compose.prod.yaml -p aurora-prod up -d
docker compose lsNAME STATUS CONFIG FILES
aurora-dev running(5) /home/joan/aurora-libros/compose.yaml,...override.yaml
aurora-prod running(5) /home/joan/aurora-libros/compose.yaml,...prod.yamlDos pilas completas, con contenedores, redes y volúmenes prefijados de forma distinta, sin compartir un solo dato. Lo único que sigue siendo global de la máquina son los puertos publicados: por eso el override de desarrollo usa 8080 y el de producción, 80.
Para no teclear los -f cien veces al día, fija el conjunto en el entorno:
# .env.prod (cargado con --env-file, o exportado en el servidor)
COMPOSE_FILE=compose.yaml:compose.prod.yaml
COMPOSE_PROJECT_NAME=aurora-prodErrores Comunes y Consejos
Invertir el orden de los -f. -f compose.prod.yaml -f compose.yaml hace que la base sobrescriba a producción. El específico va siempre el último.
Esperar que un override quite un puerto. Las listas se concatenan. Para eliminar, !reset o !override.
Hacer down sin repetir el perfil. Los servicios con perfil se quedan corriendo. Repite --profile o usa --remove-orphans.
Poner build en el fichero base. Un servidor acabará construyendo la imagen en vez de descargar la versión probada. build va en el override de desarrollo.
Desplegar sin fijar la etiqueta de la imagen. latest en producción significa que dos servidores pueden estar corriendo código distinto. Usa ${AURORA_API_VERSION:?...}.
Suponer que extends trae las dependencias. No importa depends_on: hay que redeclararlo en el servicio que extiende.
Consejo: antes de cualquier despliegue, docker compose -f ... config y léelo. Treinta segundos de lectura evitan desplegar un puerto de depuración abierto a Internet.
Ejercicios
Ejercicio 1. Crea un compose.override.yaml que, para aurora-api, cambie LOG_NIVEL a debug, añada el puerto 9229 y monte ./api/src. Sin levantar nada, demuestra con docker compose config que NODE_ENV heredado sobrevive, que hay dos puertos y que el volumen aparece junto a los de la base.
Ejercicio 2. Añade un servicio adminer bajo el perfil herramientas y responde con comandos: (a) ¿aparece en docker compose ps --services sin el perfil?, (b) ¿qué pasa si lo nombras explícitamente en up sin --profile?, y (c) ¿qué ocurre al hacer docker compose down sin el perfil? Propón la forma correcta de bajar todo.
Ejercicio 3. Escribe un compose.prod.yaml que elimine por completo la publicación del puerto 3000 de la API, exija la variable AURORA_API_VERSION e impida construir en el servidor. Demuestra con config que el resultado no contiene ni build ni el puerto 3000, y que falla de forma clara si no se define la versión.
Soluciones
Solución 1.
# compose.override.yaml
services:
aurora-api:
environment:
LOG_NIVEL: debug
ports:
- "9229:9229"
volumes:
- ./api/src:/app/srcdocker compose config | sed -n '/aurora-api:/,/aurora-cache:/p' | grep -E "NODE_ENV|LOG_NIVEL|published|source" LOG_NIVEL: debug
NODE_ENV: production
published: "3000"
published: "9229"
source: /home/joan/aurora-libros/api/srcSe ven las tres reglas en acción: environment es un mapa, así que NODE_ENV sobrevive y solo se sustituye la clave coincidente; ports y volumes son listas, así que se concatenan. Fíjate también en que config ha normalizado la ruta relativa ./api/src a ruta absoluta: esa normalización es justo lo que hace fiable la revisión previa a un despliegue.
Solución 2.
# (a)
docker compose ps --services | grep adminer || echo "adminer NO está activo"
# (b)
docker compose up -d adminer
docker compose ps --services | grep adminer
# (c)
docker compose down
docker ps --format "{{.Names}}" | grep adminer(a) Sin el perfil, el servicio ni siquiera se considera. (b) Nombrarlo lo activa implícitamente: no hace falta --profile si lo pides por su nombre. (c) Y aquí la sorpresa: tras docker compose down, adminer sigue vivo, porque down sin perfil no lo contempla. Es un contenedor huérfano que sigue ocupando el puerto 8081 y con acceso a la base de datos.
docker compose --profile herramientas down # forma correcta
docker compose down --remove-orphans # alternativa que barre todoSolución 3.
# compose.prod.yaml
services:
aurora-api:
image: auroralibros/aurora-api:${AURORA_API_VERSION:?fija la versión a desplegar}
build: !reset null
ports: !reset []
environment:
NODE_ENV: production
LOG_NIVEL: warn
restart: alwaysAURORA_API_VERSION=1.2.0 docker compose -f compose.yaml -f compose.prod.yaml config \
| sed -n '/aurora-api:/,/aurora-cache:/p' | grep -E "image:|build|3000" || echo "sin build ni 3000"
unset AURORA_API_VERSION
docker compose -f compose.yaml -f compose.prod.yaml config --quiet; echo "código: $?" image: auroralibros/aurora-api:1.2.0
error while interpolating services.aurora-api.image: required variable
AURORA_API_VERSION is missing a value: fija la versión a desplegar
código: 1La imagen queda fijada a una etiqueta concreta, no aparecen ni build ni el puerto 3000 —!reset los ha eliminado, cosa que una simple redefinición no habría conseguido con las listas— y sin la variable el despliegue falla antes de tocar nada, con un mensaje que dice qué hacer. Ese fallo temprano es exactamente lo que quieres en un pipeline: mejor un config --quiet en rojo que un servidor sirviendo una versión imprevista.
Conclusión
Un mismo proyecto sirve ya para tres entornos sin duplicar un solo servicio. Los perfiles te dan servicios opcionales bajo demanda —adminer y mailhog bajo herramientas, semillas bajo datos—, activables con --profile, con COMPOSE_PROFILES o simplemente nombrando el servicio, con dos reglas que se olvidan: un servicio base nunca debe depender de uno con perfil, y down sin repetir el perfil deja contenedores huérfanos corriendo.
Los overrides cubren el resto: el compose.override.yaml automático para las comodidades de desarrollo y la combinación explícita con varios -f donde el último gana. Dominas las reglas de fusión —escalares que se sustituyen, mapas que se fusionan clave a clave, listas que se concatenan y bloques como command o healthcheck.test que se reemplazan enteros— y sabes salir del callejón de las listas con !reset y !override, la forma limpia de quitar un puerto de depuración en producción. La estrategia queda clara: un compose.yaml neutro sin build ni puertos de depuración, un override de desarrollo y un compose.prod.yaml con etiqueta de imagen obligatoria, restart: always, límites propios y logging configurado; y docker compose config leído antes de cada despliegue.
Sabes además cuándo usar cada herramienta de reutilización: anclas YAML dentro de un fichero, extends para plantillas de servicio entre proyectos —recordando que no arrastra depends_on—, e include: para agregar ficheros completos que mantienen otros equipos, cada uno con su contexto de rutas y variables. Y la convención de nombres de fichero y de proyecto (-p aurora-dev, -p aurora-prod) que permite que dos pilas convivan en la misma máquina sin compartir ni un byte, con COMPOSE_FILE para no repetir los -f.
Queda la parte que más vas a usar: el día a día. En la lección siguiente, Desarrollo Local con Docker Compose, convertirás ese override de desarrollo en un entorno de trabajo real: bind mount del código con la solución al eterno problema de node_modules, recarga automática con --watch de Node 22 y con el bloque nativo develop.watch de Compose, depuración paso a paso desde VS Code con el inspector en el puerto 9229, pruebas en contenedores efímeros con su base de datos en tmpfs, datos de desarrollo y reseteo en un comando, el rendimiento de los bind mounts en macOS y Windows, y un Makefile con los atajos del equipo.
Docker: De Principiante a Avanzado
Módulo 1: Introducción a Docker
- ¿Qué es Docker?
- Instalando Docker
- Arquitectura de Docker
- Comandos Básicos de Docker
- Entendiendo las Imágenes de Docker
- Creando tu Primer Contenedor Docker
- El Proyecto del Curso: la Plataforma Aurora Libros
Módulo 2: Trabajando con Imágenes Docker
- Docker Hub y Repositorios
- Construyendo Imágenes Docker
- Conceptos Básicos de Dockerfile
- Instrucciones Avanzadas del Dockerfile
- Gestionando Imágenes Docker
- Etiquetado y Publicación de Imágenes
Módulo 3: Contenedores Docker
- Ejecutando Contenedores
- Ciclo de Vida del Contenedor
- Gestionando Contenedores
- Inspección y Depuración de Contenedores
- Redes en Docker
- Persistencia de Datos con Volúmenes
- Límites de Recursos y Políticas de Reinicio
Módulo 4: Docker Compose
- Introducción a Docker Compose
- Definiendo Servicios en Docker Compose
- Comandos de Docker Compose
- Aplicaciones Multi-Contenedor
- Variables de Entorno en Docker Compose
- Perfiles, Overrides y Múltiples Entornos
- Desarrollo Local con Docker Compose
Módulo 5: Conceptos Avanzados de Docker
- Profundización en Redes Docker
- Opciones de Almacenamiento Docker
- Mejores Prácticas de Seguridad en Docker
- Optimizando Imágenes Docker
- Builds Avanzadas con BuildKit y Buildx
- Registro y Monitoreo en Docker
- El Runtime por Dentro: Namespaces, Cgroups y Capas
Módulo 6: Docker en Producción
- Preparar una Imagen para Producción
- CI/CD con Docker
- Orquestando Contenedores con Docker Swarm
- Introducción a Kubernetes
- Desplegando Contenedores Docker en Kubernetes
- Escalado y Balanceo de Carga
- Estrategias de Despliegue y Rollback
