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

  1. Els estats d'un contenidor
  2. El PID 1 i la regla fonamental
  3. Per què ubuntu mor i nginx no
  4. start, stop i restart
  5. pause i unpause: congelar sense matar
  6. kill i l'enviament de senyals concrets
  7. Aturada ordenada enfront d'aturada forçada
  8. La forma shell del CMD i els seus deu segons, mesurats
  9. Codis de sortida i la seva taula de diagnòstic
  10. Aturada ordenada d'aurora-api
  11. docker wait i docker rename

  1. 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 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 exited continua 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 amb docker 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-db
running
Estat: running | PID: 24817 | Arrencat: 2026-08-04T19:28:41.113927Z

Aquest 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.

  1. 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:

docker exec aurora-db ps -o pid,comm
PID   COMMAND
    1 postgres
   67 postgres
   68 postgres
  ...

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:

docker run -d --init --name amb-init nginx:alpine
docker exec amb-init ps -o pid,comm | head -3
PID   COMMAND
    1 /sbin/docker-init
    7 nginx

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.

docker rm -f amb-init

  1. Per què ubuntu mor i nginx no

Aquesta é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.04CMD ["/bin/bash"]. Un bash sense 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:alpine executa nginx -g "daemon off;". Aquest daemon 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}}"
prova-ubuntu: Up 3 seconds

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}}"
prova-ubuntu: Up 2 seconds

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 on a 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.

docker rm -f prova-ubuntu prova-nginx

  1. start, stop i restart

docker stop aurora-cache
docker start aurora-cache
docker restart aurora-cache
aurora-cache
aurora-cache
aurora-cache
Comanda Què fa Mateix contenidor? Mateix PID?
docker stop SIGTERM, espera, SIGKILL
docker start Torna a llançar la comanda original , mateix ID i mateixa capa d'escriptura No, PID nou
docker restart stop + start en un sol pas 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:preferit
OK
aurora-cache
nota temporal
(nil)

Dos resultats oposats a la mateixa comanda:

  • /tmp/nota.txt hi continua sent: està escrit a la capa d'escriptura del contenidor, que sobreviu a aturades i arrencades. Només desapareix amb docker 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:

docker stop aurora-db aurora-cache
docker start aurora-db aurora-cache

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:

docker start -a aurora-cache

-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.

  1. pause i unpause: congelar sense matar

docker pause aurora-cache
docker ps --filter name=aurora-cache --format "{{.Names}}: {{.Status}}"
aurora-cache
aurora-cache: Up 8 minutes (Paused)

docker 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:

docker exec aurora-cache redis-cli PING

Aquesta comanda es queda penjada indefinidament: el contenidor no pot respondre perquè no s'està executant. Interromp-la amb Ctrl+C i descongela'l:

docker unpause aurora-cache
docker exec aurora-cache redis-cli PING
aurora-cache
PONG
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.

  1. kill i l'enviament de senyals concrets

docker kill aurora-cache
docker ps -a --filter name=aurora-cache --format "{{.Names}}: {{.Status}}"
docker start aurora-cache
aurora-cache
aurora-cache: Exited (137) 3 seconds ago
aurora-cache

docker 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:

docker kill -s SIGHUP <contenidor>
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}}"
recarrega-demo: Up 20 seconds

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.

docker rm -f recarrega-demo

  1. 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:

  1. Docker envia el STOPSIGNAL de la imatge (per defecte SIGTERM) al PID 1. Recorda de la lliçó 02-04 que Nginx declara STOPSIGNAL SIGQUIT.
  2. Espera el període de gràcia: 10 segons per defecte.
  3. 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 kill

Quan 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? 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.

  1. La forma shell del CMD i els seus deu segons, mesurats

Aquí 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.js
docker build -f ~/aurora-libros/api/Dockerfile.shell -t aurora-api:forma-shell ~/aurora-libros/api

Ara 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 -3
PID   COMMAND
    1 nginx
   30 nginx

PID   COMMAND
    1 sh
    7 nginx
   14 nginx

Aquí 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:

time docker stop aturada-exec
time docker stop aturada-shell
aturada-exec
real    0m0.128s

aturada-shell
real    0m10.271s

Vuitanta vegades més lent. I els codis de sortida expliquen la resta de la història:

docker inspect --format '{{.Name}} → codi {{.State.ExitCode}}' aturada-exec aturada-shell
/aturada-exec → codi 0
/aturada-shell → codi 137

Reconstruïm el que ha passat al segon cas:

  1. docker stop envia SIGQUIT (el STOPSIGNAL de Nginx) al PID 1, que és sh.
  2. sh no té cap gestor per a aquest senyal ni sap res de reenviar-lo als seus fills. L'ignora o acaba ell tot sol, però nginx no se n'assabenta de res.
  3. Docker espera els seus deu segons complets.
  4. 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 un docker-entrypoint.sh que acabi amb exec "$@", com vas fer a la lliçó 02-04.

Neteja la demostració:

docker rm aturada-exec aturada-shell
docker image rm aurora-api:forma-shell

  1. 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-api
NAMES          STATUS
aurora-cache   Up 4 minutes
aurora-db      Up 12 minutes
1

La 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 /etc
docker: 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 denied
docker 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
125

Observa 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.

docker rm codi-0 codi-1 codi-126 codi-127

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.

  1. Aturada ordenada d'aurora-api

Ha 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/api

I 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}}"
api-aturada: Up 3 seconds (health: starting)

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-aturada
api-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: 0

Les 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 node perquè é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.isOpen era 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 rm api-aturada

  1. docker wait i docker rename

Dues 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-lenta
7d3f9a1b2c48
3

Es 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 migracio
Migració correcta, arrencant l'API

docker 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
aurora-cache-antic: Up 25 minutes
aurora-db: Up 33 minutes

É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 Exited està esborrat. Continua existint, ocupa el seu nom i conserva capa d'escriptura i registres. S'esborra amb docker rm, no amb docker stop.
  • Posar un servei en mode dimoni dins del contenidor. nginx sense daemon off, httpd -k start, postgres amb pg_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 kill per 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 stop primer; kill nomé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.OOMKilled a docker 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 --time a les bases de dades. docker stop --time 30 aurora-db dona marge a PostgreSQL per fer el seu checkpoint i tancar-se netament.
  • Consell: fes servir docker start -a quan un contenidor arrenqui i mori de seguida: veus l'arrencada en directe sense haver-la de perseguir amb docker 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:

  1. Fes docker restart i comprova què sobreviu de les dues coses.
  2. Fes docker stop seguit de docker start i comprova el mateix.
  3. Fes docker rm -f i 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-complet
created / codi 0
docker 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-complet
running / codi 0
paused / codi 0
running / codi 0
time 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-complet
cicle-complet
real    0m0.156s
exited / codi 143

cicle-complet
cicle-complet
exited / codi 137

Les tres respostes:

  • Després de docker stop: codi 143, és a dir, 128 + 15 = SIGTERM. sleep no instal·la cap gestor de senyals, així que se li aplica l'acció per defecte de SIGTERM: acabar immediatament. Per això el real é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. A exited no 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.txt
(nil)
1

2. 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.txt
(nil)
1

3. 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-prova
(nil)
cat: can't open '/tmp/marca.txt': No such file or directory

L'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) , 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 fi o amb un entrypoint sense exec "$@".
docker rm senyal-a senyal-b senyal-c

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

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