La teva imatge auroralibros/aurora-api:1.1.0 està publicada i és impecable: usuari sense privilegis, metadades OCI, healthcheck i aturada neta en 0,3 segons. I continua retornant ECONNREFUSED a /llibres, exactament com t'anunciava la lliçó anterior. Aquest mòdul sencer existeix per arreglar això, i comença per on cal començar: entendre de debò la comanda que fas servir des de la lliçó 01-06 sense haver-la mirat mai de prop. docker run no és una comanda, són dues: crea un contenidor i l'arrenca. I admet més d'un centenar d'opcions, de les quals a la feina diària se'n fan servir unes quinze. En aquesta lliçó veuràs què fa run per dins, aprendràs aquestes quinze opcions agrupades per famílies, sobreescriuràs el CMD i l'ENTRYPOINT d'una imatge des de la línia de comandes, i aixecaràs per fi aurora-db i aurora-cache com a contenidors reals. En acabar tindràs tres contenidors vius... i l'API continuarà sense funcionar. Aquesta fallada, amb el missatge d'error canviat, és la pista amb què arrenca la resta del mòdul.

Contingut

  1. Què fa docker run realment: create + start
  2. Anatomia de la comanda i l'ordre dels arguments
  3. Família identitat: --name i --hostname
  4. Família execució: -d, -it, --rm, -w, -u, --entrypoint
  5. Família xarxa i ports: el mínim per treballar
  6. Família configuració: -e i --env-file
  7. Família dades: -v en la seva versió mínima
  8. Sobreescriure el CMD i l'ENTRYPOINT des de la línia de comandes
  9. Primer pla, segon pla i com desacoblar-se
  10. docker attach enfront de docker exec
  11. Pràctica: la base de dades i la memòria cau d'Aurora Libros

  1. Què fa docker run realment: create + start

docker run és una drecera. Per sota executa dues operacions diferents que pots invocar per separat:

docker create --name prova-cicle -p 8080:80 nginx:alpine
7f3a9c1e8b2d4a6f05c7e91b3d8a4b2c6e0b7d9a1f3c5e7b9d1a3f5c7e9b1d3a

Docker imprimeix l'ID complet del contenidor (64 caràcters hexadecimals) i retorna el control. Fixa't en el que ha passat i en el que no:

docker ps -a --filter name=prova-cicle --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
NAMES          STATUS    PORTS
prova-cicle    Created

El contenidor existeix, té la seva capa d'escriptura reservada i tota la configuració desada (ports, variables, comanda), però l'estat és Created i la columna PORTS està buida: no hi ha cap procés en marxa i el port 8080 no escolta a la teva màquina. Comprova-ho:

curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8080
000

Ara arrenca'l:

docker start prova-cicle
prova-cicle

docker start imprimeix el nom del contenidor arrencat. I ara sí:

docker ps --filter name=prova-cicle --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8080
NAMES          STATUS         PORTS
prova-cicle    Up 3 seconds   0.0.0.0:8080->80/tcp
200

Aquesta separació no és un detall acadèmic: explica el repartiment de responsabilitats que faràs servir durant tot el mòdul.

Fase Què fa Què es pot canviar després
create Reserva la capa d'escriptura, desa la configuració (nom, ports, variables, muntatges, comanda, límits) Gairebé res: ports, variables i muntatges queden fixats per sempre
start Crea els namespaces, aplica els cgroups i llança el procés PID 1 Es pot repetir tantes vegades com vulguis (stop/start)

La conseqüència pràctica, que convé gravar-se ja: no pots afegir un port ni una variable d'entorn a un contenidor que ja existeix. Si t'has equivocat, s'esborra i es crea de nou. És exactament per això que al mòdul 4 acabaràs escrivint aquesta configuració en un fitxer en comptes de a la línia de comandes.

Neteja la demostració:

docker rm -f prova-cicle

El flux complet, ara que coneixes les peces de la lliçó 01-03:

flowchart TD
    A["docker run -d -p 8080:80 nginx:alpine"] --> B{"Hi és la imatge<br/>en local?"}
    B -- No --> C["docker pull implícit<br/>des del registre"]
    B -- Sí --> D["FASE CREATE"]
    C --> D
    D --> D1["Capa d'escriptura (copy-on-write)"]
    D --> D2["Configuració: nom, ports,<br/>variables, comanda"]
    D1 --> E["FASE START"]
    D2 --> E
    E --> E1["Namespaces: pid, net, mnt, uts, ipc"]
    E --> E2["Cgroups: memòria i CPU"]
    E --> E3["Regles de xarxa i publicació de ports"]
    E1 --> F["runc llança el procés PID 1"]
    E2 --> F
    E3 --> F
    F --> G["Contenidor en estat running"]

  1. Anatomia de la comanda i l'ordre dels arguments

docker run [OPCIONS] IMATGE [COMANDA] [ARGUMENTS...]

Hi ha una sola regla d'or i provoca la meitat dels errors dels principiants:

Tot el que va abans de la imatge són opcions de Docker. Tot el que va després de la imatge és la comanda que s'executa dins del contenidor.

Mira-la fallar:

docker run alpine:3.20 -e MISSATGE=hola echo prova
docker: Error response from daemon: failed to create task for container:
failed to create shim task: OCI runtime create failed: exec: "-e": executable file not found in $PATH

Docker no ha interpretat -e com una opció seva: com que venia després d'alpine:3.20, ha intentat executar dins del contenidor un programa anomenat -e. La versió correcta:

docker run --rm -e MISSATGE=hola alpine:3.20 sh -c 'echo $MISSATGE'
hola

Aquí --rm i -e MISSATGE=hola són opcions de Docker (van abans de la imatge) i sh -c 'echo $MISSATGE' és la comanda dins del contenidor (va després). Necessitem sh -c perquè l'expansió de $MISSATGE la fa un shell, i sense ell Docker passaria la cadena literal.

Una ullada general a les famílies que veuràs a les seccions següents:

Família Opcions Per a què
Identitat --name, --hostname, --label Poder referir-te al contenidor i organitzar-lo
Execució -d, -it, --rm, -w, -u, --entrypoint Com i amb quina identitat corre el procés
Xarxa i ports -p, -P, --expose, --network Qui pot parlar amb ell (a fons a 03-05)
Configuració -e, --env-file Quins valors llegeix l'aplicació
Dades -v, --mount, --tmpfs Què sobreviu al contenidor (a fons a 03-06)
Recursos i resiliència -m, --cpus, --restart Quant pot consumir i què passa si falla (a fons a 03-07)

  1. Família identitat: --name i --hostname

--name

Sense --name, Docker inventa un nom de dues paraules (nostalgic_hopper, elegant_bardeen) i t'obliga a copiar IDs. Amb nom, totes les comandes següents es tornen llegibles i automatitzables amb scripts:

docker run -d --name aurora-cache redis:7-alpine
docker logs aurora-cache
docker stop aurora-cache

Regles dels noms:

Regla Detall
Caràcters vàlids [a-zA-Z0-9][a-zA-Z0-9_.-]*
Únic a tota la màquina Inclosos els contenidors aturats
Serveix com a nom DNS En una xarxa pròpia, un altre contenidor el resol per aquest nom (lliçó 03-05)

Aquest darrer punt és la clau de tot el mòdul, i per això els noms d'Aurora Libros (aurora-db, aurora-cache, aurora-api, aurora-web) no són decoratius: d'aquí a poc seran noms de host reals.

L'error més habitual amb --name:

docker run -d --name aurora-cache redis:7-alpine
docker: Error response from daemon: Conflict. The container name "/aurora-cache" is already in use by container "a1b2c3d4e5f6".
You have to remove (or rename) that container to be able to reuse that name.

Compte: el conflicte es produeix encara que el contenidor anterior estigui aturat. Un contenidor Exited continua ocupant el seu nom. Ho resols amb docker rm aurora-cache (o docker rm -f si està corrent).

--hostname

Canvia el nom que el contenidor veu de si mateix dins del seu propi namespace UTS:

docker run --rm --name prova-host --hostname aurora-node-1 alpine:3.20 hostname
aurora-node-1

Sense --hostname, el hostname és l'ID curt del contenidor:

docker run --rm --name prova-host2 alpine:3.20 hostname
9c4e1a7b2f83

--name i --hostname són coses diferents: --name és com l'anomenes tu des de fora (i com l'anomenen altres contenidors per DNS); --hostname és com s'anomena el contenidor a si mateix. Es fa servir poc, però apareix en registres d'aplicacions i en clústers de bases de dades.

  1. Família execució: -d, -it, --rm, -w, -u, --entrypoint

Opció Què fa Quan la fas servir
-d, --detach Arrenca en segon pla i imprimeix l'ID Serveis: bases de dades, APIs, servidors web
-i, --interactive Manté l'entrada estàndard oberta Quan escriuràs alguna cosa al procés
-t, --tty Assigna un pseudo-terminal (indicador, colors, Ctrl+C) Shells interactives
-it La combinació de les dues anteriors Entrar en un contenidor amb sh o bash
--rm Esborra el contenidor en acabar Comandes d'un sol ús i proves
-w, --workdir Directori de treball, sobreescriu el WORKDIR de la imatge Executar alguna cosa en una altra ruta sense reconstruir
-u, --user Usuari/UID amb què corre el procés Ajustar permisos o entrar com a root a una imatge sense privilegis
--entrypoint Substitueix l'ENTRYPOINT de la imatge Depurar una imatge on l'executable fix fa nosa

-d enfront de primer pla

docker run -d --name web-fons -p 8080:80 nginx:alpine
c3f8a1b7e2d94c6501fa7b3e8d2c9a4b6e0f1d3a5c7e9b1d3f5a7c9e1b3d5f7a

Retorna l'ID i et deixa el terminal lliure. Sense -d, el terminal queda ocupat mostrant la sortida del procés i Ctrl+C l'atura.

-it i per què calen les dues lletres

docker run --rm -it alpine:3.20 sh
/ # whoami
root
/ # exit

Prova de treure una de les dues lletres per entendre què aporta cadascuna:

Comanda Resultat
docker run --rm alpine:3.20 sh El shell arrenca, no troba entrada i acaba immediatament. Tornes a l'indicador del host
docker run --rm -i alpine:3.20 sh Funciona: pots escriure comandes, però sense indicador, sense colors i sense historial
docker run --rm -t alpine:3.20 sh Veus l'indicador / #, però el que escrius no hi arriba: el contenidor no té stdin. Queda penjat
docker run --rm -it alpine:3.20 sh Sessió interactiva completa

Regla pràctica: -it per a persones, -d per a serveis, res per a comandes d'un sol tret. I mai -it en un script automatitzat ni a CI: sense terminal real, -t provoca the input device is not a TTY.

--rm

docker run --rm alpine:3.20 date -u
Tue Aug  4 19:32:11 UTC 2026

El contenidor s'executa, imprimeix i desapareix: no queda a docker ps -a ni ocupa capa d'escriptura. És l'opció que evita acumular desenes de contenidors morts.

Dos avisos importants:

  • --rm és incompatible amb la depuració: si el contenidor falla, s'esborra juntament amb els seus registres i el seu inspect. Quan alguna cosa va malament, treu el --rm per poder investigar (lliçó 03-04).
  • --rm esborra també els volums anònims associats. Amb volums amb nom no passa res, però és un detall que es reprèn a la lliçó 03-06.

-w i -u

docker run --rm -w /tmp alpine:3.20 pwd
docker run --rm -u 1000:1000 alpine:3.20 id
docker run --rm -u root auroralibros/aurora-api:1.1.0 id
/tmp
uid=1000 gid=1000 groups=1000
uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),...
  • -w /tmp sobreescriu el WORKDIR /app que vas fixar al Dockerfile.
  • -u 1000:1000 força un UID:GID concret encara que aquest usuari no existeixi dins de la imatge (per això id mostra els números sense nom).
  • -u root anul·la el USER node de la teva imatge. És utilíssim per depurar (instal·lar una eina dins d'un contenidor sense privilegis), i alhora és un recordatori que USER és una defensa en profunditat, no una barrera infranquejable: qui pot llançar contenidors pot triar l'usuari.

--entrypoint

Ja el vas fer servir a la lliçó 02-04 i aquí formalitza el seu lloc:

docker run --rm -it --entrypoint sh auroralibros/aurora-api:1.1.0
/app $ ls
Dockerfile  node_modules  package-lock.json  package.json  server.js
/app $ node --version
v22.13.0

Sense --entrypoint, la teva imatge té ENTRYPOINT ["node"] fix i qualsevol cosa que escrivissis després de la imatge seria un argument de node. Amb --entrypoint sh hi entres a mirar. És la porta de servei de tota imatge ben construïda.

  1. Família xarxa i ports: el mínim per treballar

Aquí només l'imprescindible; el perquè complet arriba a la lliçó 03-05.

docker run -d --name aurora-web-demo -p 8080:80 nginx:alpine

-p HOST:CONTENIDOR significa "tot el que arribi al port 8080 de la meva màquina, redirigeix-ho al port 80 d'aquest contenidor". L'ordre mai s'inverteix: primer el host, després el contenidor.

Sintaxi Significat
-p 8080:80 Port 8080 de totes les interfícies del host → port 80 del contenidor
-p 127.0.0.1:8080:80 Només des de la teva pròpia màquina; no visible a la xarxa local
-p 80 Port 80 del contenidor a un port aleatori lliure del host
-p 8080:80/udp Publica UDP en comptes de TCP
-P Publica tots els ports declarats amb EXPOSE, cadascun en un port aleatori
--expose 9000 Declara el port a les metadades, sense publicar-lo al host

Comprova -P amb la teva pròpia imatge, que declara EXPOSE 3000:

docker run -d --name api-aleatoria -P auroralibros/aurora-api:1.1.0
docker port api-aleatoria
3000/tcp -> 0.0.0.0:32768

Docker ha triat el 32768 del rang efímer. docker port és la comanda que respon a "en quin port del host ha quedat això?".

Dues idees que convé fixar des d'ara, encara que es desenvolupin a 03-05:

  • EXPOSE al Dockerfile i --expose a run no obren res: són documentació que consumeixen -P i altres eines.
  • Publicar un port serveix perquè tu, des del host, arribis al contenidor. Perquè dos contenidors es parlin entre ells no cal publicar res. Aquesta distinció és la que està a punt de resoldre el misteri de l'API.

Neteja:

docker rm -f aurora-web-demo api-aleatoria

  1. Família configuració: -e i --env-file

El teu server.js no té ni un valor fix escrit: llegeix PORT, DB_HOST, DB_USER, DB_PASSWORD, DB_NAME i REDIS_HOST de l'entorn. Aquesta decisió de la lliçó 01-07 és la que permet que la mateixa imatge serveixi per al teu portàtil, per a proves i per a producció.

-e en les seves tres formes

docker run --rm -e SALUTACIO="Hola Aurora" alpine:3.20 printenv SALUTACIO
export TOKEN_LOCAL=abc123
docker run --rm -e TOKEN_LOCAL alpine:3.20 printenv TOKEN_LOCAL
docker run --rm auroralibros/aurora-api:1.1.0 --version
Hola Aurora
abc123
v22.13.0
Forma Efecte
-e CLAU=valor Defineix la variable amb aquest valor literal
-e CLAU Copia el valor que tingui aquesta variable al shell del host
Sense -e L'aplicació fa servir l'ENV de la imatge o el seu valor per defecte al codi

La segona forma és molt pràctica per no escriure secrets a la línia de comandes, on quedarien a l'historial del shell.

Les variables definides amb -e sobreescriuen les de l'ENV del Dockerfile. Comprova-ho amb la teva imatge, que porta ENV PORT=3000:

docker run --rm -e PORT=4000 --entrypoint printenv auroralibros/aurora-api:1.1.0 PORT
4000

I torna a veure tota l'escala de prioritat:

Prioritat Origen Exemple
1 (guanya) -e / --env-file a docker run -e PORT=4000
2 ENV del Dockerfile ENV PORT=3000
3 Valor per defecte al codi process.env.PORT || 3000

--env-file

Quan passes de tres variables, la línia de comandes es torna il·legible. Crea ~/aurora-libros/aurora.env:

# Configuració local d'Aurora Libros — NO es cou dins la imatge
PORT=3000
DB_HOST=aurora-db
DB_PORT=5432
DB_USER=aurora
DB_PASSWORD=aurora_secreta
DB_NAME=aurora_llibres
REDIS_HOST=aurora-cache
REDIS_PORT=6379

I fes-lo servir:

docker run --rm --env-file ~/aurora-libros/aurora.env \
  --entrypoint printenv auroralibros/aurora-api:1.1.0 DB_HOST DB_NAME
aurora-db
aurora_llibres

Les regles del format són estrictes i diferents de les d'un script de shell. Aquest és un punt on es perd molt de temps:

Regla Correcte Incorrecte i per què
Una variable per línia, CLAU=valor DB_USER=aurora export DB_USER=aurora → la clau seria export DB_USER
Sense cometes llevat que formin part del valor DB_PASSWORD=aurora_secreta DB_PASSWORD="aurora_secreta" → la contrasenya inclouria les cometes
Sense expansió de variables RUTA=/app/dades RUTA=$HOME/dades → el valor literal seria $HOME/dades
Comentaris amb # a principi de línia # comentari DB_PORT=5432 # el port → el valor seria 5432 # el port
Sense espais al voltant de l'= PORT=3000 PORT = 3000 → la clau seria PORT

I una regla de seguretat que ja coneixes del mòdul 2, ara amb la seva conseqüència concreta:

echo "aurora.env" >> ~/aurora-libros/.gitignore
echo "aurora.env" >> ~/aurora-libros/api/.dockerignore

El fitxer d'entorn conté una contrasenya: no va al repositori de Git ni al context de construcció. La imatge es queda sense secrets —com ha de ser— i la configuració viatja per fora, injectada a l'arrencada. Pots combinar les dues opcions: --env-file per al gruix i -e per al que vulguis sobreescriure puntualment, perquè -e guanya sobre --env-file independentment de l'ordre en què els escriguis.

  1. Família dades: -v en la seva versió mínima

El vas fer servir a la lliçó 01-06 per servir el teu index.html amb Nginx:

docker run -d --name aurora-web-demo -p 8080:80 \
  -v ~/aurora-libros/web/index.html:/usr/share/nginx/html/index.html:ro \
  nginx:alpine

La sintaxi mínima és -v ORIGEN:DESTINACIÓ[:opcions], amb la ruta del host sempre absoluta i :ro per muntar en només lectura. Amb això en tens prou fins a la lliçó 03-06, on veuràs els tres tipus de muntatge, els volums gestionats i per què el catàleg de llibres que estàs a punt de crear desapareixerà si no hi fas res. De moment, queda't amb la idea de la lliçó 01-05: tot el que un contenidor escriu fora d'un muntatge viu a la seva capa d'escriptura i mor amb ell.

  1. Sobreescriure el CMD i l'ENTRYPOINT des de la línia de comandes

Reprenem la taula de la lliçó 02-04, ara des del costat de l'execució. La teva imatge declara:

ENTRYPOINT ["node"]
CMD ["server.js"]

La comanda efectiva és la concatenació ENTRYPOINT + CMDnode server.js. I des de docker run pots tocar cada meitat per separat:

Comanda ENTRYPOINT CMD S'executa Resultat
docker run IMG node server.js node server.js Arrenca l'API
docker run IMG --version node --version node --version Imprimeix v22.13.0
docker run IMG -e "console.log(2+2)" node -e console.log(2+2) node -e "console.log(2+2)" Imprimeix 4
docker run --entrypoint sh IMG sh (anul·lat) sh Shell interactiva
docker run --entrypoint sh IMG -c "ls /app" sh -c ls /app sh -c "ls /app" Llista el directori
docker run --entrypoint "" IMG ls -la /app (cap) ls -la /app ls -la /app Executa ls directament

Comprova-ho:

docker run --rm auroralibros/aurora-api:1.1.0 -e "console.log('Aurora ' + (2+2))"
docker run --rm --entrypoint sh auroralibros/aurora-api:1.1.0 -c "ls /app | head -3"
docker run --rm --entrypoint "" auroralibros/aurora-api:1.1.0 ls -la /app/server.js
Aurora 4
Dockerfile
node_modules
package-lock.json
-rw-r--r--    1 node     node          4187 Aug  4 19:12 /app/server.js

Dos matisos que causen sorpreses:

  • Sobreescriure l'ENTRYPOINT descarta el CMD de la imatge. A la fila 4 de la taula, el CMD ["server.js"] desapareix: si no vols que sh rebi server.js com a argument no has de fer res, ja no hi és.
  • --entrypoint "" (cadena buida) és la manera de deixar la imatge sense executable fix, de manera que el que escriguis darrere de la imatge sigui la comanda completa. És el truc que salva el dia amb imatges l'entrypoint de les quals és un script complicat.

  1. Primer pla, segon pla i com desacoblar-se

Quan arrenques sense -d, el teu terminal queda enganxat a l'entrada i la sortida del PID 1:

docker run --name web-primer-pla -p 8080:80 nginx:alpine
/docker-entrypoint.sh: Configuration complete; ready for start up
2026/08/04 19:41:07 [notice] 1#1: start worker processes

Aquí es queda. Si prems Ctrl+C, envies SIGINT al procés i el contenidor s'atura. Això rarament és el que vols amb un servei.

L'alternativa, si vas arrencar amb -it, és desacoblar-te sense matar-lo amb la seqüència Ctrl+P seguida de Ctrl+Q:

docker run -it --name alpine-desacoblament alpine:3.20 sh
/ # sleep 300
<prems Ctrl+P i després Ctrl+Q>
read escape sequence
docker ps --filter name=alpine-desacoblament --format "{{.Names}}: {{.Status}}"
alpine-desacoblament: Up 22 seconds

Continua viu. Tres condicions perquè la seqüència funcioni, i la seva absència explica el 90 % dels "a mi no em va":

  1. El contenidor ha d'haver-se arrencat amb -t (necessita el pseudo-terminal).
  2. Ha d'estar enganxat a stdin, és a dir, amb -i.
  3. La seqüència es prem dins de la sessió, no en un altre terminal.

Si necessites Ctrl+P per a una altra cosa (a bash recupera la comanda anterior), canvia-la:

docker run -it --detach-keys="ctrl-e,e" --name alpine-tecles alpine:3.20 sh

Ara la combinació és Ctrl+E seguida de e. Ho pots fer permanent a ~/.docker/config.json:

{
  "detachKeys": "ctrl-e,e"
}

Resum de les tres maneres d'arrencar:

Forma Comanda Terminal Ctrl+C
Primer pla docker run IMG Ocupat mostrant la sortida Atura el contenidor
Primer pla interactiu docker run -it IMG sh Ocupat, amb sessió Va al procés de dins
Segon pla docker run -d IMG Lliure immediatament No aplica

Neteja:

docker rm -f web-primer-pla alpine-desacoblament alpine-tecles

  1. docker attach enfront de docker exec

Un contenidor en segon pla es pot "tornar a mirar" de dues maneres radicalment diferents:

docker run -d --name aurora-cache-demo redis:7-alpine
docker attach aurora-cache-demo

docker attach et connecta a l'entrada i sortida del procés PID 1 que ja existeix. No llança res de nou. I aquí hi ha el seu perill:

1:M 04 Aug 2026 19:45:03.221 * Ready to accept connections tcp
<prems Ctrl+C>
docker ps -a --filter name=aurora-cache-demo --format "{{.Names}}: {{.Status}}"
aurora-cache-demo: Exited (0) 4 seconds ago

Has aturat Redis. Ctrl+C durant un attach va directe al PID 1. Per sortir sense matar-lo cal fer servir Ctrl+P Ctrl+Q, o directament connectar-se en mode segur:

docker attach --sig-proxy=false aurora-cache-demo

Amb --sig-proxy=false, Ctrl+C et retorna l'indicador sense enviar el senyal al contenidor.

docker exec, en canvi, llança un procés nou dins del contenidor:

docker start aurora-cache-demo
docker exec -it aurora-cache-demo redis-cli PING
PONG

Aquest redis-cli no té res a veure amb el PID 1: és un segon procés que comparteix els namespaces del contenidor i que pot acabar sense afectar el servei.

docker attach docker exec
Què fa Es connecta al PID 1 existent Crea un procés nou
Es pot fer servir diverses vegades alhora? Sí, però tots veuen el mateix Sí, independents
Ctrl+C Mata el servei (llevat de --sig-proxy=false) Acaba només el teu procés
Sortir sense mal Ctrl+P Ctrl+Q exit
Ús típic Veure la sortida en directe d'un procés interactiu Obrir un shell, executar diagnòstics
Recomanació Evitar-lo llevat de casos concrets L'opció per defecte

La regla pràctica: per mirar la sortida fes servir docker logs i per entrar fes servir docker exec. Tots dos s'estudien a fons a la lliçó 03-04. docker attach es reserva per a processos que de debò esperen la teva entrada per teclat.

docker rm -f aurora-cache-demo

  1. Pràctica: la base de dades i la memòria cau d'Aurora Libros

És el moment. Fins ara, l'API arrencava sola i fallava perquè no hi havia ni PostgreSQL ni Redis. Els posarem.

La base de dades

docker run -d \
  --name aurora-db \
  -e POSTGRES_USER=aurora \
  -e POSTGRES_PASSWORD=aurora_secreta \
  -e POSTGRES_DB=aurora_llibres \
  -p 127.0.0.1:5432:5432 \
  postgres:16-alpine

Repassem cada línia, perquè cadascuna té el seu motiu:

Fragment Per què
-d És un servei: no volem el terminal ocupat
--name aurora-db D'aquí a dues lliçons aquest nom serà el hostname que faci servir l'API
-e POSTGRES_USER/PASSWORD/DB Són les tres variables que la imatge oficial de PostgreSQL fa servir per inicialitzar-se el primer cop. Coincideixen amb el que espera server.js
-p 127.0.0.1:5432:5432 Publiquem només en local per poder connectar-hi amb un client des del host. Si escrivissis -p 5432:5432, la teva base de dades quedaria accessible des de tota la xarxa local
postgres:16-alpine La mateixa imatge que vas descarregar al mòdul 2

Comprova que és viu:

docker ps --filter name=aurora-db --format "table {{.Names}}\t{{.Image}}\t{{.Status}}"
docker exec aurora-db pg_isready -U aurora
NAMES       IMAGE                STATUS
aurora-db   postgres:16-alpine   Up 12 seconds
/var/run/postgresql:5432 - accepting connections

pg_isready és l'eina oficial de PostgreSQL per preguntar "acceptes connexions?". L'executem dins del contenidor amb docker exec, sense necessitat de tenir PostgreSQL instal·lat al host. Aquest detall —fer servir les eines que ja venen a la imatge— és una de les comoditats més infravalorades de Docker.

I comprova que la base de dades existeix, encara que de moment estigui buida:

docker exec aurora-db psql -U aurora -d aurora_llibres -c "\dt"
Did not find any relations.

Correcte: la base de dades aurora_llibres està creada, però no hi ha taules. Falta executar db/init.sql, i això es farà de manera automàtica i elegant a la lliçó 03-06, quan muntis aquest fitxer dins del contenidor.

La memòria cau

docker run -d \
  --name aurora-cache \
  -p 127.0.0.1:6379:6379 \
  redis:7-alpine

Redis no necessita cap variable: arrenca amb la seva configuració per defecte.

docker exec aurora-cache redis-cli PING
docker exec aurora-cache redis-cli SET prova "Aurora Libros"
docker exec aurora-cache redis-cli GET prova
PONG
OK
"Aurora Libros"

Redis funciona. Dos contenidors vius.

I ara l'API... que continua sense funcionar

docker run -d \
  --name aurora-api \
  -p 3000:3000 \
  --env-file ~/aurora-libros/aurora.env \
  auroralibros/aurora-api:1.1.0
b7e3d1a9f4c25e8b06d3a7f1c9e5b2d4a8f0c6e3b1d7a5f9c3e1b7d5a9f3c1e7

Docker ha acceptat la comanda sense protestar. Però:

sleep 3
docker ps -a --filter name=aurora-api --format "table {{.Names}}\t{{.Status}}"
NAMES        STATUS
aurora-api   Exited (1) 2 seconds ago

El contenidor ni tan sols s'ha quedat en marxa. Ha arrencat i ha mort amb codi de sortida 1. Vegem per què:

docker logs aurora-api
[cache] error: getaddrinfo ENOTFOUND aurora-cache
[aurora-api] fallada en arrencar: getaddrinfo ENOTFOUND aurora-cache

Fixa't bé en el missatge, perquè ha canviat respecte al mòdul 2 i aquest canvi és una pista de primer ordre:

Configuració Error Què significa
DB_HOST=localhost (mòdul 2) ECONNREFUSED 127.0.0.1:5432 El nom que es va resoldre (a si mateix), però ningú no escolta en aquell port dins del contenidor
DB_HOST=aurora-db (ara) getaddrinfo ENOTFOUND aurora-db El nom ni tan sols s'ha pogut resoldre: per a aquest contenidor, aurora-db no existeix

I aquí hi ha la paradoxa que dona sentit a la resta del mòdul: aurora-db i aurora-cache estan corrent ara mateix, a la mateixa màquina, publicant els seus ports. Ho pots comprovar des del host:

docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
NAMES          STATUS          PORTS
aurora-cache   Up 4 minutes    127.0.0.1:6379->6379/tcp
aurora-db      Up 6 minutes    127.0.0.1:5432->5432/tcp

Són vius, estan publicats, i tot i així aurora-api no els troba. Per què?

La resposta curta: perquè no comparteixen xarxa. Cada contenidor té la seva pròpia pila de xarxa, i a la xarxa per defecte de Docker no hi ha resolució de noms entre contenidors. La resposta llarga, amb la solució completa i el curl que retorna els vuit llibres, és la lliçó 03-05. De moment, deixa la incògnita plantejada i no esborris res: aquests dos contenidors t'acompanyen durant tot el mòdul.

docker rm aurora-api

Esborrem només l'API (està aturada i no serveix de res). aurora-db i aurora-cache es queden.

Errors Habituals i Consells

  • Posar opcions després de la imatge. docker run alpine -e VAR=1 sh intenta executar un programa anomenat -e. Les opcions van abans de la imatge, sempre.
  • Esperar poder afegir un port o una variable a un contenidor existent. No es pot: es fixen a la fase create. La solució és docker rm i tornar a crear. Quan això et cansi, ja estaràs a punt per al mòdul 4.
  • Fer servir --rm mentre depures. Si el contenidor falla i s'esborra, et quedes sense registres i sense inspect. Durant la investigació, treu el --rm.
  • -p 5432:5432 en una base de dades. Publica PostgreSQL a totes les interfícies, inclosa la wifi del bar. Fes servir -p 127.0.0.1:5432:5432, o directament no el publiquis: els contenidors no necessiten ports publicats per parlar entre ells.
  • Invertir l'ordre a -p. -p 80:8080 amb Nginx no funciona i l'error és confús, perquè Docker publica alegrement el 80 del host cap a un 8080 on no escolta ningú. Primer el host, després el contenidor.
  • -t en scripts i CI. the input device is not a TTY. En automatització fes servir -i o res, mai -t.
  • Cometes en un --env-file. DB_PASSWORD="aurora_secreta" desa la contrasenya amb les cometes incloses i provoca un password authentication failed desconcertant. Sense cometes.
  • Consell: fes servir --name sempre. Un contenidor sense nom és un contenidor que d'aquí a deu minuts buscaràs per ID en un docker ps -a amb vint línies.
  • Consell: escriu els docker run llargs en diverses línies amb \ al final, i desa'ls en un fitxer de notes. Els repetiràs moltes vegades, i a la lliçó 03-07 veuràs quant han crescut.

Exercicis

Exercici 1: demostra la separació entre create i start

Sense fer servir docker run en cap moment, crea un contenidor de nginx:alpine anomenat exercici-cicle publicat al port 8090 i amb la variable ENTORN=proves. Abans d'arrencar-lo, respon amb comandes: en quin estat està?, respon el port 8090?, apareix a docker ps? Després arrenca'l, comprova les tres coses de nou, i intenta afegir-li un segon port (8091) sense esborrar-lo. Explica què passa i per què.

Exercici 2: domina la sobreescriptura d'ENTRYPOINT i CMD

Fent servir exclusivament la imatge auroralibros/aurora-api:1.1.0 i sense reconstruir-la, aconsegueix aquestes cinc sortides, escrivint la comanda exacta en cada cas:

  1. La versió de Node instal·lada.
  2. El contingut de /app/package.json.
  3. Un shell interactiu dins del contenidor.
  4. El resultat de console.log(process.env.DB_HOST) amb DB_HOST valent aurora-db.
  5. La llista de fitxers de /app executant ls directament, sense que hi intervingui node ni sh.

Exercici 3: configuració per fitxer per a dos entorns

Crea dos fitxers d'entorn, aurora-dev.env i aurora-proves.env, que difereixin en PORT (3000 i 3100), DB_NAME (aurora_llibres i aurora_llibres_test) i REDIS_HOST. Arrenca dos contenidors de l'API amb la mateixa comanda llevat de l'--env-file i el --name, verifica amb printenv que cadascun té la seva configuració, i després arrenca un tercer que faci servir aurora-dev.env però amb PORT=3200 sobreescrit des de la línia de comandes. Respon: quantes imatges diferents has necessitat?, i quina línia del teu .gitignore impedeix que això acabi al repositori?

Solucions

Solució a l'exercici 1

docker create --name exercici-cicle -p 8090:80 -e ENTORN=proves nginx:alpine
2d8f4a1c7e93b5061fa8c2e7d4b9a6f3c1e8d5b2a9f7c4e1b8d5a2f9c6e3b1d7

Estat abans d'arrencar:

docker ps -a --filter name=exercici-cicle --format "table {{.Names}}\t{{.State}}\t{{.Ports}}"
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8090
docker ps --filter name=exercici-cicle -q
NAMES            STATE     PORTS
exercici-cicle   created

000

Les tres respostes: estat created, el port no respon (codi 000, connexió rebutjada) i no apareix a docker ps (la sortida de la tercera comanda és buida), perquè docker ps sense -a només llista contenidors en execució. La configuració està desada, però no hi ha procés ni regla de xarxa.

docker start exercici-cicle
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8090
docker exec exercici-cicle printenv ENTORN
exercici-cicle
200
proves

Ara sí: apareix a docker ps, el port retorna 200 i la variable que vas definir a la fase create hi és present.

L'intent d'afegir un port:

docker update -p 8091:80 exercici-cicle
unknown flag: -p

No hi ha cap manera de fer-ho. docker update només modifica recursos (memòria, CPU, política de reinici — lliçó 03-07), mai ports, variables ni muntatges: aquests es fixen en crear el contenidor perquè impliquen namespaces de xarxa i regles de reenviament que es construeixen a l'arrencada. L'única sortida és recrear:

docker rm -f exercici-cicle
docker run -d --name exercici-cicle -p 8090:80 -p 8091:80 -e ENTORN=proves nginx:alpine
docker rm -f exercici-cicle

Aquesta rigidesa és precisament l'argument a favor de declarar la configuració en un fitxer versionat en comptes de a la línia de comandes, que és el que fa Docker Compose al mòdul 4.

Solució a l'exercici 2

# 1. Versió de Node: el CMD "server.js" se substitueix per "--version",
#    i l'ENTRYPOINT ["node"] continua al seu lloc → node --version
docker run --rm auroralibros/aurora-api:1.1.0 --version

# 2. Contingut de package.json: necessitem "cat", que no és node.
#    Anul·lem l'entrypoint per complet.
docker run --rm --entrypoint cat auroralibros/aurora-api:1.1.0 /app/package.json

# 3. Shell interactiu: entrypoint sh + les dues lletres d'interactivitat
docker run --rm -it --entrypoint sh auroralibros/aurora-api:1.1.0

# 4. Avaluar codi amb una variable d'entorn: -e de Docker (abans de la
#    imatge) i -e de node (després). El mateix guionet, dos significats.
docker run --rm -e DB_HOST=aurora-db auroralibros/aurora-api:1.1.0 \
  -e "console.log(process.env.DB_HOST)"

# 5. Executar ls directament, sense node ni sh pel mig
docker run --rm --entrypoint "" auroralibros/aurora-api:1.1.0 ls -1 /app
v22.13.0
{
  "name": "aurora-api",
  ...
}
/app $
aurora-db
Dockerfile
node_modules
package-lock.json
package.json
server.js

El cas 4 és el més instructiu de l'exercici: hi ha dos -e a la mateixa comanda i signifiquen coses diferents. El primer va abans de la imatge, així que és l'opció --env de Docker; el segon va després, així que és un argument que Docker lliura tal qual a l'ENTRYPOINT ["node"], i allà -e és l'opció de Node per avaluar codi. La regla de l'apartat 2 ho explica sense ambigüitat.

El cas 5 mostra la diferència entre --entrypoint ls i --entrypoint "". Amb --entrypoint ls hauries d'escriure els arguments després de la imatge igualment, però amb --entrypoint "" la línia es llegeix de manera natural: la imatge deixa d'imposar executable i tot el que segueix és la comanda completa.

Solució a l'exercici 3

cat > ~/aurora-libros/aurora-dev.env <<'EOF'
PORT=3000
DB_HOST=aurora-db
DB_USER=aurora
DB_PASSWORD=aurora_secreta
DB_NAME=aurora_llibres
REDIS_HOST=aurora-cache
EOF

cat > ~/aurora-libros/aurora-proves.env <<'EOF'
PORT=3100
DB_HOST=aurora-db
DB_USER=aurora
DB_PASSWORD=aurora_secreta
DB_NAME=aurora_llibres_test
REDIS_HOST=aurora-cache-test
EOF

Verificació amb printenv, sense arrencar l'API (que fallaria pel que ja saps):

docker run --rm --env-file ~/aurora-libros/aurora-dev.env \
  --entrypoint printenv auroralibros/aurora-api:1.1.0 PORT DB_NAME REDIS_HOST

docker run --rm --env-file ~/aurora-libros/aurora-proves.env \
  --entrypoint printenv auroralibros/aurora-api:1.1.0 PORT DB_NAME REDIS_HOST
3000
aurora_llibres
aurora-cache
3100
aurora_llibres_test
aurora-cache-test

El tercer, amb sobreescriptura puntual:

docker run --rm --env-file ~/aurora-libros/aurora-dev.env -e PORT=3200 \
  --entrypoint printenv auroralibros/aurora-api:1.1.0 PORT DB_NAME
3200
aurora_llibres

PORT val 3200 (ha guanyat el -e) i DB_NAME conserva el valor del fitxer. -e sempre té prioritat sobre --env-file, amb independència de l'ordre en què els escriguis a la línia de comandes: prova de posar-los a l'inrevés i obtindràs el mateix resultat.

Les dues respostes finals:

  • Una sola imatge. auroralibros/aurora-api:1.1.0 ha servit per a tres configuracions diferents sense reconstruir-se ni una sola vegada. Aquest és el principi de construir una vegada, desplegar a tot arreu que vas veure a la lliçó 02-06: el que canvia entre entorns és la configuració injectada, mai la imatge.
  • La línia del .gitignore és *.env (o aurora-*.env). Aquests fitxers contenen DB_PASSWORD=aurora_secreta; al repositori no hi entren, i al .dockerignore tampoc, perquè no acabin al context de construcció ni, per accident, dins d'una capa de la imatge.

Conclusió

docker run ha deixat de ser una fórmula màgica. Saps que són dues operacions, create i start, i ho has demostrat executant-les per separat: la fase create reserva la capa d'escriptura i congela la configuració —per això no pots afegir un port ni una variable a un contenidor existent—, i la fase start munta namespaces, cgroups i regles de xarxa i llança el PID 1. Coneixes la regla d'or de l'ordre dels arguments, que separa les opcions de Docker de la comanda de dins, i tens les cinc famílies d'opcions ordenades: identitat, execució, xarxa, configuració i dades.

Manegues -d per a serveis, -it per a persones i --rm per a comandes d'un sol ús, sabent que aquest --rm és el teu enemic tan bon punt alguna cosa falla. Saps sobreescriure la meitat CMD i la meitat ENTRYPOINT d'una imatge per separat, inclosa l'anul·lació total amb --entrypoint "". Saps desacoblar-te d'una sessió amb Ctrl+P Ctrl+Q sense matar el procés, i per què docker attach és una eina esmolada que et pot tombar un servei amb un Ctrl+C distret, enfront de docker exec, que llança un procés independent. I saps portar la configuració fora de la imatge amb --env-file, amb les seves regles de format estrictes i la seva contrasenya que mai no entra ni a Git ni al context de construcció.

Sobretot, la plataforma ha començat a moure's: aurora-db i aurora-cache estan corrent ara mateix a la teva màquina, amb PostgreSQL 16 acceptant connexions i Redis responent PONG. I aurora-api ja no dona ECONNREFUSED: dona getaddrinfo ENOTFOUND aurora-db, un error diferent que diu una cosa molt concreta —el nom ni tan sols es resol— i que apunta directament a la solució. A més, el contenidor no es va quedar unhealthy: va morir en dos segons amb codi de sortida 1.

I això planteja la pregunta amb què continua el curs: per què uns contenidors es queden corrent indefinidament i altres moren a l'instant? A la lliçó següent, Cicle de Vida del Contenidor, veuràs els set estats pels quals passa un contenidor i les transicions entre ells, la regla que un contenidor viu exactament el que visqui el seu procés PID 1, la diferència entre una aturada ordenada amb SIGTERM i un SIGKILL als deu segons, i la taula de codis de sortida —aquest 1, i també el 125, el 137 i el 143— que converteix un número críptic en un diagnòstic. I ensenyaràs a server.js a apagar-se netament, tancant el pool de PostgreSQL i el client de Redis abans de marxar.

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