La lliçó anterior va acabar amb una pregunta incòmoda: aurora-db i aurora-cache fa minuts que corren tan tranquils, però aurora-api va morir al cap de dos segons amb codi de sortida 1. I si proves docker run -d ubuntu:24.04, veuràs una cosa encara més desconcertant: el contenidor s'atura tot sol, sense error, sense registre i sense que ningú el toqui. No és una fallada de Docker; és la conseqüència directa de la regla més important de tot el mòdul: un contenidor viu exactament el que visqui el seu procés PID 1. Ni un segon més.
En aquesta lliçó recorreràs els set estats pels quals pot passar un contenidor i les transicions entre ells, entendràs què passa de debò quan escrius docker stop —la seqüència SIGTERM, període de gràcia, SIGKILL—, mesuraràs amb un cronòmetre per què la forma shell del CMD costa sempre deu segons, i aprendràs a llegir els codis de sortida: aquest 1, i també el 125, el 137 i el 143, que deixen de ser números críptics per convertir-se en diagnòstics. I ensenyaràs a server.js a apagar-se com cal, tancant el pool de PostgreSQL i el client de Redis abans de marxar.
Contingut
- Els estats d'un contenidor
- El PID 1 i la regla fonamental
- Per què
ubuntumor inginxno start,stopirestartpauseiunpause: congelar sense matarkilli l'enviament de senyals concrets- Aturada ordenada enfront d'aturada forçada
- La forma shell del
CMDi els seus deu segons, mesurats - Codis de sortida i la seva taula de diagnòstic
- Aturada ordenada d'
aurora-api docker waitidocker rename
- Els estats d'un contenidor
Un contenidor no està simplement "encès" o "apagat". Docker maneja set estats:
stateDiagram-v2
[*] --> created: docker create<br/>(o la 1a meitat de docker run)
created --> running: docker start
running --> paused: docker pause
paused --> running: docker unpause
running --> exited: el PID 1 acaba<br/>docker stop / docker kill
running --> restarting: política de reinici<br/>(lliçó 03-07)
restarting --> running: reintent correcte
restarting --> exited: s'esgoten els reintents
exited --> running: docker start
exited --> removing: docker rm
paused --> exited: docker stop
removing --> [*]: contenidor eliminat
running --> dead: fallada del dimoni<br/>o del sistema de fitxers
dead --> removing: docker rm -f
I la taula que necessites tenir a mà:
| Estat | Com s'hi arriba | Hi ha procés? | Consumeix RAM/CPU? | Què es conserva |
|---|---|---|---|---|
created |
docker create |
No | No | Capa d'escriptura buida i configuració |
running |
docker start, docker run, unpause |
Sí | Sí | Tot |
paused |
docker pause |
Sí, congelat | RAM sí, CPU no | Tot, inclosa la memòria del procés |
restarting |
Política --restart després d'una fallada |
Transitòriament no | Poc | Tot |
exited |
El PID 1 acaba, docker stop, docker kill |
No | No | Capa d'escriptura, registres i configuració |
dead |
El dimoni no el va poder eliminar correctament | No | No | Restes; només es pot esborrar |
removing |
docker rm en curs |
No | No | Res, és transitori |
Els dos estats que més es confonen són exited i removing, i la diferència és enorme:
- Un contenidor
exitedcontinua existint. Ocupa el seu nom, conserva la seva capa d'escriptura amb tots els fitxers que va escriure, desa els seus registres i la seva configuració, i el pots tornar a arrencar ambdocker start. És un contenidor adormit, no un contenidor esborrat. docker rmés el que l'elimina de debò, i amb ell se'n van la capa d'escriptura i els registres per sempre.
Consulta l'estat de qualsevol contenidor amb precisió:
docker inspect --format '{{.State.Status}}' aurora-db
docker inspect --format 'Estat: {{.State.Status}} | PID: {{.State.Pid}} | Arrencat: {{.State.StartedAt}}' aurora-dbAquest PID: 24817 és l'identificador del procés a la teva màquina: els contenidors no són màquines virtuals, són processos del host aïllats amb namespaces, com vas veure a la lliçó 01-01.
- El PID 1 i la regla fonamental
Dins del seu namespace de processos, la comanda principal del contenidor es veu a si mateixa com a PID 1. Comprova-ho:
PostgreSQL és el PID 1 dins d'aurora-db. I d'aquí en surt la regla:
El contenidor existeix mentre existeixi el seu PID 1. Quan aquest procés acaba —bé o malament, amb error o sense—, el contenidor passa a
exited. Tant se val que a dins hi hagués cent processos fills: tots moren amb ell.
Ser PID 1 té dues implicacions que es noten en el dia a dia:
| Implicació | Conseqüència pràctica |
|---|---|
| El PID 1 no té gestors de senyals per defecte per a SIGTERM en alguns llenguatges | Un procés que ignori SIGTERM es queda penjat fins al SIGKILL |
| El PID 1 és responsable de recollir els processos zombis | Un PID 1 que no ho faci acumula zombis; es resol amb --init |
L'opció --init injecta un init mínim (tini) com a PID 1, que reenvia senyals i recull zombis:
Ara nginx és el PID 7 i docker-init és l'1. Per a aurora-api no cal: node és un PID 1 correcte sempre que facis servir la forma exec, com fas des de la lliçó 02-04.
- Per què
ubuntu mor i nginx no
ubuntu mor i nginx noAquesta és la demostració que aclareix de cop el 80 % dels "el meu contenidor s'atura tot sol":
docker run -d --name prova-ubuntu ubuntu:24.04
docker run -d --name prova-nginx nginx:alpine
sleep 2
docker ps -a --filter name=prova- --format "table {{.Names}}\t{{.Status}}\t{{.Command}}"NAMES STATUS COMMAND
prova-nginx Up 2 seconds "/docker-entrypoint.…"
prova-ubuntu Exited (0) 2 seconds ago "/bin/bash"La diferència és a la columna COMMAND:
- La imatge
ubuntu:24.04téCMD ["/bin/bash"]. Unbashsense terminal i sense entrada no té res per llegir: arriba al final de la seva entrada estàndard i acaba correctament, amb codi 0. El contenidor no ha fallat; ha fet exactament el que li vas demanar, que era executar un shell que no tenia res a fer. - La imatge
nginx:alpineexecutanginx -g "daemon off;". Aquestdaemon offés la clau: obliga Nginx a quedar-se en primer pla en comptes de convertir-se en dimoni de fons. El procés no acaba mai, així que el contenidor tampoc.
Fes que el d'Ubuntu visqui donant-li alguna cosa a fer:
docker rm prova-ubuntu
docker run -d --name prova-ubuntu ubuntu:24.04 sleep 60
docker ps --filter name=prova-ubuntu --format "{{.Names}}: {{.Status}}"O dona-li un terminal, que és el que feies a la lliçó 01-06 amb -it:
docker rm -f prova-ubuntu
docker run -d -it --name prova-ubuntu ubuntu:24.04
docker ps --filter name=prova-ubuntu --format "{{.Names}}: {{.Status}}"Amb -it, bash té un terminal obert esperant entrada i no acaba. D'aquí en surt l'error clàssic:
Un contenidor no és una màquina que encens: és un procés que executes. Si vols que duri, dona-li un procés que duri. I l'error invers —"poso
daemon ona Nginx perquè funcioni com al meu servidor"— converteix el contenidor en un suïcidi immediat: Nginx se'n va al fons, el procés original acaba i Docker dona el contenidor per acabat.
És també l'explicació del que li va passar a aurora-api: el seu PID 1, node, va fer process.exit(1) en no poder connectar amb Redis. El procés va acabar, així que el contenidor va acabar.
start, stop i restart
start, stop i restart| Comanda | Què fa | Mateix contenidor? | Mateix PID? |
|---|---|---|---|
docker stop |
SIGTERM, espera, SIGKILL | — | — |
docker start |
Torna a llançar la comanda original | Sí, mateix ID i mateixa capa d'escriptura | No, PID nou |
docker restart |
stop + start en un sol pas |
Sí | No, PID nou |
L'important és entendre què sobreviu a un restart i què no, perquè és una font constant de sorpreses:
docker exec aurora-cache redis-cli SET llibre:preferit "Rayuela"
docker exec aurora-cache sh -c 'echo "nota temporal" > /tmp/nota.txt'
docker restart aurora-cache
sleep 2
docker exec aurora-cache cat /tmp/nota.txt
docker exec aurora-cache redis-cli GET llibre:preferitDos resultats oposats a la mateixa comanda:
/tmp/nota.txthi continua sent: està escrit a la capa d'escriptura del contenidor, que sobreviu a aturades i arrencades. Només desapareix ambdocker rm.- La clau de Redis ha desaparegut: vivia a la memòria del procés, i el procés és nou. Redis sense persistència configurada perd tot el que té en reiniciar-se.
Aquesta distinció entre "el que està en disc dins del contenidor" i "el que està a la memòria del procés" és imprescindible, i encara falta una tercera categoria —"el que està fora del contenidor i sobreviu fins i tot a docker rm"—, que és el tema de la lliçó 03-06.
També es poden operar diversos contenidors alhora:
Un avís sobre docker start: no accepta canvis de configuració. docker start -p 5433:5432 aurora-db no existeix. Torna a arrencar el contenidor tal com es va crear, amb els seus ports, variables i muntatges originals. És la conseqüència de la separació create/start de la lliçó anterior.
I una opció útil de start:
-a (--attach) arrenca el contenidor i enganxa el teu terminal a la seva sortida, com si l'haguessis llançat en primer pla. Serveix per veure l'arrencada d'un servei que sospites que falla.
pause i unpause: congelar sense matar
pause i unpause: congelar sense matardocker pause congela tots els processos del contenidor fent servir el freezer de cgroups. Ni el procés se n'assabenta: no rep cap senyal, simplement deixa de rebre temps de CPU. I des de fora:
Aquesta comanda es queda penjada indefinidament: el contenidor no pot respondre perquè no s'està executant. Interromp-la amb Ctrl+C i descongela'l:
pause |
stop |
|
|---|---|---|
| El procés | Congelat, continua existint | Acabat |
| La memòria del procés | Es conserva íntegra | Es perd |
| Senyals enviats | Cap | SIGTERM i, si cal, SIGKILL |
| Connexions de xarxa obertes | Es mantenen, però sense resposta (acaben expirant) | Es tanquen |
| En reprendre | Continua exactament on era | Arrenca de zero |
| Consum de RAM | Continua ocupada | Alliberada |
Usos reals de pause: alliberar CPU momentàniament sense perdre l'estat d'un procés llarg, congelar un contenidor mentre fas una còpia de seguretat coherent dels seus fitxers, o aturar temporalment una aplicació mentre en diagnostiques una altra. No serveix per estalviar memòria: la RAM continua ocupada.
kill i l'enviament de senyals concrets
kill i l'enviament de senyals concretsdocker kill aurora-cache
docker ps -a --filter name=aurora-cache --format "{{.Names}}: {{.Status}}"
docker start aurora-cachedocker kill envia SIGKILL immediatament, sense període de gràcia. El procés no la pot capturar, ignorar ni negociar: el nucli el termina a l'acte. Per això el codi de sortida és 137, del qual parlarem a l'apartat 9.
Però docker kill serveix per a molt més que matar, perquè envia qualsevol senyal:
| Senyal | Ús típic en contenidors |
|---|---|
SIGTERM (15) |
Petició cortesa d'acabar. És la que envia docker stop |
SIGKILL (9) |
Terminació immediata i incapturable. La que envia docker kill per defecte |
SIGHUP (1) |
Recarregar configuració sense reiniciar (Nginx, HAProxy) |
SIGQUIT (3) |
Aturada ordenada a Nginx; bolcat de fils a la JVM |
SIGUSR1/SIGUSR2 (10/12) |
Senyals a mida: rotar registres a Nginx, bolcar el heap a Node |
SIGINT (2) |
El que envia Ctrl+C |
Un exemple real: recarregar la configuració de Nginx sense tallar ni una connexió.
docker run -d --name recarrega-demo -p 8080:80 nginx:alpine
docker kill -s SIGHUP recarrega-demo
docker ps --filter name=recarrega-demo --format "{{.Names}}: {{.Status}}"Continua Up: el senyal no l'ha matat, Nginx l'ha interpretat com a "rellegeix la teva configuració". El faràs servir a la lliçó 03-05 quan muntis aurora-web com a proxy invers.
- Aturada ordenada enfront d'aturada forçada
Aquest és l'apartat central de la lliçó. docker stop no mata el contenidor de cop: executa una seqüència de tres passos.
sequenceDiagram
participant U as Tu
participant D as dockerd
participant P as PID 1 del contenidor
U->>D: docker stop aurora-api
D->>P: 1. SIGTERM (o el STOPSIGNAL de la imatge)
Note over P: Període de gràcia: 10 s per defecte
alt El procés acaba a temps
P-->>D: Tanca connexions, allibera recursos i surt
D-->>U: Contenidor exited amb el seu codi
else El procés no respon
Note over D,P: S'esgoten els 10 s
D->>P: 2. SIGKILL (incapturable)
P-->>D: Terminació abrupta, codi 137
D-->>U: Contenidor exited (137)
end
Els tres passos són:
- Docker envia el STOPSIGNAL de la imatge (per defecte
SIGTERM) al PID 1. Recorda de la lliçó 02-04 que Nginx declaraSTOPSIGNAL SIGQUIT. - Espera el període de gràcia: 10 segons per defecte.
- Si el procés continua viu, envia SIGKILL.
El període de gràcia s'ajusta amb --time (o -t):
docker stop --time 30 aurora-db # espera fins a 30 segons
docker stop --time 0 aurora-cache # SIGKILL immediat, equival a docker killQuan convé pujar-lo? Quan el procés necessita temps per tancar bé: una base de dades bolcant la seva memòria intermèdia a disc, un treballador acabant la tasca que tenia entre mans, una API esperant que es completin les peticions en curs. Per a PostgreSQL, 30 segons és una xifra raonable; amb 10 podries forçar una recuperació en tornar a arrencar.
docker stop |
docker kill |
|
|---|---|---|
| Senyal inicial | STOPSIGNAL de la imatge (SIGTERM) |
SIGKILL (o el de -s) |
| Es pot capturar? | Sí | No |
| Període de gràcia | 10 s per defecte, ajustable amb -t |
Cap |
| Dades a la memòria intermèdia | El procés les pot bolcar | Es perden |
| Codi de sortida típic | 0 o 143 | 137 |
| Quan fer-lo servir | Sempre per defecte | Només si el procés no respon |
I el resum que convé memoritzar: stop demana, kill obliga. Un docker kill sobre PostgreSQL és l'equivalent a desendollar el servidor.
- La forma shell del
CMD i els seus deu segons, mesurats
CMD i els seus deu segons, mesuratsAquí arriba la factura d'una decisió que semblava cosmètica a la lliçó 02-03. La mesurarem amb un cronòmetre.
Crea una imatge de demostració que faci servir la forma shell:
# ~/aurora-libros/api/Dockerfile.shell — NOMÉS per a aquesta demostració
FROM auroralibros/aurora-api:1.1.0
ENTRYPOINT []
CMD echo "[arrencada] iniciant aurora-api" && node server.jsAra arrenca les dues versions. Com que l'API encara no troba les seves dependències, farem servir Nginx perquè la comparació sigui neta i reproduïble sense dependre de res:
# Forma exec: nginx és PID 1 i rep el senyal directament
docker run -d --name aturada-exec nginx:alpine
# Forma shell: sh és PID 1 i nginx és el seu fill
docker run -d --name aturada-shell nginx:alpine \
sh -c 'echo "[arrencada] iniciant nginx" && nginx -g "daemon off;"'
docker exec aturada-exec ps -o pid,comm | head -3
docker exec aturada-shell ps -o pid,comm | head -3Aquí hi ha la diferència, visible en una sola línia: al primer nginx és el PID 1; al segon, el PID 1 és sh i nginx és un fill seu. Cronometra les aturades:
Vuitanta vegades més lent. I els codis de sortida expliquen la resta de la història:
Reconstruïm el que ha passat al segon cas:
docker stopenvia SIGQUIT (el STOPSIGNAL de Nginx) al PID 1, que éssh.shno té cap gestor per a aquest senyal ni sap res de reenviar-lo als seus fills. L'ignora o acaba ell tot sol, perònginxno se n'assabenta de res.- Docker espera els seus deu segons complets.
- SIGKILL. Tot el contenidor mor de cop, amb les connexions tallades a mitja petició. Codi 137.
Un matís honest que gairebé ningú no menciona: alguns shells optimitzen el cas més simple. Si el CMD fos exactament CMD node server.js, molts shells (inclosos dash i l'ash de BusyBox) reemplacen el seu propi procés per node mitjançant exec, i el problema no apareix. Però n'hi ha prou amb que hi hagi dues comandes encadenades amb &&, una redirecció o una canonada —com a l'exemple, i com al 90 % dels CMD reals del món— perquè el shell hagi de quedar-se com a PID 1 i la fallada es manifesti. No val la pena jugar-se el comportament d'aturada a una optimització del shell:
Fes servir sempre la forma exec:
CMD ["node", "server.js"]. Si necessites la lògica d'un shell, escriu-la en undocker-entrypoint.shque acabi ambexec "$@", com vas fer a la lliçó 02-04.
Neteja la demostració:
- Codis de sortida i la seva taula de diagnòstic
Quan un contenidor acaba, deixa un número. Aquest número és la primera pista de qualsevol investigació.
docker ps -a --filter name=aurora --format "table {{.Names}}\t{{.Status}}"
docker inspect --format '{{.State.ExitCode}}' aurora-apiLa taula que resol la majoria dels casos:
| Codi | Significat | Causa habitual a la pràctica |
|---|---|---|
| 0 | Terminació correcta | El procés va fer la seva feina i va sortir. També bash sense entrada |
| 1 | Error genèric de l'aplicació | Excepció no capturada, process.exit(1), configuració invàlida |
| 125 | Fallada del mateix docker run |
Opció mal escrita, --env-file inexistent, nom duplicat. El contenidor ni s'ha creat |
| 126 | La comanda existeix però no s'ha pogut executar | Falta el bit d'execució (chmod +x), o s'ha intentat executar un directori |
| 127 | Comanda no trobada | Errada al CMD, o binari absent en una imatge mínima |
| 137 | 128 + 9 → SIGKILL | docker kill, esgotat el termini de docker stop, o l'OOM killer (lliçó 03-07) |
| 139 | 128 + 11 → SIGSEGV | Violació de segment: bug en codi natiu o binari incompatible amb l'arquitectura |
| 143 | 128 + 15 → SIGTERM | El procés va acabar per SIGTERM sense gestor propi. És una aturada normal |
La regla que explica la meitat de la taula: si el codi és més gran que 128, el procés va morir per un senyal, i el número del senyal és codi − 128. 137 − 128 = 9 (SIGKILL); 143 − 128 = 15 (SIGTERM); 139 − 128 = 11 (SIGSEGV).
I la distinció més important per depurar, que separa dos mons:
| Rang | Qui ha fallat | On buscar |
|---|---|---|
| 125, 126, 127 | Docker o l'arrencada de la comanda | A la teva línia de comandes i al Dockerfile |
| 1, 2, … , 124 | La teva aplicació | A docker logs |
| >128 | Un senyal extern o del nucli | A docker inspect (OOMKilled, Error) |
Els provocarem per veure'ls de debò:
docker run --name codi-0 alpine:3.20 true
docker run --name codi-1 alpine:3.20 sh -c 'exit 1'
docker run --name codi-127 alpine:3.20 comanda-que-no-existeix
docker run --opcio-inventada alpine:3.20
docker run --name codi-126 alpine:3.20 /etcdocker: Error response from daemon: failed to create task for container:
failed to create shim task: OCI runtime create failed: exec: "comanda-que-no-existeix": executable file not found in $PATH
unknown flag: --opcio-inventada
docker: Error response from daemon: failed to create task for container:
failed to create shim task: OCI runtime create failed: exec: "/etc": permission denieddocker ps -a --filter name=codi- --format "table {{.Names}}\t{{.Status}}"
echo "Sortida del docker run invàlid: $?"NAMES STATUS
codi-126 Created
codi-127 Created
codi-1 Exited (1) 8 seconds ago
codi-0 Exited (0) 12 seconds ago
125Observa el detall revelador: codi-127 i codi-126 es van quedar en estat Created, no Exited. El contenidor es va arribar a crear però mai no va arrencar, perquè l'executable no existia o no es podia executar. En canvi, codi-1 sí que va arrencar i la seva aplicació va decidir sortir amb error. És la diferència entre "no va poder començar" i "va començar i va fallar", i t'estalvia buscar al lloc equivocat.
Aplicat a Aurora Libros, l'Exited (1) de la lliçó anterior queda diagnosticat sense ambigüitat: l'aplicació va arrencar i va fallar ella mateixa, no va ser Docker ni un senyal. Els registres confirmen el motiu (getaddrinfo ENOTFOUND aurora-cache) i el process.exit(1) de server.js explica el número exacte.
- Aturada ordenada d'
aurora-api
aurora-apiHa arribat el moment d'arreglar dues coses alhora a server.js: que el procés no mori perquè una dependència opcional no estigui disponible, i que quan li demanin acabar ho faci tancant el que té obert.
Substitueix el bloc final de ~/aurora-libros/api/server.js (el que comença a // --- Arrencada ---) per aquest:
// --- Arrencada ---
let servidor;
async function arrencar() {
// La memòria cau és una dependència OPCIONAL: si Redis no respon, l'API ha de
// continuar servint el catàleg des de PostgreSQL, no morir a l'arrencada.
cache.connect().catch((err) =>
console.error('[cache] no disponible en arrencar:', err.message)
);
servidor = app.listen(PORT, '0.0.0.0', () => {
console.log(`[aurora-api] escoltant al port ${PORT}`);
console.log(`[aurora-api] base de dades: ${DB_HOST}:${DB_PORT}/${DB_NAME}`);
console.log(`[aurora-api] memòria cau: ${REDIS_HOST}:${REDIS_PORT}`);
});
}
// --- Aturada ordenada ---
let aturant = false;
async function aturar(senyal) {
if (aturant) return; // Un segon Ctrl+C no ha de reentrar aquí
aturant = true;
console.log(`[aurora-api] rebut ${senyal}, tancant ordenadament...`);
// Xarxa de seguretat: sortir pel nostre peu ABANS que arribi el SIGKILL.
// 8 segons < els 10 del període de gràcia de docker stop.
const forcar = setTimeout(() => {
console.error('[aurora-api] el tancament s\'ha encallat, sortida forçada');
process.exit(1);
}, 8000);
forcar.unref();
servidor.close(async () => {
try {
await pool.end();
console.log('[aurora-api] pool de PostgreSQL tancat');
if (cache.isOpen) {
await cache.quit();
console.log('[aurora-api] client de Redis tancat');
}
} catch (err) {
console.error('[aurora-api] error durant el tancament:', err.message);
}
console.log('[aurora-api] tancat netament');
process.exit(0);
});
}
process.on('SIGTERM', () => aturar('SIGTERM'));
process.on('SIGINT', () => aturar('SIGINT'));
arrencar().catch((err) => {
console.error('[aurora-api] fallada en arrencar:', err.message);
process.exit(1);
});Repassem les decisions una per una, perquè cadascuna respon a alguna cosa que has après en aquesta lliçó:
| Línia | Per què hi és |
|---|---|
cache.connect().catch(...) sense await |
L'arrencada no es bloqueja per una dependència opcional. El contenidor es queda running i el podràs estudiar, en comptes de morir amb codi 1 |
servidor = app.listen(...) desat en una variable |
Sense la referència al servidor no es pot cridar .close() |
if (aturant) return; |
Evita que dos senyals seguits llancin dos tancaments simultanis |
setTimeout(..., 8000) |
8 < 10: si alguna cosa s'encalla, sortim nosaltres abans del SIGKILL i amb un codi nostre, no amb un 137 |
.unref() |
Impedeix que aquest temporitzador mantingui viu el procés si el tancament va bé |
servidor.close(callback) |
Deixa d'acceptar connexions noves i espera que acabin les en curs |
await pool.end() |
Tanca les connexions a PostgreSQL. Sense això, la base de dades les manté obertes fins que expirin |
if (cache.isOpen) |
quit() sobre un client que mai no va connectar llançaria una excepció |
process.exit(0) |
Sortida correcta i explícita: codi 0, no 143 |
SIGINT a més de SIGTERM |
Perquè Ctrl+C en primer pla es comporti igual que docker stop |
Construeix la nova versió. En canviar el comportament sense trencar l'API, puja el número MENOR segons el que vas decidir a la lliçó 02-06:
docker build -t auroralibros/aurora-api:1.2.0 \
-t auroralibros/aurora-api:1.2 \
--build-arg VERSION=1.2.0 \
~/aurora-libros/apiI ara la comprovació cronometrada. Compara la versió antiga amb la nova:
# Versió 1.1.0: moria en arrencar. Amb la 1.2.0 el contenidor es manté viu.
docker run -d --name api-aturada --env-file ~/aurora-libros/aurora.env \
auroralibros/aurora-api:1.2.0
sleep 3
docker ps --filter name=api-aturada --format "{{.Names}}: {{.Status}}"El contenidor sobreviu. Ja no mor per no trobar Redis; simplement ho registra i continua. Ara atura'l mesurant el temps:
time docker stop api-aturada
docker logs --tail 6 api-aturada
docker inspect --format 'Codi de sortida: {{.State.ExitCode}}' api-aturadaapi-aturada
real 0m0.187s
[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] rebut SIGTERM, tancant ordenadament...
[aurora-api] pool de PostgreSQL tancat
[aurora-api] tancat netament
Codi de sortida: 0Les tres coses que buscàvem, confirmades en una sola sortida:
- 0,187 segons, enfront dels 10,271 de la forma shell. El senyal va arribar a
nodeperquè és PID 1 i la forma exec el deixa passar. - Els registres expliquen el tancament: el pool de PostgreSQL es va tancar explícitament abans de sortir. El client de Redis no hi apareix perquè mai no es va arribar a obrir (
cache.isOpenera fals), exactament com preveia el codi. - Codi de sortida 0, no 143. La diferència entre "el procés va ser acabat per un senyal" i "el procés va decidir acabar correctament perquè li ho van demanar bé".
En un orquestrador aquesta diferència és la que separa un desplegament sense errors d'un amb peticions tallades a mitges i connexions òrfenes a la base de dades.
docker wait i docker rename
docker wait i docker renameDues comandes petites que resolen problemes concrets.
docker wait
Bloqueja fins que el contenidor acabi i imprimeix el seu codi de sortida:
docker run -d --name tasca-lenta alpine:3.20 sh -c 'sleep 5; exit 3'
docker wait tasca-lenta
docker rm tasca-lentaEs queda cinc segons aturat i després imprimeix 3. És la peça que permet encadenar en un script: "espera que acabi la migració de la base de dades i continua només si va anar bé".
docker run -d --name migracio alpine:3.20 sh -c 'echo "migrant..."; sleep 3'
if [ "$(docker wait migracio)" -eq 0 ]; then
echo "Migració correcta, arrencant l'API"
else
echo "La migració ha fallat, avortant el desplegament"
fi
docker rm migraciodocker rename
Canvia el nom d'un contenidor en calent, sense aturar-lo ni recrear-lo:
docker rename aurora-cache aurora-cache-antic
docker ps --format "{{.Names}}: {{.Status}}" --filter name=aurora
docker rename aurora-cache-antic aurora-cacheÉs més útil del que sembla: en un desplegament sense tall, reanomenes el contenidor antic a aurora-api-antic, arrenques el nou amb el nom bo i esborres l'antic quan confirmes que tot va bé. I un avís que enllaça amb la lliçó 03-05: si un altre contenidor l'estava resolent per DNS amb el seu nom anterior, deixarà de trobar-lo tan bon punt el reanomenis.
Errors Habituals i Consells
- Creure que un contenidor
Exitedestà esborrat. Continua existint, ocupa el seu nom i conserva capa d'escriptura i registres. S'esborra ambdocker rm, no ambdocker stop. - Posar un servei en mode dimoni dins del contenidor.
nginxsensedaemon off,httpd -k start,postgresambpg_ctl start: el procés principal acaba immediatament i el contenidor s'atura. Els serveis en contenidors corren en primer pla, sempre. - Fer servir
docker killper costum. Un SIGKILL sobre PostgreSQL força una recuperació en arrencar i pot perdre el que hi hagués a la memòria intermèdia.docker stopprimer;killnomés si l'altre no respon. - No capturar SIGTERM a l'aplicació. El procés mor de cop amb les connexions obertes. Amb gestor, tanques el pool i surts amb codi 0.
- Posar el temporitzador de seguretat en 10 segons o més. Ha de ser menor que el període de gràcia; si no, mai no s'arriba a disparar perquè el SIGKILL arriba abans.
- Interpretar el 137 sempre com a "l'he matat jo". També el produeix l'OOM killer quan el contenidor es passa de memòria. Es distingeixen mirant
.State.OOMKilledadocker inspect(lliçó 03-07). - Confondre 125 amb 1. El 125 és de Docker: el contenidor ni tan sols existeix, així que no hi ha registres per mirar. Revisa la línia de comandes.
- Consell: augmenta el
--timea les bases de dades.docker stop --time 30 aurora-dbdona marge a PostgreSQL per fer el seu checkpoint i tancar-se netament. - Consell: fes servir
docker start -aquan un contenidor arrenqui i mori de seguida: veus l'arrencada en directe sense haver-la de perseguir ambdocker logs.
Exercicis
Exercici 1: recorre els set estats
Fent servir alpine:3.20 i un contenidor anomenat cicle-complet que executi sleep 600, porta el contenidor per aquesta seqüència i anota després de cada pas el resultat de docker inspect --format '{{.State.Status}} / codi {{.State.ExitCode}}': crear sense arrencar → arrencar → pausar → reprendre → aturar ordenadament → tornar a arrencar → matar → esborrar. Respon: quin és el codi de sortida després de docker stop i per què?, i després de docker kill?, en quin estat la memòria del procés continua ocupada?
Exercici 2: demostra la pèrdua de dades segons el tipus d'aturada
Amb un contenidor de redis:7-alpine anomenat cache-prova, escriu la clau cataleg:versio amb valor 1 i també un fitxer /tmp/marca.txt amb el mateix contingut. Després:
- Fes
docker restarti comprova què sobreviu de les dues coses. - Fes
docker stopseguit dedocker starti comprova el mateix. - Fes
docker rm -fi torna a crear un contenidor amb el mateix nom i imatge. Comprova el mateix.
Explica els resultats en termes de memòria del procés, capa d'escriptura i contenidor.
Exercici 3: mesura el cost de no gestionar senyals
Prepara tres contenidors a partir de node:22-alpine que executin aquests tres programes i cronometra docker stop en cadascun, anotant a més el codi de sortida:
- A:
node -e "setInterval(()=>{},1000)"— un procés que no fa res i no captura senyals. - B:
sh -c 'echo inici && node -e "setInterval(()=>{},1000)"'— el mateix, però embolcallat en un shell amb&&. - C:
node -e "process.on('SIGTERM',()=>{console.log('adeu');process.exit(0)});setInterval(()=>{},1000)"— amb gestor de SIGTERM.
Explica els tres temps i els tres codis, i digues quin dels tres es correspon amb auroralibros/aurora-api:1.1.0 i quin amb 1.2.0.
Solucions
Solució a l'exercici 1
docker create --name cicle-complet alpine:3.20 sleep 600
docker inspect --format '{{.State.Status}} / codi {{.State.ExitCode}}' cicle-completdocker start cicle-complet && docker inspect --format '{{.State.Status}} / codi {{.State.ExitCode}}' cicle-complet
docker pause cicle-complet && docker inspect --format '{{.State.Status}} / codi {{.State.ExitCode}}' cicle-complet
docker unpause cicle-complet && docker inspect --format '{{.State.Status}} / codi {{.State.ExitCode}}' cicle-complettime docker stop cicle-complet
docker inspect --format '{{.State.Status}} / codi {{.State.ExitCode}}' cicle-complet
docker start cicle-complet
docker kill cicle-complet
docker inspect --format '{{.State.Status}} / codi {{.State.ExitCode}}' cicle-complet
docker rm cicle-completLes tres respostes:
- Després de
docker stop: codi 143, és a dir, 128 + 15 = SIGTERM.sleepno instal·la cap gestor de senyals, així que se li aplica l'acció per defecte de SIGTERM: acabar immediatament. Per això elrealés de tot just 0,156 s i no va caldre el SIGKILL. És una aturada neta des del punt de vista de Docker, encara que el procés no executés cap lògica de tancament pròpia: exactament el cas d'aurora-api:1.1.0. - Després de
docker kill: codi 137 (128 + 9 = SIGKILL), de manera instantània i sense període de gràcia. Compara els dos números: el 143 significa "se li va demanar que acabés i va acabar"; el 137 significa "se'l va acabar sense preguntar". Un 137 acompanyat d'una espera de deu segons apuntaria a més a un procés que va ignorar la petició. - La memòria continua ocupada a
paused. Els processos estan congelats pel freezer de cgroups, però continuen existint amb tota la seva memòria reservada. Aexitedno hi ha procés i la memòria està alliberada; el que persisteix és la capa d'escriptura en disc.
Solució a l'exercici 2
docker run -d --name cache-prova redis:7-alpine
docker exec cache-prova redis-cli SET cataleg:versio 1
docker exec cache-prova sh -c 'echo 1 > /tmp/marca.txt'1. Després de docker restart:
docker restart cache-prova && sleep 2
docker exec cache-prova redis-cli GET cataleg:versio
docker exec cache-prova cat /tmp/marca.txt2. Després de docker stop + docker start:
docker stop cache-prova && docker start cache-prova && sleep 2
docker exec cache-prova redis-cli GET cataleg:versio
docker exec cache-prova cat /tmp/marca.txt3. Després de docker rm -f i recrear:
docker rm -f cache-prova
docker run -d --name cache-prova redis:7-alpine && sleep 2
docker exec cache-prova redis-cli GET cataleg:versio
docker exec cache-prova cat /tmp/marca.txt
docker rm -f cache-provaL'explicació en tres nivells:
| Nivell | Què sobreviu a restart/stop+start |
Què sobreviu a rm |
|---|---|---|
| Memòria del procés (clau de Redis) | No. El procés és nou, la seva memòria comença buida | No |
Capa d'escriptura (/tmp/marca.txt) |
Sí. És disc, i el contenidor és el mateix | No. Es destrueix amb el contenidor |
| Volum (lliçó 03-06) | Sí | Sí, i aquí hi ha la clau |
Els casos 1 i 2 són equivalents: restart és literalment stop + start. L'interessant és que cap nivell d'aquesta taula no sobreviu a un docker rm, i això significa que ara mateix, si esborres aurora-db, perds el catàleg d'Aurora Libros. Aquesta és exactament la demostració amb què obre la lliçó 03-06.
Solució a l'exercici 3
docker run -d --name senyal-a node:22-alpine node -e "setInterval(()=>{},1000)"
docker run -d --name senyal-b node:22-alpine sh -c 'echo inici && node -e "setInterval(()=>{},1000)"'
docker run -d --name senyal-c node:22-alpine node -e "process.on('SIGTERM',()=>{console.log('adeu');process.exit(0)});setInterval(()=>{},1000)"
for c in senyal-a senyal-b senyal-c; do
echo "--- $c ---"
{ time docker stop "$c" ; } 2>&1 | grep real
docker inspect --format 'codi {{.State.ExitCode}}' "$c"
done--- senyal-a ---
real 0m0.142s
codi 143
--- senyal-b ---
real 0m10.238s
codi 137
--- senyal-c ---
real 0m0.118s
codi 0| Cas | Temps | Codi | Per què |
|---|---|---|---|
| A | 0,14 s | 143 | node és PID 1 i rep SIGTERM. Sense gestor propi, s'aplica l'acció per defecte: acabar. Ràpid, però abrupte: no va tancar res |
| B | 10,24 s | 137 | El PID 1 és sh, que no reenvia el senyal. Deu segons d'espera i SIGKILL. El pitjor de tots els mons |
| C | 0,12 s | 0 | node captura SIGTERM, executa la seva lògica de tancament i surt per decisió pròpia amb codi 0 |
La correspondència amb Aurora Libros:
auroralibros/aurora-api:1.1.0és el cas A. Forma exec (ENTRYPOINT ["node"]), així que el senyal hi arriba, però sense gestor: s'aturava en 0,3 segons amb codi 143 i deixava el pool de PostgreSQL obert. Eren els "0,3 segons" que celebraves a la lliçó 02-04, correctes però incomplets.auroralibros/aurora-api:1.2.0és el cas C. Mateixa velocitat, però tancant el pool i el client de Redis i sortint amb codi 0.- El cas B no ha d'existir mai a les teves imatges. És el que obtindries amb
CMD node server.js && echo fio amb un entrypoint senseexec "$@".
Conclusió
Ja saps per què uns contenidors viuen i altres moren a l'instant, i no hi ha cap misteri: un contenidor dura exactament el que duri el seu procés PID 1. ubuntu:24.04 executa un bash sense entrada que acaba netament amb codi 0; nginx:alpine executa nginx -g "daemon off;", que no acaba mai. I aurora-api moria perquè el seu propi codi cridava process.exit(1). Coneixes els set estats i les seves transicions, i la diferència crucial entre exited —adormit, amb la seva capa d'escriptura, els seus registres i el seu nom intactes— i esborrat de debò amb docker rm.
Domines els verbs del cicle de vida: start, stop, restart, pause/unpause amb el seu freezer de cgroups que congela sense enviar ni un senyal i sense alliberar memòria, i kill com a emissor de senyals arbitraris, inclòs aquest SIGHUP que recarrega Nginx sense tallar ni una sola connexió. I entens el que passa de debò després d'un docker stop: SIGTERM, deu segons de gràcia ajustables amb --time, i SIGKILL. Ho has mesurat: 0,128 segons amb la forma exec enfront de 10,271 segons i codi 137 amb un shell pel mig que no reenvia senyals.
Els codis de sortida han deixat de ser soroll. Saps que per damunt de 128 hi ha un senyal amagat (137 = SIGKILL, 143 = SIGTERM, 139 = SIGSEGV), que 125, 126 i 127 apunten a Docker o a l'arrencada de la comanda i no a la teva aplicació, i que un contenidor en estat Created després d'un docker run significa que l'executable ni tan sols existia. I has portat la teoria al projecte: auroralibros/aurora-api:1.2.0 captura SIGTERM i SIGINT, tanca el pool de PostgreSQL i el client de Redis, es protegeix amb un temporitzador de 8 segons deliberadament inferior als 10 del període de gràcia, i surt amb codi 0 en 0,187 segons. A més, ja no se suïcida perquè Redis no hi sigui: es queda viu i ho registra, que és el que ha de fer un servei seriós.
Amb aurora-db i aurora-cache corrent i aurora-api per fi capaç de mantenir-se dret, comences a tenir alguna cosa semblant a una flota. I una flota s'ha de saber mirar i ordenar. A la lliçó següent, Gestionant Contenidors, esprémeràs docker ps fins al fons: cada columna explicada, la mida real de la capa d'escriptura amb -s, els filtres per estat, nom, imatge, etiqueta i salut, i les plantilles --format amb què muntaràs un petit tauler de control dels quatre serveis d'Aurora Libros. Aprendràs a compondre comandes amb -q per operar sobre desenes de contenidors alhora sense destrossar res, a copiar fitxers entre el host i un contenidor amb docker cp, a veure amb docker diff què ha canviat un contenidor respecte a la seva imatge, i per què docker commit mai no s'ha de fer servir per construir imatges 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
