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
- Els dos sistemes, separats des del principi
- Sistema A: substitució de variables al YAML
- D'on treu Compose els valors per substituir
- Sistema B: variables dins del contenidor
- La taula de precedència completa
- El fitxer
.envdel projecte - Aurora Libros parametritzada:
.envi.env.example - Gestió de secrets:
environmentno és un lloc per a contrasenyes - Depuració amb
docker compose config
- 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.
- 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}error while interpolating services.aurora-db.environment.POSTGRES_PASSWORD:
required variable DB_PASSWORD is missing a value: falta DB_PASSWORD; copia .env.example a .envUn 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.
- 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.
- Sistema B: variables dins del contenidor
Forma de mapa (recomanada: es llegeix millor i es fusiona bé entre fitxers d'override):
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 ${...}? |
Sí | 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.
- 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
- El fitxer
.env del projecte
.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-librosFixa't en l'última línia: al .env també pots fixar les variables de configuració de la mateixa CLI —COMPOSE_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
- Aurora Libros parametritzada:
.env i .env.example
.env i .env.exampleAplica 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=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.
- Gestió de secrets:
environment no és un lloc per a contrasenyes
environment no és un lloc per a contrasenyesTot 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 PASSTres 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 propietariLes 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_passwordEl 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í | Sí | Sí, per sempre | Mai |
environment a Compose |
Sí | Sí | No | Només valors no sensibles |
env_file |
Sí (acaba a l'entorn) | Sí | 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.
- Depuració amb
docker compose config
docker compose configQuan 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"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.
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-fitxerA 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/" >> .gitignoresecrets:
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
9Les 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
- Què és Docker?
- Instal·lant Docker
- Arquitectura de Docker
- Comandes Bàsiques de Docker
- Entenent les Imatges de Docker
- Creant el teu Primer Contenidor Docker
- El Projecte del Curs: la Plataforma Aurora Libros
Mòdul 2: Treballant amb Imatges Docker
- Docker Hub i Repositoris
- Construint Imatges Docker
- Conceptes Bàsics de Dockerfile
- Instruccions Avançades del Dockerfile
- Gestionant Imatges Docker
- Etiquetatge i Publicació d'Imatges
Mòdul 3: Contenidors Docker
- Executant Contenidors
- Cicle de Vida del Contenidor
- Gestionant Contenidors
- Inspecció i Depuració de Contenidors
- Xarxes a Docker
- Persistència de Dades amb Volums
- Límits de Recursos i Polítiques de Reinici
Mòdul 4: Docker Compose
- Introducció a Docker Compose
- Definint Serveis a Docker Compose
- Comandes de Docker Compose
- Aplicacions Multi-Contenidor
- Variables d'Entorn a Docker Compose
- Perfils, Overrides i Múltiples Entorns
- Desenvolupament Local amb Docker Compose
Mòdul 5: Conceptes Avançats de Docker
- Aprofundiment en Xarxes Docker
- Opcions d'Emmagatzematge Docker
- Millors Pràctiques de Seguretat a Docker
- Optimitzant Imatges Docker
- Builds Avançades amb BuildKit i Buildx
- Registre i Monitoratge a Docker
- El Runtime per Dins: Namespaces, Cgroups i Capes
Mòdul 6: Docker en Producció
- Preparar una Imatge per a Producció
- CI/CD amb Docker
- Orquestrant Contenidors amb Docker Swarm
- Introducció a Kubernetes
- Desplegant Contenidors Docker a Kubernetes
- Escalat i Balanceig de Càrrega
- Estratègies de Desplegament i Rollback
