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
- Un mètode abans que una comanda
docker logsa fons- La regla d'or: stdout i stderr
docker exec: entrar al contenidor- Depurar imatges mínimes sense eines
docker inspect: el JSON complet del contenidordocker top: els processos de dinsdocker stats: consum en temps realdocker events: el flux del dimoni- Cas A: el contenidor surt amb codi 1
- Cas B:
ECONNREFUSEDa/llibres - Cas C: el healthcheck en
unhealthy
- 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.
docker logs a fons
docker logs a fonsPostgreSQL 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 connectionsLes 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 logsfunciona amb el contenidor aturat. Els registres viuen al host i sobreviuen adocker stop. Només desapareixen ambdocker rm. És la raó per la qual--rmés el teu enemic mentre depures.--tailsense-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+Cnomés interromp el visor: no toca el contenidor. A diferència dedocker attach, aquíCtrl+Cés completament segur.
- 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'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-fluxos2D'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:
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/0Si 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.
docker exec: entrar al contenidor
docker exec: entrar al contenidor/ # psql -U aurora -d aurora_llibres -c "SELECT COUNT(*) FROM llibres;"
count
-------
8
(1 row)
/ # exitJa 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:
OCI runtime exec failed: exec failed: unable to start container process:
exec: "bash": executable file not found in $PATH: unknownUn truc que funciona gairebé sempre:
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:
-u rootet dona privilegis dins del contenidor encara que la imatge declari un altre usuari. La restricció d'USERprotegeix del codi que corre a dins, no de qui controla el dimoni de Docker.- Instal·lar eines amb
apk adddins 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, aquellcurldesapareix. El que s'arregla, s'arregla al Dockerfile. - I aquí tens el diagnòstic servit en safata:
errorDb: getaddrinfo ENOTFOUND aurora-db. L'endpoint/salutque 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:0Fixa'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.
- 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/netshootEl 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-dbQuatre conclusions en quatre comandes, sense instal·lar res a la imatge de producció:
ss -tlnpconfirma que l'API sí que escolta al 3000, a0.0.0.0(bé: no a127.0.0.1, que seria inabastable des de fora).ps -efmostra elnode server.jsdel contenidor veí gràcies a--pid container:, i a més que corre amb UID 1000.nslookup aurora-dbrespon NXDOMAIN: el servidor DNS intern de Docker (127.0.0.11) existeix i respon, però no coneix aquest nom.curlfunciona encara que la imatge de l'API no tinguicurl, 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:
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 inspect: el JSON complet del contenidor
docker inspect: el JSON complet del contenidordocker inspect retorna tot el que Docker sap d'un contenidor: unes 200 línies de JSON.
[
"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-cacheDues 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/dataNota 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.
docker top: els processos de dins
docker top: els processos de dinsUID 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 launcherDues particularitats que el fan especial:
- Els PID són els del host, no els de dins del contenidor.
docker exec aurora-db psmostraria el mateixpostgrescom 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 imatgesdistroless.
Serveix per respondre ràpid a: quants processos hi ha?, hi ha processos zombis?, l'aplicació està llançant fills que no hauria?
docker stats: consum en temps real
docker stats: consum en temps realCONTAINER 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 6Sense --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)
docker events: el flux del dimoni
docker events: el flux del dimoni2026-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_statusEls 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).
- 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-apiCodi 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?
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-apiAquí 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.0Una 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 -qLa 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.
- Cas B:
ECONNREFUSED a /llibres
ECONNREFUSED a /llibresAquí hi ha el cas que arrossegues des del mòdul 2. El resoldrem amb el mètode complet.
Pregunta 1: està corrent?
Tots tres estan corrent. No és un problema d'arrencada.
Pregunta 2: què diuen els registres?
[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.3I 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:
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.confEl 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ó.
- Cas C: el healthcheck en
unhealthy
unhealthySí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-apiL'històric complet és a .State.Health.Log, un array amb els últims cinc intents:
{
"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:
{"servei":"aurora-api","version":"1.0.0","db":"ko","cache":"ko","errorDb":"getaddrinfo ENOTFOUND aurora-db","errorCache":"The client is closed"}
HTTP 503I ara el matís que separa un unhealthy real d'un fals positiu:
El contenidor està sa; el servei no. El procés
nodeviu, escolta al 3000 i respon en 173 mil·lisegons. El que està trencat són les seves dependències. ElHEALTHCHECKestà 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.
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 logsen deu segons. Segueix l'ordre: estat → registres → configuració → dins. - Depurar amb
--rm. Si el contenidor s'esborra en morir, no hi ha registres, niinspect, ni autòpsia. Durant la investigació, sense--rm. - Buscar registres d'una aplicació que escriu en un fitxer.
docker logsbuit 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
ECONNREFUSEDambENOTFOUND. 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 statssense límits configurats. La columna LIMIT mostra tota la RAM del host i elMEM %resulta enganyosament tranquil·litzador. - Creure que
unhealthyreinicia alguna cosa. Docker Engine només el marca. Sense orquestrador, ningú no actua. - Bolcar
docker inspectsencer al terminal. Són 200 línies de JSON. Fes servir--formatojqi vés directe a la dada. - Consell: desa àlies per a les consultes d'
inspectque repeteixis. Per exempledip() { docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}} {{end}}' "$1"; }. - Consell:
docker eventsen un segon terminal mentre reprodueixes la fallada. Veure eldieamb el seuexitCodea 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:
- Esbrina la seva adreça IP.
- Comprova des d'un contenidor efímer de
netshootque Nginx respon al seu port 80. - Demostra que des del host, amb
curl localhost:80, no respon, i explica per què això no contradiu el punt anterior. - 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}}' autopsiaEl 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/nullLa 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}}' autopsiaMODE=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 autopsiaVa 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-mut2. Comprovar que respon, des de netshoot:
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'3. Des del host:
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:
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 processdocker 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.
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 1docker 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}}"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/salutEl 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 aunhealthysimultà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.
pgrepdiria "sa" indefinidament mentre els usuaris veuen errors. Aquest escenari és literalment el que va motivarHEALTHCHECKa 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.
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
- 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
