Fins ara has vist contenidors que funcionen i contenidors que fallen de manera evident. A la feina real, la situació més freqüent és la tercera: un contenidor que no funciona i no diu per què. Està Up 3 hours però no respon. Surt amb codi 1 i desapareix abans que el puguis mirar. Es marca unhealthy sense cap més explicació. I tu, amb vint pestanyes obertes, provant coses a l'atzar.

Aquesta lliçó et dona un mètode i les cinc eines que el sostenen: docker logs, docker exec, docker inspect, docker stats i docker events. Aprendràs a llegir-les a fons, a entrar en imatges mínimes que no tenen ni ping, i a extreure del JSON d'inspect exactament la dada que necessites. I ho aplicaràs a tres avaries reals d'Aurora Libros, entre elles la que fa tres lliçons que t'espera: per què aurora-api no troba aurora-db encara que tots dos estiguin corrent a la mateixa màquina. Al final d'aquesta lliçó en sabràs la resposta exacta.

Contingut

  1. Un mètode abans que una comanda
  2. docker logs a fons
  3. La regla d'or: stdout i stderr
  4. docker exec: entrar al contenidor
  5. Depurar imatges mínimes sense eines
  6. docker inspect: el JSON complet del contenidor
  7. docker top: els processos de dins
  8. docker stats: consum en temps real
  9. docker events: el flux del dimoni
  10. Cas A: el contenidor surt amb codi 1
  11. Cas B: ECONNREFUSED a /llibres
  12. Cas C: el healthcheck en unhealthy

  1. Un mètode abans que una comanda

La diferència entre algú que depura ràpid i algú que dona pals de cec no són les comandes, sinó l'ordre en què es fan les preguntes:

flowchart TD
    A["Alguna cosa no funciona"] --> B{"Està corrent?<br/>docker ps -a"}
    B -- "No hi apareix" --> B1["No es va crear mai:<br/>revisa el docker run (codi 125)"]
    B -- "Created" --> B2["L'executable no existeix<br/>o no s'ha pogut executar (126/127)"]
    B -- "Exited" --> C{"Quin codi<br/>de sortida?"}
    B -- "Restarting" --> R["Bucle de reinicis:<br/>logs + política (03-07)"]
    B -- "Up" --> D{"Està healthy?"}
    C -- "1-124" --> L["docker logs:<br/>ha fallat LA TEVA aplicació"]
    C -- "137 / 139 / 143" --> S["docker inspect:<br/>senyal, OOMKilled, Error"]
    D -- "unhealthy" --> H["docker inspect<br/>.State.Health.Log"]
    D -- "healthy / sense check" --> E{"Els registres<br/>diuen alguna cosa?"}
    E -- "Sí" --> L
    E -- "No" --> F{"Com està<br/>configurat?"}
    F --> G["docker inspect:<br/>entorn, xarxa, muntatges, ports"]
    G --> I["docker exec:<br/>mirar des de dins"]
    I --> J["Contenidor efímer amb netshoot<br/>si falten eines"]

Les quatre preguntes, en aquest ordre i sense saltar-se'n cap:

# Pregunta Eina Què descarta
1 Està corrent? docker ps -a Distingeix "no va arrencar" de "va arrencar i va fallar"
2 Què diuen els registres? docker logs Resol la majoria dels casos en 10 segons
3 Com està configurat? docker inspect Variables, xarxa, muntatges, ports: el que et pensaves haver posat
4 Què passa a dins? docker exec, top, stats El que només es veu des de dins

L'error més comú és començar per la 4 —entrar al contenidor a mirar— quan la resposta era a la 2. I el segon error més comú és no fer mai la 3, quedant-se hores convençut d'haver passat una variable d'entorn que en realitat tenia una errada.

  1. docker logs a fons

docker logs aurora-db
PostgreSQL init process complete; ready for start up.
2026-08-04 19:28:43.117 UTC [1] LOG:  starting PostgreSQL 16.4 on x86_64-pc-linux-musl
2026-08-04 19:28:43.121 UTC [1] LOG:  listening on IPv4 address "0.0.0.0", port 5432
2026-08-04 19:28:43.198 UTC [1] LOG:  database system is ready to accept connections

Les opcions que es fan servir de debò:

Opció Què fa Exemple
-f, --follow Segueix la sortida en directe docker logs -f aurora-api
--tail N Només les últimes N línies docker logs --tail 50 aurora-db
-t, --timestamps Afegeix marca de temps a cada línia docker logs -t aurora-db
--since Des d'un moment donat --since 10m, --since 2026-08-04T19:30:00
--until Fins a un moment donat --until 5m
--details Mostra metadades extra Poc usat

Combinacions que resolen problemes reals:

# Les 20 últimes línies i seguir en directe: el més usat del dia a dia
docker logs -f --tail 20 aurora-db

# Què va passar en els últims 5 minuts, amb hora exacta
docker logs -t --since 5m aurora-db

# Acotar una finestra concreta al voltant d'un incident
docker logs -t --since 2026-08-04T19:28:00 --until 2026-08-04T19:29:00 aurora-db

# Buscar errors a tot l'històric
docker logs aurora-db 2>&1 | grep -i "error\|fatal"

Tres detalls importants:

  • docker logs funciona amb el contenidor aturat. Els registres viuen al host i sobreviuen a docker stop. Només desapareixen amb docker rm. És la raó per la qual --rm és el teu enemic mentre depures.
  • --tail sense -f és la manera més ràpida de veure el final d'un registre de 100.000 línies sense bolcar-lo sencer al terminal.
  • Amb -f, Ctrl+C només interromp el visor: no toca el contenidor. A diferència de docker attach, aquí Ctrl+C és completament segur.

  1. La regla d'or: stdout i stderr

docker logs mostra exactament dues coses: la sortida estàndard i la sortida d'error del PID 1. Ni una més.

docker run --rm --name demo-fluxos alpine:3.20 \
  sh -c 'echo "això va a stdout"; echo "això va a stderr" >&2; echo "això va a un fitxer" > /tmp/ocult.log'
això va a stdout
això va a stderr

La tercera línia no apareix enlloc, perquè es va escriure en un fitxer dins del contenidor. Pots separar els dos fluxos amb la redirecció estàndard del shell:

docker run --name demo-fluxos2 alpine:3.20 \
  sh -c 'echo "sortida normal"; echo "un error" >&2'
docker logs demo-fluxos2 2>/dev/null      # només stdout
docker logs demo-fluxos2 1>/dev/null      # només stderr
docker rm demo-fluxos2
sortida normal
un error

D'aquí surt la regla que governa tota l'operació de contenidors:

Una aplicació en un contenidor escriu els seus registres a la sortida estàndard i a la sortida d'error. Mai en un fitxer.

Els motius:

Si registra a stdout/stderr Si registra en un fitxer
docker logs funciona docker logs està buit i sembla que l'app no fa res
Els registres es recullen automàticament Cal entrar amb exec o treure'ls amb docker cp
El fitxer no creix dins la capa d'escriptura El contenidor engreixa fins a omplir el disc del host
Qualsevol agregador (Loki, ELK, CloudWatch) els recull sense configurar res Cal muntar volums i desplegar agents
S'esborren sols amb el contenidor Queden orfes

El teu server.js ja ho fa bé des de la lliçó 01-07: fa servir console.log i console.error, que a Node escriuen a stdout i stderr respectivament.

I una comprovació pràctica quan docker logs està sospitosament buit:

docker exec aurora-db ls -la /proc/1/fd/1 /proc/1/fd/2
lrwx------    1 root     root      64 Aug  4 19:28 /proc/1/fd/1 -> /dev/pts/0
lrwx------    1 root     root      64 Aug  4 19:28 /proc/1/fd/2 -> /dev/pts/0

Si aquests descriptors apuntessin a un fitxer en comptes de a la consola, allà tindries la teva explicació.

Un apunt d'abast: on es desen realment aquests registres, com canviar el driver de logging, com rotar-los i com enviar-los a un sistema centralitzat és el contingut de la lliçó 05-06. Aquí ens quedem a llegir-los.

  1. docker exec: entrar al contenidor

docker exec -it aurora-db sh
/ # psql -U aurora -d aurora_llibres -c "SELECT COUNT(*) FROM llibres;"
 count
-------
     8
(1 row)
/ # exit

Ja saps de la lliçó 03-01 que exec llança un procés nou dins dels namespaces del contenidor, i que per això sortir-ne no afecta el servei. Les seves opcions:

Opció Per a què
-it Sessió interactiva amb terminal
-u, --user Executar com un altre usuari, típicament -u root
-w, --workdir Començar en un altre directori
-e Afegir una variable només per a aquest procés
-d Llançar-lo en segon pla dins del contenidor
--privileged Amb capacitats ampliades (últim recurs)

Quin shell fer servir

Imatge base Shell disponible Comanda
Alpine (alpine, node:22-alpine, redis:7-alpine) sh (BusyBox ash). No hi ha bash docker exec -it X sh
Debian/Ubuntu (node:22, postgres:16, ubuntu) bash i sh docker exec -it X bash
distroless, scratch Cap Vegeu l'apartat 5

Si t'equivoques, l'error és inconfusible:

docker exec -it aurora-cache bash
OCI runtime exec failed: exec failed: unable to start container process:
exec: "bash": executable file not found in $PATH: unknown

Un truc que funciona gairebé sempre:

docker exec -it aurora-cache sh -c 'command -v bash || command -v sh'

Entrar com a root en una imatge sense privilegis

El teu aurora-api corre amb USER node des de la lliçó 02-04. Això està molt bé per a producció i és una molèstia per depurar:

docker run -d --name api-depurar --env-file ~/aurora-libros/aurora.env auroralibros/aurora-api:1.2.0
docker exec -it api-depurar id
docker exec -it -u root api-depurar id
docker exec -it -u root api-depurar sh -c 'apk add --no-cache curl && curl -s localhost:3000/salut'
uid=1000(node) gid=1000(node) groups=1000(node)
uid=0(root) gid=0(root) groups=0(root),1(bin),...
{"servei":"aurora-api","version":"1.0.0","db":"ko","cache":"ko","errorDb":"getaddrinfo ENOTFOUND aurora-db","errorCache":"The client is closed"}

Tres coses per aprendre d'aquesta sortida:

  1. -u root et dona privilegis dins del contenidor encara que la imatge declari un altre usuari. La restricció d'USER protegeix del codi que corre a dins, no de qui controla el dimoni de Docker.
  2. Instal·lar eines amb apk add dins d'un contenidor en marxa és legítim per depurar i absolutament prohibit com a manera d'"arreglar" res: tan bon punt recreïs el contenidor, aquell curl desapareix. El que s'arregla, s'arregla al Dockerfile.
  3. I aquí tens el diagnòstic servit en safata: errorDb: getaddrinfo ENOTFOUND aurora-db. L'endpoint /salut que vas escriure a la lliçó 01-07 està fent just la feina per a la qual el vas dissenyar.

Comandes de diagnòstic dins del contenidor

docker exec api-depurar printenv | sort | head -8   # quina configuració veu de debò?
docker exec api-depurar ps -o pid,comm              # quins processos hi ha?
docker exec api-depurar cat /etc/hosts              # quins noms coneix?
docker exec api-depurar cat /etc/resolv.conf        # a quin DNS pregunta?
docker exec api-depurar getent hosts aurora-db      # resol aquest nom?
docker exec api-depurar df -h /                     # queda espai?
docker exec api-depurar ls -la /app                 # és el codi on em penso?
DB_HOST=aurora-db
DB_NAME=aurora_llibres
DB_PASSWORD=aurora_secreta
DB_PORT=5432
DB_USER=aurora
HOME=/home/node
HOSTNAME=4c8f2a1e9b73
NODE_ENV=production

PID   COMMAND
    1 node

127.0.0.1	localhost
::1	localhost ip6-localhost ip6-loopback
172.17.0.4	4c8f2a1e9b73

nameserver 127.0.0.11
options ndots:0

Fixa't en /etc/hosts: hi ha tres entrades i cap no es diu aurora-db. La comanda getent hosts aurora-db ni tan sols va imprimir res (va retornar codi 2, "no trobat"). Guarda't aquesta dada: és la meitat de la resposta al cas B.

  1. Depurar imatges mínimes sense eines

Una imatge ben optimitzada no porta curl, ni ping, ni netstat, ni dig. I les imatges distroless o construïdes FROM scratch no porten ni tan sols un shell. Això és excel·lent per a la seguretat i la mida (lliçó 05-04) i desesperant quan alguna cosa falla.

La solució moderna: un contenidor efímer carregat d'eines que comparteix els namespaces del contenidor malalt.

docker run --rm -it \
  --network container:api-depurar \
  --pid container:api-depurar \
  nicolaka/netshoot
                    dP            dP                           dP
                    88            88                           88
88d888b. .d8888b. d8888P .d8888b. 88d888b. .d8888b. .d8888b. d8888P
...

El que fa cada opció:

Opció Efecte
--network container:X El contenidor nou comparteix la pila de xarxa de X: mateix localhost, mateixa IP, mateixes interfícies
--pid container:X Comparteix l'espai de processos: veus els processos de X i els pots examinar
nicolaka/netshoot Una imatge amb curl, dig, nmap, tcpdump, netstat, ss, iperf, jq i desenes més

I ara, des de dins d'aquest contenidor, diagnostiques la xarxa d'api-depurar com si hi fossis:

 ~ ❯ ss -tlnp
State   Recv-Q  Send-Q   Local Address:Port    Peer Address:Port  Process
LISTEN  0       511            0.0.0.0:3000         0.0.0.0:*

 ~ ❯ ps -ef
PID   USER     TIME  COMMAND
    1 1000      0:00 node server.js
   28 root      0:00 zsh

 ~ ❯ nslookup aurora-db
Server:		127.0.0.11
Address:	127.0.0.11:53

** server can't find aurora-db: NXDOMAIN

 ~ ❯ curl -s localhost:3000/salut | jq -r .errorDb
getaddrinfo ENOTFOUND aurora-db

Quatre conclusions en quatre comandes, sense instal·lar res a la imatge de producció:

  1. ss -tlnp confirma que l'API sí que escolta al 3000, a 0.0.0.0 (bé: no a 127.0.0.1, que seria inabastable des de fora).
  2. ps -ef mostra el node server.js del contenidor veí gràcies a --pid container:, i a més que corre amb UID 1000.
  3. nslookup aurora-db respon NXDOMAIN: el servidor DNS intern de Docker (127.0.0.11) existeix i respon, però no coneix aquest nom.
  4. curl funciona encara que la imatge de l'API no tingui curl, perquè l'aporta netshoot.

Aquest NXDOMAIN és la prova definitiva. Hi tornarem al cas B.

docker debug

Docker Desktop inclou una versió integrada d'aquesta idea:

docker debug api-depurar

Obre un shell amb un conjunt d'eines muntat sobre el contenidor sense modificar-lo: funciona fins i tot en imatges distroless sense shell, i en sortir no en queda ni rastre. Requereix una subscripció Pro o superior; el truc de netshoot és gratuït, funciona a qualsevol Docker i convé conèixer-lo igualment.

docker rm -f api-depurar

  1. docker inspect: el JSON complet del contenidor

docker inspect retorna tot el que Docker sap d'un contenidor: unes 200 línies de JSON.

docker inspect aurora-db | head -20
docker inspect aurora-db | jq 'keys'
[
  "AppArmorProfile", "Args", "Config", "Created", "Driver", "ExecIDs",
  "GraphDriver", "HostConfig", "HostnamePath", "HostsPath", "Id", "Image",
  "LogPath", "MountLabel", "Mounts", "Name", "NetworkSettings", "Path",
  "Platform", "ProcessLabel", "ResolvConfPath", "RestartCount", "State"
]

Els sis blocs que importen:

Bloc Conté
.State Estat, PID, codis de sortida, OOMKilled, salut
.Config El que ve de la imatge: Env, Cmd, Entrypoint, Labels, Healthcheck, User
.HostConfig El que vas posar tu al docker run: ports, memòria, CPU, política de reinici
.NetworkSettings IPs, xarxes, ports publicats, passarel·la
.Mounts Volums i bind mounts actius
.RestartCount Quantes vegades l'ha reiniciat Docker (lliçó 03-07)

Llegir 200 línies de JSON no és depurar. El que es fa és extreure la dada concreta, amb --format o amb jq:

Necessites saber Comanda
Estat i codi de sortida docker inspect -f '{{.State.Status}} ({{.State.ExitCode}})' X
La IP del contenidor docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' X
A quines xarxes està connectat docker inspect -f '{{range $k,$v := .NetworkSettings.Networks}}{{$k}} {{end}}' X
Ports publicats docker inspect -f '{{json .NetworkSettings.Ports}}' X
Variables d'entorn docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' X
Una variable concreta docker inspect -f '{{json .Config.Env}}' X | jq -r '.[]|select(startswith("DB_HOST"))'
Muntatges docker inspect -f '{{range .Mounts}}{{.Type}}: {{.Source}} -> {{.Destination}}{{println}}{{end}}' X
Comanda efectiva docker inspect -f '{{.Path}} {{.Args}}' X
Estat de salut docker inspect -f '{{.State.Health.Status}}' X
Última fallada del healthcheck docker inspect -f '{{(index .State.Health.Log 0).Output}}' X
El va matar l'OOM killer? docker inspect -f '{{.State.OOMKilled}}' X
Nombre de reinicis docker inspect -f '{{.RestartCount}}' X
Límit de memòria docker inspect -f '{{.HostConfig.Memory}}' X
Etiquetes docker inspect -f '{{json .Config.Labels}}' X | jq

Un exemple complet sobre la flota:

docker inspect -f '{{.Name}} | {{.State.Status}} | {{range $k,$v := .NetworkSettings.Networks}}{{$k}}={{$v.IPAddress}} {{end}}' aurora-db aurora-cache
/aurora-db | running | bridge=172.17.0.2
/aurora-cache | running | bridge=172.17.0.3

Dues observacions que seran decisives d'aquí a cinc minuts: tots dos són a la xarxa bridge i cadascun té la seva pròpia IP.

Amb jq pots fer consultes més riques sobre el JSON cru:

docker inspect aurora-db | jq -r '.[0].Config.Env[] | select(startswith("POSTGRES"))'
docker inspect aurora-db | jq -r '.[0].Mounts[] | "\(.Type): \(.Destination)"'
POSTGRES_USER=aurora
POSTGRES_PASSWORD=aurora_secreta
POSTGRES_DB=aurora_llibres
volume: /var/lib/postgresql/data

Nota de seguretat, i és seriosa: docker inspect mostra les contrasenyes en clar. Qualsevol amb accés al dimoni de Docker pot llegir totes les variables d'entorn de tots els contenidors. Per això les variables d'entorn no són un mecanisme de secrets; ho són els gestors de secrets, que es veuen a la lliçó 05-03.

  1. docker top: els processos de dins

docker top aurora-db
UID     PID     PPID    C   STIME   TTY   TIME       CMD
70      24817   24795   0   19:28   ?     00:00:00   postgres
70      24893   24817   0   19:28   ?     00:00:00   postgres: checkpointer
70      24894   24817   0   19:28   ?     00:00:00   postgres: background writer
70      24896   24817   0   19:28   ?     00:00:00   postgres: walwriter
70      24897   24817   0   19:28   ?     00:00:00   postgres: autovacuum launcher

Dues particularitats que el fan especial:

  • Els PID són els del host, no els de dins del contenidor. docker exec aurora-db ps mostraria el mateix postgres com a PID 1; aquí és el 24817. És la doble numeració dels namespaces de PID.
  • No necessita que la imatge tingui ps. La comanda l'executa el dimoni al host. Per això funciona fins i tot en imatges distroless.

Serveix per respondre ràpid a: quants processos hi ha?, hi ha processos zombis?, l'aplicació està llançant fills que no hauria?

  1. docker stats: consum en temps real

docker stats --no-stream
CONTAINER ID   NAME           CPU %   MEM USAGE / LIMIT     MEM %   NET I/O           BLOCK I/O     PIDS
3f8a1c9e7b2d   aurora-db      0.02%   38.41MiB / 7.628GiB   0.49%   1.24kB / 0B       12.3MB / 8.19MB   7
9d4b7e2f1a6c   aurora-cache   0.15%   9.83MiB / 7.628GiB    0.13%   1.86kB / 1.02kB   0B / 0B           6

Sense --no-stream, la vista es refresca contínuament com un top. Columna a columna:

Columna Què mesura Com interpretar-la
CPU % Percentatge de CPU Pot superar el 100 %: 400 % = quatre nuclis saturats
MEM USAGE / LIMIT Memòria usada / límit Si no vas posar límit, LIMIT és tota la RAM del host. Compte
MEM % Ús respecte al límit Sostingut a prop del 100 % = candidat que el mati l'OOM killer
NET I/O Rebut / enviat per la xarxa Un 0B en un servei web indica que ningú no li està parlant
BLOCK I/O Llegit / escrit en disc Un valor que creix sense parar pot ser un registre descontrolat
PIDS Nombre de processos i fils Si creix sense parar, hi ha una fuita de processos

Aquest LIMIT de 7,628 GiB a les dues files és el senyal d'alarma que resoldràs a la lliçó 03-07: cap dels dos contenidors no té límit de memòria, així que qualsevol d'ells pot consumir tota la RAM de la màquina.

Formats útils:

docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.PIDs}}"
docker stats --no-stream $(docker ps -q --filter label=projecte=aurora-libros)

  1. docker events: el flux del dimoni

docker events --since 10m --filter container=aurora-db
2026-08-04T19:28:41.882 container create 3f8a1c9e... (image=postgres:16-alpine, name=aurora-db)
2026-08-04T19:28:41.913 network connect 6b2f... (container=3f8a1c9e..., name=bridge, type=bridge)
2026-08-04T19:28:42.104 container start 3f8a1c9e... (image=postgres:16-alpine, name=aurora-db)
2026-08-04T19:41:07.221 container exec_create: psql -U aurora... 3f8a1c9e...
2026-08-04T19:41:07.238 container exec_start: psql -U aurora... 3f8a1c9e...

docker events és el registre de tot el que fa el dimoni. Sense --since, es queda escoltant en directe. És l'eina que respon a preguntes que cap altra no pot:

# Qui ha matat el meu contenidor i quan?
docker events --since 1h --filter event=die --filter event=kill

# Veure en directe el que passa mentre reprodueixes la fallada en un altre terminal
docker events --filter label=projecte=aurora-libros

# Només esdeveniments de salut
docker events --since 30m --filter event=health_status

Els esdeveniments més útils: create, start, die (amb el seu exitCode), kill, oom, health_status, restart, destroy, i els de xarxa connect/disconnect. Un oom en aquesta llista és la confirmació que va ser el nucli i no pas tu (lliçó 03-07).

  1. Cas A: el contenidor surt amb codi 1

Símptoma: un company et diu que l'API "no arrenca a la seva màquina". Apliques el mètode.

Pregunta 1: està corrent?

docker run -d --name aurora-api \
  --env-file ~/aurora-libros/aurora.env \
  -p 3000:3000 \
  auroralibros/aurora-api:1.2.0 serve.js
sleep 2
docker ps -a --filter name=aurora-api --format "table {{.Names}}\t{{.Status}}"
docker inspect -f '{{.State.Status}} / codi {{.State.ExitCode}}' aurora-api
NAMES        STATUS
aurora-api   Exited (1) 1 second ago
exited / codi 1

exited / codi 1

Codi 1: de la taula de la lliçó 03-02, això significa que l'aplicació va arrencar i va fallar ella mateixa. No és Docker (seria 125), ni un executable inexistent (127), ni un senyal (>128). Així que cal anar als registres.

Pregunta 2: què diuen els registres?

docker logs aurora-api
node:internal/modules/cjs/loader:1215
  throw err;
  ^

Error: Cannot find module '/app/serve.js'
    at Module._resolveFilename (node:internal/modules/cjs/loader:1212:15)
    ...
  code: 'MODULE_NOT_FOUND'

Resolt en dues comandes i quinze segons. El fitxer es diu server.js, no serve.js. D'on surt aquest nom?

Pregunta 3: com està configurat?

docker inspect -f 'Path: {{.Path}} | Args: {{.Args}} | Cmd de la imatge: {{.Config.Cmd}}' aurora-api
Path: node | Args: [serve.js] | Cmd de la imatge: [serve.js]

Aquí ho tens: el CMD de la imatge (["server.js"]) va ser sobreescrit des de la línia de comandes per l'argument solt serve.js, tal com vas aprendre a la lliçó 03-01. La imatge és perfecta; l'error era al docker run.

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

Una variant del mateix símptoma que convé reconèixer, perquè el diagnòstic és completament diferent:

docker run -d --name prova-125 --env-file ~/aurora-libros/no-existeix.env alpine:3.20
echo "codi: $?"
docker ps -a --filter name=prova-125 -q
docker: open /home/junior/aurora-libros/no-existeix.env: no such file or directory
codi: 125

La comanda docker ps -a no retorna res: el contenidor no es va arribar ni a crear. Amb un 125 no hi ha registres per mirar ni contenidor per inspeccionar; l'error és a la teva línia de comandes. És la primera bifurcació de l'arbre de decisió i estalvia molt de temps perdut.

  1. Cas B: ECONNREFUSED a /llibres

Aquí hi ha el cas que arrossegues des del mòdul 2. El resoldrem amb el mètode complet.

Pregunta 1: està corrent?

docker ps --filter name=aurora --format "table {{.Names}}\t{{.Status}}"
NAMES          STATUS
aurora-api     Up 40 seconds (unhealthy)
aurora-db      Up 1 hour
aurora-cache   Up 1 hour

Tots tres estan corrent. No és un problema d'arrencada.

Pregunta 2: què diuen els registres?

docker logs aurora-api
curl -s localhost:3000/llibres | head -c 200
[cache] no disponible en arrencar: getaddrinfo ENOTFOUND aurora-cache
[aurora-api] escoltant al port 3000
[aurora-api] base de dades: aurora-db:5432/aurora_llibres
[aurora-api] memòria cau: aurora-cache:6379
[/llibres] error: getaddrinfo ENOTFOUND aurora-db

{"error":"No s'ha pogut obtenir el catàleg","detall":"getaddrinfo ENOTFOUND aurora-db"}

El missatge és clar: getaddrinfo és la funció de resolució de noms, i ENOTFOUND significa que el nom aurora-db no s'ha pogut traduir a cap adreça IP. No és un rebuig de connexió: és que l'API no sap on connectar-se.

Pregunta 3: com està configurat?

docker inspect -f '{{.Name}}: {{range $k,$v := .NetworkSettings.Networks}}xarxa={{$k}} ip={{$v.IPAddress}}{{end}}' \
  aurora-api aurora-db aurora-cache
/aurora-api: xarxa=bridge ip=172.17.0.4
/aurora-db: xarxa=bridge ip=172.17.0.2
/aurora-cache: xarxa=bridge ip=172.17.0.3

I aquí arriba la sorpresa que fa interessant el cas: tots tres són a la mateixa xarxa, la bridge per defecte, amb IPs del mateix rang 172.17.0.0/16. No és un problema d'aïllament.

Verifica les variables, per descartar:

docker exec aurora-api printenv | grep -E "DB_HOST|REDIS_HOST"
DB_HOST=aurora-db
REDIS_HOST=aurora-cache

Correctes. La configuració és la que volies.

Pregunta 4: què passa a dins?

docker exec aurora-api getent hosts aurora-db
echo "codi de getent: $?"
docker exec aurora-api cat /etc/resolv.conf
codi de getent: 2

nameserver 127.0.0.11
options ndots:0

El DNS intern de Docker (127.0.0.11) està configurat, però no resol aurora-db. Confirma-ho amb eines de debò, fent servir el truc de l'apartat 5:

docker run --rm --network container:aurora-api nicolaka/netshoot \
  sh -c 'nslookup aurora-db; echo "---"; nc -zv 172.17.0.2 5432'
Server:		127.0.0.11
Address:	127.0.0.11:53

** server can't find aurora-db: NXDOMAIN
---
Connection to 172.17.0.2 5432 port [tcp/*] succeeded!

Aquest és el diagnòstic definitiu, i són dos fets oposats a la mateixa sortida:

Prova Resultat Què demostra
nslookup aurora-db NXDOMAIN El nom no es resol
nc -zv 172.17.0.2 5432 succeeded La connectivitat de xarxa existeix i funciona perfectament

O sigui: aurora-api pot parlar amb aurora-db; simplement no sap com es diu. El problema mai no va ser de tallafocs, ni de ports, ni de PostgreSQL: és de resolució de noms.

I la causa és una característica molt concreta de Docker: la xarxa bridge per defecte no té DNS intern entre contenidors. Només les xarxes definides per l'usuari el tenen. Ningú no t'ho havia dit fins ara perquè és exactament el tema de la lliçó vinent.

I no podries posar la IP directament i acabar abans?

# Funcionaria... avui
docker run -d --name api-per-ip -e DB_HOST=172.17.0.2 -e REDIS_HOST=172.17.0.3 ...

Podries, i seria un error. Aquestes IPs les assigna Docker en l'ordre d'arrencada: reinicia la màquina, canvia l'ordre dels contenidors i 172.17.0.2 serà un altre. Escriure IPs a mà és construir un castell sobre sorra. La solució correcta —una xarxa pròpia on els noms funcionin— és la primera pràctica de la lliçó 03-05, i ara ja saps exactament per què la necessites.

La incògnita del mòdul queda tancada. Només falta aplicar la solució.

  1. Cas C: el healthcheck en unhealthy

Símptoma: docker ps mostra Up 40 seconds (unhealthy) a aurora-api. Què significa exactament i d'on surt aquest veredicte?

docker inspect -f '{{.State.Health.Status}} | {{len .State.Health.Log}} intents | {{.State.Health.FailingStreak}} fallades seguides' aurora-api
unhealthy | 5 intents | 5 fallades seguides

L'històric complet és a .State.Health.Log, un array amb els últims cinc intents:

docker inspect -f '{{json .State.Health}}' aurora-api | jq '.Log[-1]'
{
  "Start": "2026-08-04T20:31:12.114Z",
  "End": "2026-08-04T20:31:12.287Z",
  "ExitCode": 1,
  "Output": "Connecting to localhost:3000 (127.0.0.1:3000)\nwget: server returned error: HTTP/1.1 503\n"
}

Llegeix-ho amb calma, perquè explica tota la història:

Camp Valor Interpretació
Start / End 0,173 s de diferència La comprovació no va expirar: respon ràpid, però malament
ExitCode 1 La comanda del HEALTHCHECK va retornar error. Qualsevol valor diferent de 0 és una fallada
Output HTTP/1.1 503 L'API va contestar, amb un 503

Aquest 503 no és casual: el vas escriure tu a la lliçó 01-07. L'endpoint /salut retorna 200 només si la base de dades i la memòria cau responen, i 503 en qualsevol altre cas:

curl -s -w "\nHTTP %{http_code}\n" localhost:3000/salut
{"servei":"aurora-api","version":"1.0.0","db":"ko","cache":"ko","errorDb":"getaddrinfo ENOTFOUND aurora-db","errorCache":"The client is closed"}
HTTP 503

I ara el matís que separa un unhealthy real d'un fals positiu:

El contenidor està sa; el servei no. El procés node viu, escolta al 3000 i respon en 173 mil·lisegons. El que està trencat són les seves dependències. El HEALTHCHECK està fent exactament el que li vas demanar: informar que aquest contenidor no està en condicions d'atendre trànsit.

Els quatre estats de salut possibles:

Estat Quan apareix
starting Durant el --start-period (10 s al teu Dockerfile). Les fallades aquí no compten
healthy L'última comprovació va retornar 0
unhealthy Ha fallat --retries vegades seguides (3 en el teu cas)
(cap) La imatge no defineix HEALTHCHECK — el cas de postgres:16-alpine

I el detall que sorprèn tothom: unhealthy no fa res per si sol. Docker Engine no reinicia el contenidor, no el treu de rotació ni avisa ningú; es limita a marcar-lo i a emetre un esdeveniment health_status. Qui actua sobre aquesta informació és un orquestrador (Swarm a la lliçó 06-03, Kubernetes a la 06-05) o tu, mirant docker ps. Ni tan sols la política --restart reacciona a un unhealthy, i aquest matís s'explica a la lliçó 03-07.

docker events --since 5m --filter event=health_status --filter container=aurora-api
2026-08-04T20:31:12.311 container health_status: unhealthy 4c8f2a1e9b73 (name=aurora-api)

Deixa aurora-api tal com està: a la lliçó vinent es posarà healthy tot sol.

Errors Habituals i Consells

  • Començar per entrar al contenidor. El 70 % de les avaries es veuen a docker logs en deu segons. Segueix l'ordre: estat → registres → configuració → dins.
  • Depurar amb --rm. Si el contenidor s'esborra en morir, no hi ha registres, ni inspect, ni autòpsia. Durant la investigació, sense --rm.
  • Buscar registres d'una aplicació que escriu en un fitxer. docker logs buit no significa "no passa res": comprova on escriu realment el procés.
  • Instal·lar eines al contenidor "per arreglar-lo". Es perden en recrear-lo. Per diagnosticar, un contenidor efímer amb netshoot; per arreglar, el Dockerfile.
  • Confondre ECONNREFUSED amb ENOTFOUND. El primer diu "el nom s'ha resolt però ningú no escolta allà"; el segon, "no sé qui és aquest". Són dues avaries diferents i porten a llocs diferents.
  • Llegir docker stats sense límits configurats. La columna LIMIT mostra tota la RAM del host i el MEM % resulta enganyosament tranquil·litzador.
  • Creure que unhealthy reinicia alguna cosa. Docker Engine només el marca. Sense orquestrador, ningú no actua.
  • Bolcar docker inspect sencer al terminal. Són 200 línies de JSON. Fes servir --format o jq i vés directe a la dada.
  • Consell: desa àlies per a les consultes d'inspect que repeteixis. Per exemple dip() { docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}} {{end}}' "$1"; }.
  • Consell: docker events en un segon terminal mentre reprodueixes la fallada. Veure el die amb el seu exitCode a l'instant exacte val més que deu conjectures.

Exercicis

Exercici 1: autòpsia d'un contenidor mort

Llança aquest contenidor, que fallarà a propòsit, i fes la investigació completa sense tornar-lo a executar:

docker run -d --name autopsia -e MODE=produccio alpine:3.20 \
  sh -c 'echo "[init] arrencant en mode $MODE"; echo "[init] falta la variable API_KEY" >&2; sleep 2; exit 78'

Respon amb comandes concretes: en quin estat està i amb quin codi va sortir?, aquest codi apunta a Docker o a l'aplicació?, què va escriure a stdout i què a stderr, per separat?, quines variables d'entorn tenia configurades?, quina era la seva comanda efectiva?, quant de temps va estar viu (calcula-ho amb StartedAt i FinishedAt)?

Exercici 2: diagnostica una xarxa sense eines

Arrenca un contenidor web-mut amb nginx:alpine sense publicar cap port. Després, sense instal·lar-hi res a dins i sense recrear-lo:

  1. Esbrina la seva adreça IP.
  2. Comprova des d'un contenidor efímer de netshoot que Nginx respon al seu port 80.
  3. Demostra que des del host, amb curl localhost:80, no respon, i explica per què això no contradiu el punt anterior.
  4. Esbrina quins processos corren a dins sense fer servir docker exec.

Exercici 3: el healthcheck que menteix

Crea una imatge aurora-api:fals-sa a partir d'auroralibros/aurora-api:1.2.0 que substitueixi el HEALTHCHECK per un que comprovi simplement que el procés existeix (CMD pgrep node || exit 1). Arrenca-la sense base de dades ni memòria cau i compara, amb comandes, l'estat de salut que reporta enfront del d'auroralibros/aurora-api:1.2.0 en les mateixes condicions. Després respon: quin dels dos healthchecks és "millor"?, en quina situació concreta el segon t'hauria evitat un incident i en quina t'hauria donat una falsa alarma?

Solucions

Solució a l'exercici 1

docker ps -a --filter name=autopsia --format "table {{.Names}}\t{{.Status}}"
docker inspect -f '{{.State.Status}} / codi {{.State.ExitCode}}' autopsia
NAMES      STATUS
autopsia   Exited (78) 30 seconds ago
exited / codi 78

El codi 78 està al rang 1–124, així que és de l'aplicació, no de Docker. Un 125 hauria significat un error del mateix docker run, un 127 un executable inexistent i un 137 un senyal. Aquí, el programa va decidir sortir amb aquest número, així que l'explicació és al seu codi i als seus registres.

echo "--- stdout ---"; docker logs autopsia 2>/dev/null
echo "--- stderr ---"; docker logs autopsia 1>/dev/null
--- stdout ---
[init] arrencant en mode produccio
--- stderr ---
[init] falta la variable API_KEY

La separació de fluxos és el que dona el diagnòstic: a stdout hi ha soroll informatiu i a stderr hi ha la causa real. En un registre entremesclat de 500 línies, aquest filtre és la diferència entre trobar-ho i no trobar-ho.

docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' autopsia
docker inspect -f 'Path: {{.Path}} | Args: {{.Args}}' autopsia
MODE=produccio
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

Path: sh | Args: [-c echo "[init] arrencant en mode $MODE"; echo "[init] falta la variable API_KEY" >&2; sleep 2; exit 78]

Confirmat: MODE estava definida i API_KEY no apareix enlloc, que és just el que denunciava el stderr.

docker inspect -f 'Inici: {{.State.StartedAt}}{{println}}Fi:    {{.State.FinishedAt}}' autopsia
docker rm autopsia
Inici: 2026-08-04T20:52:03.417218Z
Fi:    2026-08-04T20:52:05.583904Z

Va viure 2,17 segons, coherents amb el sleep 2 de la comanda. Aquest càlcul és més útil del que sembla: un contenidor que viu mil·lisegons sol fallar en arrencar; un que viu hores i després mor apunta a una fuita de memòria o a un esdeveniment extern.

Solució a l'exercici 2

docker run -d --name web-mut nginx:alpine
docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' web-mut
172.17.0.5

2. Comprovar que respon, des de netshoot:

docker run --rm nicolaka/netshoot curl -s -o /dev/null -w "HTTP %{http_code}\n" http://172.17.0.5
HTTP 200

Nginx funciona perfectament. Una altra manera, compartint directament la seva pila de xarxa:

docker run --rm --network container:web-mut nicolaka/netshoot \
  sh -c 'ss -tlnp; curl -s -o /dev/null -w "HTTP %{http_code}\n" localhost'
State  Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0      511          0.0.0.0:80        0.0.0.0:*
HTTP 200

3. Des del host:

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

No hi ha cap contradicció: el contenidor escolta al port 80 de la seva pròpia pila de xarxa, però com que no es va publicar cap port amb -p, no existeix cap regla que reenviï el port 80 del host cap a ell. La connectivitat existeix dins de la xarxa de Docker (on viu netshoot) i no existeix des del host. És just la distinció entre "publicar un port" i "que dos contenidors es parlin" que es formalitza a la lliçó 03-05.

4. Processos sense docker exec:

docker top web-mut
UID     PID     PPID    C   STIME   TTY   TIME       CMD
root    26104   26082   0   21:03   ?     00:00:00   nginx: master process nginx -g daemon off;
101     26155   26104   0   21:03   ?     00:00:00   nginx: worker process

docker top l'executa el dimoni al host, així que no necessita entrar al contenidor ni que la imatge tingui ps. I de propina es veu la bona pràctica de Nginx: el master corre com a root i els workers com l'usuari 101.

docker rm -f web-mut

Solució a l'exercici 3

# ~/aurora-libros/api/Dockerfile.fals-sa
FROM auroralibros/aurora-api:1.2.0
# pgrep ve amb BusyBox, així que no cal instal·lar res
HEALTHCHECK --interval=10s --timeout=3s --start-period=5s --retries=3 \
  CMD pgrep node || exit 1
docker build -f ~/aurora-libros/api/Dockerfile.fals-sa -t aurora-api:fals-sa ~/aurora-libros/api
docker run -d --name api-fals-sa --env-file ~/aurora-libros/aurora.env aurora-api:fals-sa
sleep 45
docker ps --filter name=api --format "table {{.Names}}\t{{.Status}}"
NAMES            STATUS
api-fals-sa      Up 45 seconds (healthy)
aurora-api       Up 15 minutes (unhealthy)

Dos contenidors idèntics en tot llevat de la seva comprovació de salut, i veredictes oposats. El de l'esquerra diu estar sa; cap dels dos no pot servir ni un sol llibre.

docker inspect -f '{{json .State.Health}}' api-fals-sa | jq '.Log[-1] | {ExitCode, Output}'
curl -s -o /dev/null -w "HTTP %{http_code}\n" localhost:3000/salut
{ "ExitCode": 0, "Output": "1\n" }

El healthcheck fals respon 0 perquè pgrep node troba el procés. I té tota la raó: el procés està viu. Simplement està mesurant el que no importa.

Les respostes:

  • El d'auroralibros/aurora-api:1.2.0 és millor en aquest escenari, perquè comprova la capacitat real de donar servei (una petició HTTP d'extrem a extrem, que al seu torn consulta la base de dades i la memòria cau) en comptes de la mera existència d'un procés.
  • Quan t'hauria salvat el segon (pgrep): quan la caiguda és d'una dependència i no vols que totes les teves rèpliques es declarin malaltes alhora. Si PostgreSQL cau 30 segons, amb el healthcheck estricte les deu rèpliques de l'API passen a unhealthy simultàniament, l'orquestrador les treu de rotació i et quedes amb un tall total en comptes d'un servei degradat que encara serveix el que hi ha a la memòria cau. És el conegut efecte dòmino dels healthchecks massa profunds.
  • Quan t'hauria donat una falsa alarma... o pitjor, cap: quan el procés de Node està viu però el seu bucle d'esdeveniments està bloquejat, el pool de connexions esgotat o responent 500 a tot. pgrep diria "sa" indefinidament mentre els usuaris veuen errors. Aquest escenari és literalment el que va motivar HEALTHCHECK a la lliçó 02-04.

La solució professional, que ja apuntaves a l'exercici 3 de la lliçó 02-04, és separar dos endpoints: un de vivacitat (/salut/viu, superficial, per decidir reinicis) i un altre de disponibilitat (/salut, profund, per decidir si rep trànsit). És la distinció entre liveness i readiness que es desenvolupa a la lliçó 06-05.

docker rm -f api-fals-sa
docker image rm aurora-api:fals-sa

Conclusió

Tens un mètode, i això val més que la llista de comandes: està corrent? → què diuen els registres? → com està configurat? → què passa a dins?, en aquest ordre i sense saltar-se passos. Saps que un Created assenyala un executable inexistent, que un 125 significa que el contenidor ni es va crear i no hi ha res a inspeccionar, i que un codi entre 1 i 124 mena directament a docker logs.

Manegues docker logs amb les seves finestres de temps, el seu --tail i el seu -f que pots tallar amb Ctrl+C sense por, i tens gravada la regla d'or: els contenidors registren a stdout i stderr, mai en un fitxer, i saps separar els dos fluxos amb una redirecció perquè l'error aparegui sol. Entres amb docker exec triant el shell correcte segons la base, saps que -u root et dona privilegis encara que la imatge declari USER node, i —el més valuós— saps depurar imatges mínimes sense eines llançant un contenidor efímer de netshoot que comparteix els namespaces de xarxa i de processos del malalt, sense embrutar la imatge de producció.

Del JSON de docker inspect n'extreus la dada exacta amb plantilles Go i jq, amb una taula de consultes per a IP, xarxes, ports, muntatges, entorn, salut, codi de sortida i reinicis, i saps que aquesta mateixa comanda mostra les contrasenyes en clar, raó per la qual les variables d'entorn no són un mecanisme de secrets. Amb docker top veus els processos des del host encara que la imatge no tingui ps, amb docker stats interpretes les set columnes —i has descobert que els teus contenidors no tenen cap límit de memòria— i amb docker events reconstrueixes què va passar i quan.

I has tancat la investigació que arrossegaves des del mòdul 2. aurora-api, aurora-db i aurora-cache són a la mateixa xarxa i amb connectivitat plena entre ells: nc -zv 172.17.0.2 5432 respon succeeded. El que falla és una sola cosa, i ara la saps anomenar amb precisió: nslookup aurora-db retorna NXDOMAIN, perquè la xarxa bridge per defecte no té DNS intern entre contenidors. No és un tallafocs, ni un port, ni PostgreSQL: és que l'API no sap com es diu la seva base de dades.

Només falta aplicar la solució, i és més curta que el diagnòstic. A la lliçó següent, Xarxes a Docker, veuràs per què cada contenidor té la seva pròpia pila de xarxa i el seu propi localhost, compararàs els drivers bridge, host i none, i entendràs la diferència decisiva entre la bridge per defecte i una xarxa definida per l'usuari, on el DNS intern resol noms de contenidor automàticament. Crearàs aurora-net, recrearàs els tres serveis dins seu, i executaràs per fi aquell curl http://localhost:3000/llibres que fa sis lliçons que espera per retornar-te El jardín de senderos que se bifurcan, Rayuela i els altres sis títols. Després afegiràs aurora-web com a proxy invers, i la plataforma d'Aurora Libros estarà viva de debò.

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