El compose.yaml d'Aurora Libros funciona, però té la contrasenya de PostgreSQL escrita en clar, la versió de la imatge fixada a mà i el port 8080 incrustat. Així no serveix per a més d'un entorn ni es pot pujar a un repositori compartit.

Aquest és el tema on més gent es perd, i per una raó concreta: hi ha dos sistemes diferents que s'assemblen i no tenen res a veure. Un actua sobre el fitxer YAML abans d'arrencar res; l'altre defineix el que veu el procés dins del contenidor. Separa'ls des de la primera línia i tota la resta encaixa.

Contingut

  1. Els dos sistemes, separats des del principi
  2. Sistema A: substitució de variables al YAML
  3. D'on treu Compose els valors per substituir
  4. Sistema B: variables dins del contenidor
  5. La taula de precedència completa
  6. El fitxer .env del projecte
  7. Aurora Libros parametritzada: .env i .env.example
  8. Gestió de secrets: environment no és un lloc per a contrasenyes
  9. Depuració amb docker compose config

  1. Els dos sistemes, separats des del principi

Sistema A: substitució Sistema B: entorn del contenidor
Sintaxi ${VARIABLE} a qualsevol part del YAML Claus environment: i env_file:
Quan actua Abans de crear res, en llegir el fitxer En crear el contenidor
Qui el processa El procés docker compose a la teva màquina El dimoni Docker, dins del contenidor
Per a què serveix Parametritzar el mateix fitxer: versions, ports, rutes Configurar l'aplicació: DB_HOST, PORT...
Font principal El fitxer .env del projecte i el teu shell El que escriguis a environment / env_file
El veu el procés? No, llevat que a més el passis pel sistema B Sí, és el seu entorn
graph LR
    E[".env del projecte"] --> S
    SH["Entorn del shell"] --> S
    CLI["--env-file"] --> S
    S["SUBSTITUCIÓ<br/>Compose resol els ${VAR}"] --> Y["compose.yaml resolt<br/>(docker compose config)"]
    Y --> D["Docker crea el contenidor"]
    EN["environment:"] --> D
    EF["env_file:"] --> D
    IMG["ENV del Dockerfile"] --> D
    D --> P["Entorn del procés<br/>dins del contenidor"]

La confusió clàssica: algú escriu DB_PASSWORD=secreta al .env i creu que l'aplicació la veurà. No la veurà. El .env alimenta la substitució; perquè arribi al contenidor cal, a més, un environment: { DB_PASSWORD: ${DB_PASSWORD} } o un env_file.

  1. Sistema A: substitució de variables al YAML

Qualsevol ${VARIABLE} o $VARIABLE del fitxer se substitueix abans que Docker vegi res. Serveix en qualsevol posició: noms d'imatge, ports, rutes, límits.

services:
  aurora-api:
    image: auroralibros/aurora-api:${AURORA_API_VERSION}
    ports:
      - "${API_PORT}:3000"

Fes servir sempre les claus, ${VAR}: sense elles, $VARIABLE_llarga es pot retallar de manera inesperada en tocar un caràcter no alfanumèric.

Els modificadors són la part que de debò marca la diferència entre un fitxer fràgil i un de robust:

Sintaxi Si la variable no està definida Si està definida però buida
${VAR} Cadena buida (silenciós) Cadena buida
${VAR:-defecte} defecte defecte
${VAR-defecte} defecte Cadena buida
${VAR:?missatge} Error i avorta Error i avorta
${VAR?missatge} Error i avorta Cadena buida

Els dos punts signifiquen "tracta el valor buit igual que l'absent". A la pràctica: :- per a allò que tingui un valor per defecte sensat i :? per a allò que no pot faltar.

    image: auroralibros/aurora-api:${AURORA_API_VERSION:-1.2.0}
    ports:
      - "${WEB_PORT:-8080}:80"
    environment:
      DB_PASSWORD: ${DB_PASSWORD:?falta DB_PASSWORD; copia .env.example a .env}
docker compose config --quiet
error while interpolating services.aurora-db.environment.POSTGRES_PASSWORD:
required variable DB_PASSWORD is missing a value: falta DB_PASSWORD; copia .env.example a .env

Un error immediat i amb instruccions, en comptes d'una base de dades que arrenca amb contrasenya buida. Aquesta és la diferència entre ${VAR} i ${VAR:?...}, i val la pena aplicar-la a totes les credencials.

Quan necessitis un $ literal —molt habitual a command de shell o en patrons de nginx— duplica'l:

    command: sh -c 'echo "PID actual: $$$$"; exec node server.js'
    environment:
      PLANTILLA: "Hola $${NOM}"        # arriba al contenidor com: Hola ${NOM}

Un sol $ el consumeix Compose; $$ produeix un $ literal a la sortida. Si veus errors del tipus Invalid interpolation format, gairebé sempre és un $ sense escapar.

  1. D'on treu Compose els valors per substituir

Per resoldre un ${VAR}, Compose busca en aquest ordre, i el primer que trobi guanya:

Ordre Font
1 Variable passada a la mateixa línia: AURORA_API_VERSION=1.3.0 docker compose up -d
2 Variable exportada al teu shell (export AURORA_API_VERSION=1.3.0)
3 Fitxer indicat amb --env-file
4 Fitxer .env del directori del projecte
5 Valor per defecte del mateix ${VAR:-...}

Que el shell guanyi al .env és deliberat i molt pràctic: permet a un pipeline de CI sobreescriure la versió de la imatge sense tocar cap fitxer.

AURORA_API_VERSION=1.3.0-rc1 docker compose up -d

  1. Sistema B: variables dins del contenidor

Forma de mapa (recomanada: es llegeix millor i es fusiona bé entre fitxers d'override):

    environment:
      PORT: "3000"
      DB_HOST: aurora-db
      NODE_ENV: production

Forma de llista, amb una capacitat que la de mapa no té:

    environment:
      - PORT=3000
      - DB_HOST=aurora-db
      - HTTP_PROXY              # SENSE valor: pren el del shell del host (pas a través)

Aquesta última línia és el pass-through: si HTTP_PROXY existeix al teu shell, es passa al contenidor amb el seu valor; si no existeix, la variable no es defineix. Útil per a proxies corporatius i credencials temporals que no vols escriure en cap fitxer.

env_file carrega variables des de fitxers, en l'ordre donat i guanyant l'últim:

    env_file:
      - ./config/comuns.env
      - path: ./config/local.env
        required: false        # si no existeix, no falla (forma llarga)
Aspecte environment env_file
On és el valor Al compose.yaml En un fitxer a part
S'hi interpola ${...}? També, des de Compose v2.24
Va a Git? Sí, amb el fitxer Normalment no
Precedència Major Menor
Bon ús Valors no sensibles i estructurals Moltes variables o valors per entorn

Sintaxi d'un fitxer .env/env_file: una CLAU=valor per línia, # per als comentaris, sense espais al voltant de l'=, sense export, i les cometes es conserven com a part del valor llevat que envoltin tota la cadena. DB_PASSWORD="secreta" i DB_PASSWORD=secreta donen el mateix resultat; DB_PASSWORD=se creta també funciona, perquè no cal posar els espais entre cometes.

  1. La taula de precedència completa

Quan una mateixa variable apareix en diversos llocs, aquest és l'ordre que decideix què veu el procés, de major a menor prioritat:

# Origen Exemple
1 docker compose run -e / exec -e docker compose run -e NODE_ENV=test aurora-api npm test
2 environment sense valor (pas a través del shell) - NODE_ENV amb export NODE_ENV=debug al shell
3 environment amb valor environment: { NODE_ENV: production }
4 --env-file indicat a la CLI --env-file .env.prod
5 env_file declarat al servei env_file: [./config/api.env]
6 ENV del Dockerfile de la imatge ENV NODE_ENV=production

I una precisió que estalvia hores perdudes: el .env del projecte no apareix en aquesta taula. No injecta res als contenidors; només alimenta la substitució del sistema A. Si vols que una variable del .env arribi al procés, l'has de passar explícitament.

# Comprovació empírica de la precedència
docker compose exec aurora-api env | sort | grep -E "^(NODE_ENV|DB_HOST|PORT)="
docker compose run --rm -e NODE_ENV=test aurora-api env | grep NODE_ENV
DB_HOST=aurora-db
NODE_ENV=production
PORT=3000
NODE_ENV=test

  1. El fitxer .env del projecte

És un fitxer anomenat exactament .env, situat al directori del projecte (el del compose.yaml, o el que indiquis amb --project-directory). Compose el carrega automàticament i el fa servir només per a la interpolació.

# .env — valors locals d'Aurora Libros. NO es puja a Git.
AURORA_API_VERSION=1.2.0
WEB_PORT=8080
DB_USUARI=aurora
DB_PASSWORD=aurora_secreta
DB_NOM=aurora_llibres
COMPOSE_PROJECT_NAME=aurora-libros

Fixa't en l'última línia: al .env també pots fixar les variables de configuració de la mateixa CLICOMPOSE_PROJECT_NAME, COMPOSE_FILE, COMPOSE_PROFILES—, que afecten el comportament de Compose i no els contenidors.

Amb --env-file pots fer servir un altre fitxer, o diversos:

docker compose --env-file .env.produccio config
docker compose --env-file .env --env-file .env.local up -d   # l'últim guanya

  1. Aurora Libros parametritzada: .env i .env.example

Aplica el sistema A a les tres coses que canvien entre entorns: versió de la imatge, ports publicats i credencials.

services:
  aurora-db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: ${DB_USUARI:-aurora}
      POSTGRES_PASSWORD: ${DB_PASSWORD:?defineix DB_PASSWORD al teu fitxer .env}
      POSTGRES_DB: ${DB_NOM:-aurora_llibres}

  aurora-api:
    image: auroralibros/aurora-api:${AURORA_API_VERSION:-1.2.0}
    environment:
      PORT: "3000"
      DB_HOST: aurora-db
      DB_USER: ${DB_USUARI:-aurora}
      DB_PASSWORD: ${DB_PASSWORD:?defineix DB_PASSWORD al teu fitxer .env}
      DB_NAME: ${DB_NOM:-aurora_llibres}
      REDIS_HOST: aurora-cache
      NODE_ENV: ${NODE_ENV:-production}
      LOG_NIVELL: ${LOG_NIVELL:-info}

  aurora-web:
    image: nginx:alpine
    ports:
      - "${WEB_PORT:-8080}:80"

Ara, la peça cultural que converteix això en una cosa usable per un equip: el .env no es versiona, però la seva plantilla sí.

# .env.example — plantilla versionada a Git.
# Copia-la a .env i omple els valors locals:  cp .env.example .env
AURORA_API_VERSION=1.2.0
WEB_PORT=8080
DB_USUARI=aurora
DB_NOM=aurora_llibres
NODE_ENV=production
LOG_NIVELL=info

# Obligatòria i sense valor per defecte: cadascú hi posa la seva en local.
DB_PASSWORD=
# .gitignore
.env
.env.local
.env.*.local
secrets/
git add .env.example .gitignore compose.yaml
git status --short
A  .env.example
A  .gitignore
A  compose.yaml

El .env real no hi apareix: està ignorat. Amb això, qui clona el repositori executa cp .env.example .env, escriu la seva contrasenya i aixeca la plataforma. I si se n'oblida, no obté una fallada críptica, sinó el missatge de ${DB_PASSWORD:?...} dient-li exactament què ha de fer.

  1. Gestió de secrets: environment no és un lloc per a contrasenyes

Tot l'anterior parametritza bé, però no protegeix res. Una contrasenya a environment és visible per a qualsevol que tingui accés al dimoni Docker:

docker inspect aurora-libros-aurora-db-1 --format '{{range .Config.Env}}{{println .}}{{end}}' | grep PASS
docker compose exec aurora-api env | grep PASSWORD
docker compose exec aurora-api cat /proc/1/environ | tr '\0' '\n' | grep PASS
POSTGRES_PASSWORD=aurora_secreta
DB_PASSWORD=aurora_secreta
DB_PASSWORD=aurora_secreta

Tres vies diferents, la mateixa contrasenya en clar. I n'hi ha més: les variables d'entorn acaben als bolcats d'estat del contenidor, s'hereten a tots els processos fills (inclosa qualsevol dependència de tercers que decideixi enviar-les en un informe d'errors) i s'escolen als registres d'auditoria.

L'alternativa de Compose és el bloc secrets:, que munta cada secret com un fitxer dins de /run/secrets/:

secrets:
  db_password:
    file: ./secrets/db_password.txt      # el fitxer viu fora de Git

services:
  aurora-db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: aurora
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password   # la RUTA, no el valor
      POSTGRES_DB: aurora_llibres
    secrets:
      - db_password

  aurora-api:
    environment:
      DB_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - source: db_password
        target: db_password        # ruta final: /run/secrets/db_password
        mode: 0400                 # només lectura per al propietari

Les imatges oficials de PostgreSQL, MySQL i moltes altres admeten el sufix _FILE a les seves variables: si defineixes POSTGRES_PASSWORD_FILE, l'entrypoint llegeix la contrasenya del fitxer i mai no l'exposa com a variable d'entorn. Per al teu propi codi, el patró és de tres línies:

import { readFileSync } from 'node:fs';

// Prefereix el fitxer de secret; cau a la variable només si no existeix.
const dbPassword = process.env.DB_PASSWORD_FILE
  ? readFileSync(process.env.DB_PASSWORD_FILE, 'utf8').trim()
  : process.env.DB_PASSWORD;
mkdir -p secrets && chmod 700 secrets
printf 'aurora_secreta' > secrets/db_password.txt   # printf, no echo: sense salt de línia
chmod 600 secrets/db_password.txt
docker compose up -d
docker compose exec aurora-db env | grep -c "PASSWORD=aurora_secreta" || echo "ja no és a l'entorn"
docker compose exec aurora-db cat /run/secrets/db_password
ja no és a l'entorn
aurora_secreta

El secret ha desaparegut de l'entorn i viu en un fitxer amb permisos restringits, muntat en un tmpfs que no toca el disc del contenidor i no queda en cap capa de la imatge.

Mètode Visible a inspect A l'entorn dels fills En capes de la imatge Recomanació
ENV al Dockerfile Sí, per sempre Mai
environment a Compose No Només valors no sensibles
env_file Sí (acaba a l'entorn) No Només valors no sensibles
secrets: amb fitxer No No No Correcte per a desenvolupament i un sol host
Gestor extern (Vault, KMS...) No No No Producció

Avís important. L'anterior és el mecanisme bàsic de Compose i és adequat per a desenvolupament local i desplegaments petits en un sol host, amb dades fictícies com les d'aquest curs. La gestió de credencials reals —rotació, control d'accés, auditoria, xifratge en repòs— s'ha de validar sempre amb el responsable de seguretat de la teva organització, que decidirà quin gestor de secrets correspon. Res del que facis amb fitxers locals no substitueix aquesta conversa. L'enduriment general de contenidors es tracta a la lliçó 05-03.

  1. Depuració amb docker compose config

Quan una variable no arriba on esperes, no ho endevinis:

docker compose config                       # tot resolt
docker compose config --no-interpolate      # amb els ${...} sense resoldre
docker compose config --format json | jq '.services["aurora-api"].environment'
{
  "DB_HOST": "aurora-db",
  "DB_NAME": "aurora_llibres",
  "DB_PASSWORD": "aurora_secreta",
  "NODE_ENV": "production",
  "PORT": "3000"
}

Comparar config amb config --no-interpolate et diu a l'instant si el problema és a la substitució (sistema A) o al pas al contenidor (sistema B). I docker compose exec <servei> env et dóna la veritat definitiva: el que el procés veu de debò.

Compte: la sortida de config conté els secrets resolts en clar. No la bolquis mai en un log de CI ni l'enganxis en un tiquet.

Errors Habituals i Consells

Creure que el .env arriba als contenidors. No hi arriba. Només alimenta la interpolació. Necessites environment o env_file a més.

Fer servir ${VAR} sense :? per a valors obligatoris. Una contrasenya absent es converteix en cadena buida i arrenques una base de dades sense contrasenya, en silenci.

Pujar el .env a Git. És l'escapament de credencials més freqüent que existeix. Ignora'l des del primer commit i versiona .env.example.

Oblidar el $$ per a un $ literal. Provoca Invalid interpolation format o, pitjor, una cadena buida on esperaves un patró.

Fer servir echo per crear el fitxer d'un secret. Hi afegeix un salt de línia final que forma part de la contrasenya. Fes servir printf o retalla amb .trim().

Posar cometes al .env esperant que s'ignorin. En alguns casos formen part del valor; en cas de dubte, verifica-ho amb docker compose config.

Consell: una regla d'or per decidir on va cada cosa. Canvia entre entorns i no és sensible? Interpolació amb ${VAR:-defecte}. És sensible? secrets:. És estructural i no canvia mai, com ara DB_HOST: aurora-db? Escriu-ho directament al compose.yaml.

Exercicis

Exercici 1. Parametritza el port de la web amb ${WEB_PORT:-8080} i demostra les quatre fonts de valor amb la seva precedència: sense res definit, amb .env, amb una variable exportada al shell i amb una variable a la mateixa línia de comandes. Fes servir docker compose config per verificar cada cas sense aixecar res.

Exercici 2. Defineix al compose.yaml NODE_ENV: ${NODE_ENV:-production} i, a més, un env_file que contingui NODE_ENV=des-de-fitxer. Prediu quin valor veurà el procés, verifica-ho, i explica el resultat amb la taula de precedència.

Exercici 3. Converteix la contrasenya de PostgreSQL en un secret de fitxer per a aurora-db i aurora-api, i demostra amb tres comandes diferents que ja no apareix a l'entorn de cap dels dos contenidors.

Solucions

Solució 1.

# (a) Sense res: guanya el valor per defecte del mateix ${...}
rm -f .env; unset WEB_PORT
docker compose config | grep -A1 "published"
# (b) Amb .env
echo "WEB_PORT=9090" > .env
docker compose config | grep -A1 "published"
# (c) El shell guanya al .env
export WEB_PORT=7070
docker compose config | grep -A1 "published"
# (d) La línia de comandes guanya a tot
WEB_PORT=6060 docker compose config | grep -A1 "published"
published: "8080"
published: "9090"
published: "7070"
published: "6060"

L'escala completa en quatre comandes: defecte < .env < shell exportat < línia de comandes. Que el shell guanyi al .env és el que permet a un pipeline de CI canviar la versió de la imatge sense tocar fitxers, i el que alhora explica un desconcert molt comú: si tens una variable vella exportada a la teva sessió, el .env no la sobreescriu. Neteja amb unset abans de sospitar del fitxer.

unset WEB_PORT   # deixar la sessió neta

Solució 2. El procés veurà production.

  aurora-api:
    environment:
      NODE_ENV: ${NODE_ENV:-production}
    env_file:
      - ./config/api.env       # conté NODE_ENV=des-de-fitxer
docker compose up -d aurora-api
docker compose exec aurora-api env | grep NODE_ENV
NODE_ENV=production

A la taula de precedència, environment (nivell 3) està per damunt d'env_file (nivell 5). El fitxer només aporta les variables que el bloc environment no defineix, així que serveix com a base i environment com a sobreescriptura puntual. I compte amb el ${NODE_ENV:-production}: si a més exportes NODE_ENV=development al teu shell, la interpolació ho resol a development i el resultat canvia, encara que el nivell de precedència continuï sent el 3.

Solució 3.

mkdir -p secrets && chmod 700 secrets
printf 'aurora_secreta' > secrets/db_password.txt && chmod 600 secrets/db_password.txt
grep -q "^secrets/" .gitignore || echo "secrets/" >> .gitignore
secrets:
  db_password:
    file: ./secrets/db_password.txt

services:
  aurora-db:
    environment:
      POSTGRES_USER: aurora
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
      POSTGRES_DB: aurora_llibres
    secrets: [db_password]

  aurora-api:
    environment:
      DB_PASSWORD_FILE: /run/secrets/db_password
    secrets: [db_password]

Amb l'ajust de server.js de l'apartat 8 per llegir DB_PASSWORD_FILE, la verificació:

docker compose up -d --force-recreate
docker inspect aurora-libros-aurora-db-1 --format '{{range .Config.Env}}{{println .}}{{end}}' | grep -i pass
docker compose exec aurora-api env | grep -i password
docker compose exec aurora-api cat /proc/1/environ | tr '\0' '\n' | grep -i password
curl -s http://localhost:8080/api/llibres | jq -r '.llibres | length'
POSTGRES_PASSWORD_FILE=/run/secrets/db_password
DB_PASSWORD_FILE=/run/secrets/db_password
DB_PASSWORD_FILE=/run/secrets/db_password
9

Les tres vies mostren la ruta, mai el valor, i els nou llibres confirmen que la connexió continua funcionant. Un detall essencial: si canvies això sobre una base de dades ja inicialitzada, la contrasenya no s'actualitza, perquè POSTGRES_PASSWORD* només s'aplica a la primera inicialització del volum. En un entorn de proves, docker compose down -v; en un de real, un ALTER USER.

Conclusió

Tens separats els dos sistemes que causen gairebé tota la confusió amb Compose. El sistema A és la substitució ${VAR} que Compose fa sobre el YAML abans de crear res, amb els seus modificadors :- i - per a valors per defecte i :? i ? per a valors obligatoris que avorten amb un missatge útil, l'escapament $$ per al dòlar literal, i el seu ordre de cerca: línia de comandes, shell exportat, --env-file i .env del projecte. El sistema B és el que veu el procés: environment en forma de mapa o de llista —amb el pass-through d'una clau sense valor— i env_file amb el seu required: false.

Coneixes la taula de precedència completa, de run -e fins a l'ENV del Dockerfile, i el parany que amaga: el .env del projecte no és en aquesta taula, perquè no injecta res als contenidors. Aurora Libros queda parametritzada en versió d'imatge, ports i credencials, amb un .env local ignorat per Git i un .env.example versionat que documenta què cal per arrencar.

I saps que environment no és lloc per a una contrasenya: l'has vista en clar per tres vies diferents, i l'has retirada amb el bloc secrets:, que munta fitxers a /run/secrets/ i encaixa amb el patró _FILE de les imatges oficials i amb tres línies del teu propi codi. Amb l'avís que no has d'oblidar: per a credencials reals, valida sempre l'enfocament amb el responsable de seguretat de la teva organització.

A la lliçó següent, Perfils, Overrides i Múltiples Entorns, faràs que un mateix projecte serveixi per a desenvolupament, proves i producció sense duplicar fitxers: perfils per aixecar sota demanda eines com Adminer o un servei de llavors, el compose.override.yaml automàtic i la combinació explícita amb diversos -f juntament amb les seves regles de fusió, extends i include: per compartir definicions entre equips, i la convenció de noms de projecte que permet que les piles de dos entorns convisquin a la mateixa màquina.

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