Aurora Libros ja està declarada i parametritzada, però a la vida real la mateixa plataforma s'aixeca de tres maneres diferents: al portàtil de qui programa, al runner d'integració contínua i al servidor. Les diferències són poques —construir en comptes de descarregar, exposar o no certs ports, límits més alts— i la temptació és duplicar el fitxer.

Duplicar-lo és el pitjor camí: dos fitxers divergeixen en una setmana. Compose ofereix dos mecanismes complementaris per evitar-ho: perfils, que activen serveis opcionals sota demanda, i fitxers d'override, que es fusionen sobre una base comuna.

Contingut

  1. Perfils: serveis que no sempre vols
  2. Perfils i dependències: les regles
  3. L'override automàtic: compose.override.yaml
  4. Overrides explícits amb diversos -f
  5. Les regles de fusió
  6. !reset i !override: substituir en comptes de fusionar
  7. L'estratègia d'Aurora Libros: base, desenvolupament i producció
  8. extends: reutilitzar definicions entre projectes
  9. include:: compondre fitxers de diversos equips
  10. Convencions de noms de fitxer i de projecte

  1. Perfils: serveis que no sempre vols

Un servei amb la clau profiles: no s'aixeca llevat que el seu perfil estigui actiu. És la manera de tenir eines opcionals al mateix fitxer sense que molestin.

  adminer:
    image: adminer:5
    profiles: [eines]
    ports:
      - "${ADMINER_PORT:-8081}:8080"
    environment:
      ADMINER_DEFAULT_SERVER: aurora-db
    depends_on:
      aurora-db: { condition: service_healthy }
    networks: [frontal, posterior]

  mailhog:
    image: mailhog/mailhog:v1.0.1
    profiles: [eines]
    ports:
      - "8025:8025"     # interfície web per llegir els correus capturats
    networks: [frontal]

  llavors:
    image: auroralibros/aurora-api:${AURORA_API_VERSION:-1.2.0}
    profiles: [dades]
    command: ["node", "llavors.js", "--cataleg-demo"]
    environment:
      DB_HOST: aurora-db
      DB_USER: ${DB_USUARI:-aurora}
      DB_NAME: ${DB_NOM:-aurora_llibres}
    depends_on:
      aurora-db: { condition: service_healthy }
    restart: "no"
    networks: [posterior]

Un servei pot pertànyer a diversos perfils (profiles: [eines, ci]), i s'activa si algun d'ells està actiu.

docker compose up -d                                   # només els 5 serveis base
docker compose --profile eines up -d                   # base + adminer + mailhog
docker compose --profile eines --profile dades up -d   # tot
COMPOSE_PROFILES=eines,dades docker compose up -d      # equivalent
docker compose --profile "*" up -d                     # tots els perfils
docker compose ps --services
docker compose --profile eines ps --services
aurora-api
aurora-cache
aurora-db
aurora-migracions
aurora-web
(amb el perfil eines)
adminer
aurora-api
aurora-cache
aurora-db
aurora-migracions
aurora-web
mailhog

Amb Adminer aixecat, http://localhost:8081 et dóna una interfície web per explorar el catàleg sense instal·lar res a la teva màquina. I COMPOSE_PROFILES=eines al teu .env local activa el perfil permanentment per a tu, sense afectar ningú més.

Un ús molt pràctic d'un perfil per a tasques puntuals:

docker compose --profile dades run --rm llavors

  1. Perfils i dependències: les regles

Aquí hi ha tres comportaments que convé tenir clars, perquè són font de sorpreses:

Situació Què fa Compose
Servei amb perfil, perfil inactiu No es crea, ni tan sols si un altre el té a depends_on
Servei sense perfil que depèn d'un amb perfil Error: depends_on a un servei no habilitat
Servei amb perfil que depèn d'un sense perfil Correcte: la dependència s'aixeca automàticament
S'anomena el servei explícitament (up adminer) El seu perfil s'activa sol, sense --profile
down sense --profile No atura els serveis amb perfil actiu

Les dues files importants: anomenar un servei amb perfil l'activa implícitament —docker compose up -d adminer funciona sense més—, i docker compose down deixa orfes els serveis de perfil si no repeteixes el perfil. És la causa número u de "he fet down i encara hi ha contenidors".

docker compose --profile eines down           # correcte
docker compose down --remove-orphans          # alternativa contundent

Regla de disseny: els serveis base mai no han de dependre d'un servei amb perfil. La dependència va sempre en la direcció contrària.

  1. L'override automàtic: compose.override.yaml

Si existeix un fitxer anomenat compose.override.yaml (o .yml) al costat del compose.yaml, Compose el carrega i el fusiona automàticament, sense que hagis d'indicar res.

docker compose up -d
# equival exactament a:
docker compose -f compose.yaml -f compose.override.yaml up -d

És el mecanisme perfecte per a l'entorn de desenvolupament: el compose.yaml descriu la plataforma de manera neutra i l'override hi afegeix el que només té sentit a la teva màquina. I com que l'override s'ignora al servidor —on es fan servir altres fitxers explícits—, no hi ha risc que un port de depuració acabi en producció.

Quan passis fitxers amb -f, l'override automàtic deixa de carregar-se: manes tu.

  1. Overrides explícits amb diversos -f

docker compose -f compose.yaml -f compose.prod.yaml up -d

Compose llegeix els fitxers en l'ordre indicat i fusiona cadascun sobre el resultat acumulat: l'últim guanya. L'ordre importa i és una font clàssica d'errors; si inverteixes els fitxers, el fitxer base sobreescriu l'específic.

Les rutes relatives es resolen respecte al directori del primer fitxer, llevat que facis servir --project-directory. Si els teus overrides viuen en un subdirectori, tingues-ho present:

docker compose -f compose.yaml -f entorns/compose.prod.yaml --project-directory . up -d

  1. Les regles de fusió

El que passa en fusionar depèn del tipus de cada clau:

Tipus de clau Exemples Comportament
Escalar image, restart, user, container_name Substitueix: guanya l'últim fitxer
Mapa environment (forma mapa), labels, deploy, healthcheck Es fusionen clau a clau: guanya l'últim a les coincidents, es conserven les altres
Llista ports, volumes, dns, env_file, networks Es concatenen: apareixen els elements de tots dos
Llista tractada com a bloc command, entrypoint, healthcheck.test Substitueix sencera: no es concatena
environment en forma de llista - CLAU=valor Es fusiona per nom de variable, no es duplica

La distinció entre les dues primeres files i la tercera explica el 90 % de les sorpreses. Un exemple:

# compose.yaml
  aurora-api:
    image: auroralibros/aurora-api:1.2.0
    environment:
      NODE_ENV: production
      LOG_NIVELL: info
    ports:
      - "3000:3000"
# compose.override.yaml
  aurora-api:
    environment:
      LOG_NIVELL: debug
    ports:
      - "9229:9229"
docker compose config | grep -A6 "aurora-api:"
    environment:
      LOG_NIVELL: debug
      NODE_ENV: production
    ports:
      - "3000:3000"
      - "9229:9229"

environment s'ha fusionat (NODE_ENV sobreviu, LOG_NIVELL se substitueix) i ports s'ha concatenat (els dos ports). Que les llistes es concatenin és còmode per afegir, però significa que no en pots treure un port o un volum des d'un override... llevat del que ve ara.

  1. !reset i !override: substituir en comptes de fusionar

Compose v2.24 va introduir dues etiquetes YAML que resolen justament aquest problema:

# compose.prod.yaml
services:
  aurora-api:
    ports: !reset []          # elimina TOTS els ports heretats
    volumes: !override        # substitueix la llista en comptes de concatenar-la
      - aurora-logs:/app/logs

!reset buida la clau heretada (i amb null l'elimina per complet), i !override substitueix el valor en comptes de fusionar-lo. Són l'única manera neta de treure en un override el que la base hi afegeix, i eviten l'antic apedaçament de mantenir dos fitxers base gairebé idèntics.

  1. L'estratègia d'Aurora Libros: base, desenvolupament i producció

L'estructura recomanada són tres fitxers amb responsabilitats ben separades:

Fitxer Contingut Quan es fa servir
compose.yaml La plataforma neutra: serveis, xarxes, volums, dependències, sondes Sempre
compose.override.yaml Comoditats de desenvolupament Automàtic en local
compose.prod.yaml Enduriment i ajustos de servidor Explícit amb -f

El base és el de la lliçó 04-04, amb una regla afegida: res específic d'un entorn. Ni build, ni ports de depuració, ni NODE_ENV fixat a mà.

# compose.override.yaml — desenvolupament local (automàtic)
services:

  aurora-api:
    build:                       # construir des del codi, no descarregar
      context: ./api
      dockerfile: Dockerfile
    environment:
      NODE_ENV: development
      LOG_NIVELL: debug
    ports:
      - "3000:3000"              # atacar l'API directament
      - "9229:9229"              # inspector de Node (lliçó 04-07)
    volumes:
      - ./api/src:/app/src       # codi muntat des del host
    restart: "no"                # que una fallada no s'amagui en un bucle

  aurora-db:
    ports:
      - "127.0.0.1:5432:5432"    # psql des del host
    networks: [posterior, frontal] # necessària per poder publicar el port

  adminer:
    profiles: [eines]
    image: adminer:5
    ports: ["8081:8080"]
    environment: { ADMINER_DEFAULT_SERVER: aurora-db }
    networks: [frontal, posterior]

El detall dels bind mounts de codi i de la recàrrega automàtica és la lliçó 04-07; aquí només interessa on viu aquesta configuració: a l'override, mai a la base.

# compose.prod.yaml — servidor
services:

  aurora-api:
    image: auroralibros/aurora-api:${AURORA_API_VERSION:?fixa la versió a desplegar}
    build: !reset null           # al servidor no es construeix: només es descarrega
    environment:
      NODE_ENV: production
      LOG_NIVELL: warn
    ports: !reset []             # sense accés directe: tot passa pel 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" } }

Fixa't en tres decisions: la versió de la imatge és obligatòria (:?), perquè desplegar latest és desplegar qualsevol cosa; build: !reset null garanteix que el servidor no construeixi mai; i ports: !reset [] elimina el 3000 heretat, no l'hi afegeix.

# Desenvolupament: l'override es carrega sol
docker compose up -d --build

# Producció
docker compose -f compose.yaml -f compose.prod.yaml -p aurora-prod up -d --wait

# CI: ni override de desenvolupament ni ajustos de servidor
docker compose -f compose.yaml -f compose.ci.yaml up -d --wait

Verifica sempre un desplegament abans d'executar-lo:

docker compose -f compose.yaml -f compose.prod.yaml config | grep -E "image:|restart:|memory:"
    image: auroralibros/aurora-api:1.2.0
    restart: always
      memory: "536870912"

  1. extends: reutilitzar definicions entre projectes

Mentre que els overrides fusionen fitxers complets, extends importa un servei concret, fins i tot des d'un altre fitxer o projecte:

# comuns/serveis-base.yaml
services:
  node-base:
    image: node:22-alpine
    working_dir: /app
    user: "1001:1001"
    init: true
    restart: unless-stopped
# compose.yaml
services:
  aurora-api:
    extends:
      file: comuns/serveis-base.yaml
      service: node-base
    image: auroralibros/aurora-api:1.2.0    # el que és local guanya
    environment:
      PORT: "3000"
Avantatge Limitació
Comparteix definicions entre projectes diferents No importa depends_on, volumes_from ni links
No exigeix ordre de -f Només un extends per servei, encara que es pot encadenar
Documenta explícitament l'origen Les rutes relatives es resolen des del fitxer estès

La limitació de depends_on és deliberada: una dependència només té sentit dins del projecte que la defineix. Fes servir extends per a plantilles de configuració (imatge base, usuari, política de reinici) i àncores YAML per a la repetició dins del mateix fitxer.

  1. include:: compondre fitxers de diversos equips

include: va un pas més enllà: incorpora fitxers de Compose complets, amb els seus serveis, xarxes i volums, com si estiguessin escrits al teu.

# compose.yaml
include:
  - path: ../plataforma-comuna/compose.observabilitat.yaml
  - path: ./pagaments/compose.yaml
    env_file: ./pagaments/.env          # cada fitxer inclòs resol LES SEVES variables
    project_directory: ./pagaments      # i les seves rutes relatives

services:
  aurora-api:
    depends_on:
      - collector-metriques              # servei definit al fitxer inclòs

La diferència amb -f és important: amb diversos -f els fitxers es fusionen (s'espera que parlin dels mateixos serveis), mentre que include agrega fitxers independents que aporten serveis propis. Cada fitxer inclòs manté el seu propi context de rutes i de variables, la qual cosa permet que l'equip de pagaments mantingui el seu compose.yaml sense coordinar-se amb el de plataforma. A canvi, els noms de servei han de ser únics en el conjunt.

  1. Convencions de noms de fitxer i de projecte

Fitxer Ús
compose.yaml Base neutra. Sempre
compose.override.yaml Desenvolupament local. Automàtic
compose.prod.yaml Servidor
compose.ci.yaml Integració contínua
compose.proves.yaml Proves d'integració amb dades efímeres

I el nom de projecte per entorn, perquè dues piles convisquin a la mateixa màquina sense trepitjar-se:

docker compose -p aurora-dev up -d
docker compose -f compose.yaml -f compose.prod.yaml -p aurora-prod up -d
docker compose ls
NAME          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.yaml

Dues piles completes, amb contenidors, xarxes i volums prefixats de manera diferent, sense compartir ni una sola dada. L'única cosa que continua sent global de la màquina són els ports publicats: per això l'override de desenvolupament fa servir el 8080 i el de producció, el 80.

Per no teclejar els -f cent vegades al dia, fixa el conjunt a l'entorn:

# .env.prod (carregat amb --env-file, o exportat al servidor)
COMPOSE_FILE=compose.yaml:compose.prod.yaml
COMPOSE_PROJECT_NAME=aurora-prod

Errors Habituals i Consells

Invertir l'ordre dels -f. -f compose.prod.yaml -f compose.yaml fa que la base sobreescrigui producció. L'específic va sempre l'últim.

Esperar que un override tregui un port. Les llistes es concatenen. Per eliminar, !reset o !override.

Fer down sense repetir el perfil. Els serveis amb perfil es queden corrent. Repeteix --profile o fes servir --remove-orphans.

Posar build al fitxer base. Un servidor acabarà construint la imatge en comptes de descarregar la versió provada. build va a l'override de desenvolupament.

Desplegar sense fixar l'etiqueta de la imatge. latest en producció significa que dos servidors poden estar corrent codi diferent. Fes servir ${AURORA_API_VERSION:?...}.

Suposar que extends porta les dependències. No importa depends_on: cal tornar-lo a declarar al servei que estén.

Consell: abans de qualsevol desplegament, docker compose -f ... config i llegeix-lo. Trenta segons de lectura eviten desplegar un port de depuració obert a Internet.

Exercicis

Exercici 1. Crea un compose.override.yaml que, per a aurora-api, canviï LOG_NIVELL a debug, hi afegeixi el port 9229 i munti ./api/src. Sense aixecar res, demostra amb docker compose config que el NODE_ENV heretat sobreviu, que hi ha dos ports i que el volum apareix al costat dels de la base.

Exercici 2. Afegeix un servei adminer sota el perfil eines i respon amb comandes: (a) apareix a docker compose ps --services sense el perfil?, (b) què passa si l'anomenes explícitament a up sense --profile?, i (c) què passa en fer docker compose down sense el perfil? Proposa la manera correcta d'abaixar-ho tot.

Exercici 3. Escriu un compose.prod.yaml que elimini per complet la publicació del port 3000 de l'API, exigeixi la variable AURORA_API_VERSION i impedeixi construir al servidor. Demostra amb config que el resultat no conté ni build ni el port 3000, i que falla de manera clara si no es defineix la versió.

Solucions

Solució 1.

# compose.override.yaml
services:
  aurora-api:
    environment:
      LOG_NIVELL: debug
    ports:
      - "9229:9229"
    volumes:
      - ./api/src:/app/src
docker compose config | sed -n '/aurora-api:/,/aurora-cache:/p' | grep -E "NODE_ENV|LOG_NIVELL|published|source"
      LOG_NIVELL: debug
      NODE_ENV: production
        published: "3000"
        published: "9229"
        source: /home/joan/aurora-libros/api/src

Es veuen les tres regles en acció: environment és un mapa, així que NODE_ENV sobreviu i només se substitueix la clau coincident; ports i volumes són llistes, així que es concatenen. Fixa't també que config ha normalitzat la ruta relativa ./api/src a ruta absoluta: aquesta normalització és justament el que fa fiable la revisió prèvia a un desplegament.

Solució 2.

# (a)
docker compose ps --services | grep adminer || echo "adminer NO està actiu"
# (b)
docker compose up -d adminer
docker compose ps --services | grep adminer
# (c)
docker compose down
docker ps --format "{{.Names}}" | grep adminer
adminer NO està actiu
adminer
aurora-libros-adminer-1

(a) Sense el perfil, el servei ni tan sols es considera. (b) Anomenar-lo l'activa implícitament: no cal --profile si el demanes pel seu nom. (c) I aquí la sorpresa: després de docker compose down, adminer continua viu, perquè down sense perfil no el contempla. És un contenidor orfe que continua ocupant el port 8081 i amb accés a la base de dades.

docker compose --profile eines down           # forma correcta
docker compose down --remove-orphans          # alternativa que ho escombra tot

Solució 3.

# compose.prod.yaml
services:
  aurora-api:
    image: auroralibros/aurora-api:${AURORA_API_VERSION:?fixa la versió a desplegar}
    build: !reset null
    ports: !reset []
    environment:
      NODE_ENV: production
      LOG_NIVELL: warn
    restart: always
AURORA_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 "sense build ni 3000"
unset AURORA_API_VERSION
docker compose -f compose.yaml -f compose.prod.yaml config --quiet; echo "codi: $?"
    image: auroralibros/aurora-api:1.2.0
error while interpolating services.aurora-api.image: required variable
AURORA_API_VERSION is missing a value: fixa la versió a desplegar
codi: 1

La imatge queda fixada a una etiqueta concreta, no apareixen ni build ni el port 3000!reset els ha eliminat, cosa que una simple redefinició no hauria aconseguit amb les llistes— i sense la variable el desplegament falla abans de tocar res, amb un missatge que diu què cal fer. Aquesta fallada primerenca és exactament el que vols en un pipeline: millor un config --quiet en vermell que un servidor servint una versió imprevista.

Conclusió

Un mateix projecte serveix ja per a tres entorns sense duplicar ni un sol servei. Els perfils et donen serveis opcionals sota demanda —adminer i mailhog sota eines, llavors sota dades—, activables amb --profile, amb COMPOSE_PROFILES o simplement anomenant el servei, amb dues regles que s'obliden: un servei base mai no ha de dependre d'un amb perfil, i down sense repetir el perfil deixa contenidors orfes corrent.

Els overrides cobreixen la resta: el compose.override.yaml automàtic per a les comoditats de desenvolupament i la combinació explícita amb diversos -f on l'últim guanya. Domines les regles de fusió —escalars que se substitueixen, mapes que es fusionen clau a clau, llistes que es concatenen i blocs com command o healthcheck.test que es reemplacen sencers— i saps sortir del carreró de les llistes amb !reset i !override, la manera neta de treure un port de depuració en producció. L'estratègia queda clara: un compose.yaml neutre sense build ni ports de depuració, un override de desenvolupament i un compose.prod.yaml amb etiqueta d'imatge obligatòria, restart: always, límits propis i logging configurat; i docker compose config llegit abans de cada desplegament.

Saps a més quan fer servir cada eina de reutilització: àncores YAML dins d'un fitxer, extends per a plantilles de servei entre projectes —recordant que no arrossega depends_on—, i include: per agregar fitxers complets que mantenen altres equips, cadascun amb el seu context de rutes i variables. I la convenció de noms de fitxer i de projecte (-p aurora-dev, -p aurora-prod) que permet que dues piles convisquin a la mateixa màquina sense compartir ni un byte, amb COMPOSE_FILE per no repetir els -f.

Queda la part que més faràs servir: el dia a dia. A la lliçó següent, Desenvolupament Local amb Docker Compose, convertiràs aquest override de desenvolupament en un entorn de treball real: bind mount del codi amb la solució a l'etern problema de node_modules, recàrrega automàtica amb --watch de Node 22 i amb el bloc natiu develop.watch de Compose, depuració pas a pas des de VS Code amb l'inspector al port 9229, proves en contenidors efímers amb la seva base de dades en tmpfs, dades de desenvolupament i reinici en una comanda, el rendiment dels bind mounts a macOS i Windows, i un Makefile amb les dreceres de l'equip.

Docker: De Principiant a Avançat

Mòdul 1: Introducció a Docker

Mòdul 2: Treballant amb Imatges Docker

Mòdul 3: Contenidors Docker

Mòdul 4: Docker Compose

Mòdul 5: Conceptes Avançats de Docker

Mòdul 6: Docker en Producció

Mòdul 7: Ecosistema i Eines de Docker

© Copyright 2026. Tots els drets reservats