Aurora Libros funciona: quatre contenidors en una xarxa pròpia, el catàleg fora de perill en un volum amb nom, la web servint-se a través d'un proxy invers. Però queden dos riscos que ja van treure el cap en lliçons anteriors i que en un servidor real acaben en trucada de matinada.

El primer el vas veure a docker stats: la columna LIMIT mostrava 7,628 GiB a tots els contenidors, és a dir, tota la RAM de la màquina. Una consulta desbocada a PostgreSQL o una fuita de memòria a Node poden deixar sense memòria el host sencer i arrossegar-hi els altres tres serveis. El segon és més simple: si un contenidor mor a les tres de la matinada, allà es queda fins que algú l'aixequi.

Aquesta lliçó tanca tots dos fronts. Posaràs límits de memòria, CPU, disc i nombre de processos, provocaràs un OOM a propòsit per veure'l amb els teus ulls, i configuraràs polítiques de reinici amb les seves diferències subtils. I en acabar, tindràs al davant la llista completa de comandes que cal per aixecar la plataforma des de zero... i entendràs perfectament per què existeix el mòdul 4.

Contingut

  1. Per què un contenidor sense límits és un problema
  2. Límits de memòria
  3. L'OOM killer, provocat i diagnosticat
  4. Límits de CPU
  5. Límits d'E/S de disc
  6. --pids-limit: defensa davant d'una fork bomb
  7. Verificar i modificar en calent
  8. Polítiques de reinici
  9. always enfront d'unless-stopped
  10. Per què una política de reinici no substitueix un healthcheck
  11. Pràctica final: el quadre de límits d'Aurora Libros
  12. L'script complet, i per què no pot continuar així

  1. Per què un contenidor sense límits és un problema

docker stats --no-stream --format "table {{.Name}}\t{{.MemUsage}}\t{{.CPUPerc}}\t{{.PIDs}}"
NAME           MEM USAGE / LIMIT     CPU %   PIDS
aurora-web     3.44MiB / 7.628GiB    0.00%   3
aurora-api     52.17MiB / 7.628GiB   0.13%   11
aurora-cache   9.91MiB / 7.628GiB    0.18%   6
aurora-db      41.28MiB / 7.628GiB   0.02%   7

Tots quatre tenen el mateix límit: tota la memòria del host. Per defecte, un contenidor pot consumir tanta RAM i tanta CPU com n'hi hagi de disponible. Recorda de la lliçó 01-01 que un contenidor no és una màquina virtual amb recursos preassignats, sinó un procés del host aïllat amb namespaces; el que reparteix els recursos són els cgroups, i per defecte estan oberts de bat a bat.

Les conseqüències, totes reals:

Escenari Què passa sense límits
Fuita de memòria a l'API Node creix fins a esgotar la RAM. El nucli comença a matar processos, i no necessàriament el culpable
Consulta pesada a PostgreSQL Un ORDER BY sobre una taula enorme es menja diversos GB i expulsa de memòria la resta de serveis
Pic de trànsit Un contenidor acapara tota la CPU i els altres deixen de respondre
Contenidor compromès Un minador de criptomonedes fa servir el 100 % de tots els nuclis
Bug amb fork() Milers de processos esgoten la taula de processos del host, no només la del contenidor

La idea que sosté tot l'apartat: els límits no són una optimització, són un mecanisme de contenció. Serveixen perquè la fallada d'un servei no es converteixi en la fallada de la màquina sencera.

  1. Límits de memòria

Opció Què fa
-m, --memory Límit dur. En superar-lo, el nucli mata el procés
--memory-swap Límit de memòria + swap combinats
--memory-reservation Límit tou: sota pressió, el nucli intenta reduir-lo fins a aquest valor
--memory-swappiness De 0 a 100: quanta tendència a fer servir swap
--oom-kill-disable No matar el contenidor en esgotar la memòria

Les unitats són b, k, m i g: -m 512m, -m 1g. El mínim acceptat és 6 MB.

La relació entre --memory i --memory-swap

Aquesta és la part que confon tothom, perquè --memory-swap no és la quantitat de swap: és el total de memòria més swap.

--memory --memory-swap RAM disponible Swap disponible Total
512m (sense especificar) 512 MB 512 MB (igual que la RAM) 1 GB
512m 1g 512 MB 512 MB 1 GB
512m 512m 512 MB 0 (swap desactivat) 512 MB
512m -1 512 MB Il·limitat Il·limitat
(sense especificar) 1g Il·limitada Error: --memory-swap requereix --memory

La fila més important és la tercera: --memory i --memory-swap amb el mateix valor desactiva el swap. I aquesta sol ser la configuració correcta en servidors, perquè un procés que comença a intercanviar a disc no està funcionant, està agonitzant: els temps de resposta es disparen fins a fer el servei inútil, però com que tècnicament continua viu, els healthchecks poden no assabentar-se'n. És preferible una fallada neta i ràpida a una degradació lenta i silenciosa.

docker run -d --name limit-demo -m 256m --memory-swap 256m alpine:3.20 sleep 300
docker inspect -f 'Memory: {{.HostConfig.Memory}} bytes | MemorySwap: {{.HostConfig.MemorySwap}} bytes' limit-demo
docker stats --no-stream limit-demo --format "{{.Name}}: {{.MemUsage}}"
docker rm -f limit-demo
Memory: 268435456 bytes | MemorySwap: 268435456 bytes
limit-demo: 1.02MiB / 256MiB

Ara la columna LIMIT diu 256 MiB en comptes dels 7,628 GiB del host.

--memory-reservation: el límit tou

docker run -d --name tou-demo -m 512m --memory-reservation 256m redis:7-alpine
docker inspect -f '{{.HostConfig.MemoryReservation}}' tou-demo
docker rm -f tou-demo
268435456

La diferència entre tots dos:

--memory (dur) --memory-reservation (tou)
Es pot superar? No, mai Sí, si hi ha memòria lliure al host
Què passa en superar-lo El contenidor mor (OOM) El nucli pressiona perquè baixi
Per a què serveix Contenció absoluta Prioritzar qui cedeix memòria quan escasseja

El patró habitual és fer servir tots dos: reserva el consum normal esperat i limita al punt on alguna cosa va clarament malament.

--oom-kill-disable

docker run -d --name sense-oom -m 128m --oom-kill-disable alpine:3.20 sleep 300

Amb aquesta opció, en esgotar la memòria el nucli no mata el procés: el congela indefinidament. Sona millor i gairebé sempre és pitjor: acabes amb un contenidor Up que no respon a res, sense registres nous i sense codi de sortida per investigar. Un procés mort es reinicia; un de congelat s'ha de diagnosticar a mà. Fes-lo servir només si saps exactament per què, i mai sense un límit de memòria (sense -m, congelaries la màquina sencera).

docker rm -f sense-oom

  1. L'OOM killer, provocat i diagnosticat

Quan un contenidor supera el seu límit de memòria, actua l'OOM killer (Out Of Memory killer) del nucli de Linux. No és Docker: és el nucli, i no negocia.

El provocarem. Aquest contenidor té 64 MB i intentarà escriure 200 MB en memòria compartida, que es comptabilitza contra el mateix cgroup:

docker run -d --name oom-demo \
  -m 64m --memory-swap 64m --shm-size 512m \
  alpine:3.20 sh -c 'echo "començo a omplir memòria"; dd if=/dev/zero of=/dev/shm/farciment bs=1M count=200; echo "he acabat"'

sleep 5
docker ps -a --filter name=oom-demo --format "table {{.Names}}\t{{.Status}}"
NAMES       STATUS
oom-demo    Exited (137) 2 seconds ago

Codi 137. De la taula de la lliçó 03-02: 137 = 128 + 9 = SIGKILL. Però aquest mateix 137 el produeix també un docker kill o un docker stop que esgota el seu termini, així que cal una prova més:

docker inspect -f 'OOMKilled: {{.State.OOMKilled}} | ExitCode: {{.State.ExitCode}} | Error: {{.State.Error}}' oom-demo
docker logs oom-demo
OOMKilled: true | ExitCode: 137 | Error:
començo a omplir memòria

OOMKilled: true és la confirmació inequívoca. I fixa't en els registres: es veu el "començo a omplir memòria" però mai el "he acabat". El procés va ser eliminat a mig fer, sense oportunitat d'escriure res més, sense excepció capturable i sense aturada ordenada. SIGKILL no es pot interceptar.

L'esdeveniment també queda registrat al dimoni:

docker events --since 5m --filter container=oom-demo --filter event=oom
2026-08-04T23:14:07.442 container oom 8b2f1c9e... (image=alpine:3.20, name=oom-demo)

I al host, el nucli hi deixa la seva pròpia constància:

sudo dmesg | grep -i "killed process" | tail -2
[189234.117] Memory cgroup out of memory: Killed process 41207 (dd) total-vm:213504kB, anon-rss:65112kB

Com diagnosticar un 137 sospitós, en ordre:

Comprovació Si és afirmativa
docker inspect -f '{{.State.OOMKilled}}'true Va ser l'OOM killer: puja el límit o arregla la fuita
docker events --filter event=oom mostra l'esdeveniment Confirmació des del dimoni, amb hora exacta
Algú va executar docker kill o docker stop Va ser una acció humana o un script
El procés va ignorar SIGTERM durant 10 s És el cas de la forma shell de la lliçó 03-02
docker rm oom-demo

  1. Límits de CPU

Tres opcions que fan coses diferents i es confonen constantment:

Opció Què és Tipus Quan fer-la servir
--cpus Quants nuclis pot fer servir com a màxim (admet decimals) Límit dur L'opció per defecte: predictible i fàcil de raonar
--cpu-shares Pes relatiu enfront d'altres contenidors (1024 per defecte) Prioritat Repartir CPU en contenció sense malgastar-la quan en sobra
--cpuset-cpus Quins nuclis concrets pot fer servir Fixació Aïllar càrregues sensibles a la latència o respectar NUMA
docker run -d --name cpu-limitat --cpus 0.5 alpine:3.20 sh -c 'while true; do :; done'
sleep 3
docker stats --no-stream cpu-limitat --format "{{.Name}}: {{.CPUPerc}}"
docker rm -f cpu-limitat
cpu-limitat: 49.87%

Un bucle infinit que devoraria un nucli sencer es queda al 50 % d'un, que és just el que demanava --cpus 0.5. Tingues present que a docker stats el 100 % equival a un nucli: un contenidor amb --cpus 2 pot arribar a marcar 200 %.

--cpu-shares no és un límit

docker run -d --name pes-alt  --cpu-shares 1024 alpine:3.20 sh -c 'while true; do :; done'
docker run -d --name pes-baix --cpu-shares 256  alpine:3.20 sh -c 'while true; do :; done'
sleep 5
docker stats --no-stream pes-alt pes-baix --format "{{.Name}}: {{.CPUPerc}}"
docker rm -f pes-alt pes-baix
pes-alt: 318.42%
pes-baix: 79.61%

La proporció és aproximadament 4:1, exactament la relació entre 1024 i 256. Però la clau és en una altra banda: si pes-baix estigués sol a la màquina, faria servir tota la CPU disponible. Els shares només entren en joc quan hi ha competència; no malgasten capacitat ociosa.

--cpus 1 --cpu-shares 1024
Amb la màquina lliure Fa servir com a màxim 1 nucli Fa servir tots els que hi hagi
Amb la màquina saturada Fa servir 1 nucli Fa servir la seva part proporcional
Predictible No, depèn dels veïns
Ús típic Producció, facturació per recursos Prioritats relatives dins d'un host

--cpuset-cpus

docker run -d --name fixat --cpuset-cpus "0,1" alpine:3.20 sh -c 'while true; do :; done'
docker inspect -f '{{.HostConfig.CpusetCpus}}' fixat
docker rm -f fixat
0,1

El contenidor només s'executarà als nuclis 0 i 1, sigui quina sigui la càrrega de la resta. Es fa servir poc, però és valuós per a càrregues sensibles a la latència (evita que el planificador mogui el procés entre nuclis i es perdin les memòries cau) i per reservar nuclis a processos crítics del sistema.

  1. Límits d'E/S de disc

Menys habituals, però imprescindibles quan un contenidor satura el disc i arrossega els altres:

Opció Què limita
--blkio-weight Pes relatiu d'E/S, de 10 a 1000 (500 per defecte). Com --cpu-shares, però per a disc
--device-read-bps Bytes per segon de lectura sobre un dispositiu
--device-write-bps Bytes per segon d'escriptura
--device-read-iops / --device-write-iops Operacions per segon
docker run --rm --device-write-bps /dev/sda:1mb alpine:3.20 \
  dd if=/dev/zero of=/tmp/prova bs=1M count=10 oflag=direct
10+0 records in
10+0 records out
10485760 bytes (10.0MB) copied, 10.031459 seconds, 1.0MB/s

Exactament 1 MB/s, deu segons per a deu megues. Dos advertiments: cal indicar el dispositiu real del host (/dev/sda, /dev/nvme0n1; mira'l amb lsblk), i aquestes opcions només funcionen amb el driver d'emmagatzematge i el sistema de fitxers adequats —en moltes configuracions amb overlay2 sobre ext4 requereixen oflag=direct per notar-se, com a l'exemple—.

  1. --pids-limit: defensa davant d'una fork bomb

Limita el nombre de processos i fils que pot crear un contenidor. És la protecció més barata i de les més efectives:

docker run --rm --pids-limit 20 alpine:3.20 sh -c \
  'for i in $(seq 1 50); do sleep 30 & done; echo "llançats els que ha estat possible"'
sh: can't fork: Resource temporarily unavailable
sh: can't fork: Resource temporarily unavailable
...
llançats els que ha estat possible

A partir del procés número 20, el nucli denega el fork(). Sense aquest límit, una fork bomb —un bug o un atac que crea processos sense parar— esgota la taula de processos del host, i a partir d'aquell moment no es pot llançar ni un ps per diagnosticar: cal reiniciar la màquina.

docker run -d --name pids-demo --pids-limit 100 nginx:alpine
docker inspect -f '{{.HostConfig.PidsLimit}}' pids-demo
docker stats --no-stream pids-demo --format "{{.Name}}: {{.PIDs}} processos"
docker rm -f pids-demo
100
pids-demo: 3 processos

Valors raonables: 100 per a un servei web senzill, 200–500 per a una base de dades, i sempre mesurant abans amb docker stats quin és el consum normal, amb un marge folgat.

  1. Verificar i modificar en calent

docker inspect -f 'Memòria: {{.HostConfig.Memory}} | Swap: {{.HostConfig.MemorySwap}} | NanoCPUs: {{.HostConfig.NanoCpus}} | PIDs: {{.HostConfig.PidsLimit}} | Política: {{.HostConfig.RestartPolicy.Name}}' aurora-api
Memòria: 0 | Swap: 0 | NanoCPUs: 0 | PIDs: 0 | Política: no

Un 0 significa "sense límit", i NanoCPUs expressa --cpus en milmilionèsimes: --cpus 1.5 es desa com a 1500000000.

El gran avantatge d'aquestes opcions, enfront de ports, xarxes i muntatges, és que sí que es poden canviar sense recrear el contenidor, perquè viuen als cgroups i no als namespaces:

docker update --memory 512m --memory-swap 512m --cpus 1.0 --pids-limit 200 aurora-db
docker inspect -f 'Memòria: {{.HostConfig.Memory}} | NanoCPUs: {{.HostConfig.NanoCpus}}' aurora-db
docker stats --no-stream aurora-db --format "{{.Name}}: {{.MemUsage}}"
aurora-db
Memòria: 536870912 | NanoCPUs: 1000000000
aurora-db: 41.83MiB / 512MiB

Sense aturar PostgreSQL, sense recrear el contenidor i sense perdre cap connexió. La columna LIMIT ja diu 512 MiB. És l'excepció de la regla "tot es fixa en crear", i per això existeix docker update.

Dos límits de docker update: no pot baixar la memòria per sota del que ja està en ús (fallaria), i algunes opcions com --memory-swap requereixen indicar també --memory a la mateixa crida.

  1. Polítiques de reinici

Un contenidor que mor no torna sol. L'opció --restart canvia això:

Política Reinicia si el procés surt amb error Reinicia si surt amb 0 Reinicia en arrencar el dimoni Després d'un docker stop manual
no (per defecte) No No No
on-failure , indefinidament No Sí, si havia fallat No
on-failure:N Sí, fins a N vegades No No
always Sí, sempre Sí, en reiniciar el dimoni
unless-stopped Sí, llevat que l'hagis aturat tu No
docker run -d --name reinici-demo --restart on-failure:3 \
  alpine:3.20 sh -c 'echo "intent"; sleep 2; exit 1'
sleep 15
docker ps -a --filter name=reinici-demo --format "table {{.Names}}\t{{.Status}}"
docker inspect -f 'Reinicis: {{.RestartCount}} | Estat: {{.State.Status}} | Codi: {{.State.ExitCode}}' reinici-demo
docker logs reinici-demo
NAMES           STATUS
reinici-demo    Exited (1) 3 seconds ago
Reinicis: 3 | Estat: exited | Codi: 1
intent
intent
intent
intent

Quatre "intent" als registres: l'arrencada original més tres reinicis, i després Docker es rendeix. RestartCount és el comptador que ho confirma i és dels primers camps a mirar quan un servei "va i ve".

El backoff exponencial

Docker no reinicia en bucle tancat: espera cada vegada més entre intents.

Intent Espera aproximada
1r 100 ms
2n 200 ms
3r 400 ms
4t 800 ms
Doblant fins a un màxim d'1 minut

El comptador es reinicia si el contenidor aconsegueix mantenir-se en marxa almenys 10 segons. Aquest detall explica un comportament desconcertant: un servei que arrenca, funciona 30 segons i mor, amb --restart always pot quedar-se reiniciant-se eternament cada pocs segons sense que el backoff arribi mai a créixer. Es detecta així:

docker ps --filter status=restarting
docker inspect -f '{{.RestartCount}}' <contenidor>

Un RestartCount de 847 és un servei que fa hores que falla i ningú no se n'ha assabentat.

  1. always enfront d'unless-stopped

Les dues semblen iguals i es diferencien en un únic escenari, que a més és el més important: quan es reinicia el dimoni de Docker o la màquina.

flowchart TD
    A["docker stop aurora-api<br/>(aturada manual deliberada)"] --> B["Contenidor exited"]
    B --> C["Es reinicia el host<br/>o el servei docker"]
    C --> D{"Quina política<br/>tenia?"}
    D -- "always" --> E["Docker l'ARRENCA<br/>La teva aturada manual s'ignora"]
    D -- "unless-stopped" --> F["Docker el DEIXA aturat<br/>Es respecta la teva decisió"]
Situació always unless-stopped
El procés falla Reinicia Reinicia
El procés surt amb codi 0 Reinicia Reinicia
docker stop manual No reinicia (en aquell moment) No reinicia
Reinici del dimoni després d'un docker stop manual L'arrenca El deixa aturat
Reinici del dimoni estant en marxa L'arrenca L'arrenca

Per què importa: imagina't que atures aurora-api per investigar un problema i te'n vas a dinar. Amb always, un reinici del dimoni —una actualització automàtica, un reinici del servidor— la torna a aixecar en l'estat trencat que estaves investigant. Amb unless-stopped, la teva decisió es respecta.

Recomanació general: unless-stopped per a serveis de llarga durada i on-failure:N per a processos que han d'acabar. always només si de debò vols que res no pugui deixar el contenidor aturat.

I dos matisos operatius:

  • La política es pot canviar en calent: docker update --restart unless-stopped aurora-db.
  • Amb --rm, --restart és incompatible: Docker el rebutja, perquè no pot alhora esborrar el contenidor i reiniciar-lo.
docker rm -f reinici-demo

  1. Per què una política de reinici no substitueix un healthcheck

Aquest és el matís que separa una configuració que sembla robusta d'una que ho és.

docker run -d --name penjat --restart unless-stopped nginx:alpine
docker exec penjat nginx -s stop
sleep 5
docker ps --filter name=penjat --format "{{.Names}}: {{.Status}}"
docker rm -f penjat
penjat: Up 12 seconds

Nginx està aturat dins del contenidor i Docker diu Up, tan tranquil, perquè el PID 1 continua viu (el master de Nginx encara no ha acabat del tot). Generalitzant el problema:

Tipus de fallada La detecta --restart? La detecta HEALTHCHECK?
El procés mor
El procés surt amb error
El procés viu però està bloquejat No
Pool de connexions esgotat No
Retorna 500 a totes les peticions No
Ha perdut la base de dades No

I ara el detall que sorprèn: Docker Engine no reinicia un contenidor per estar unhealthy. La política de reinici reacciona a la mort del procés, no al veredicte del healthcheck. Pots tenir un contenidor Up 3 days (unhealthy) amb --restart always durant tres dies sense que ningú faci res.

Qui actua llavors sobre la salut?

Entorn Què fa amb unhealthy
Docker Engine sol Marca l'estat i emet un esdeveniment health_status. Res més
Docker Swarm Reemplaça la tasca per una de nova (lliçó 06-03)
Kubernetes La liveness probe reinicia el contenidor; la readiness el treu de balanceig (lliçó 06-05)
Tu, amb un script El que programis, vigilant docker events --filter event=health_status

La conclusió, que convé tenir clara en sortir d'aquest mòdul: la política de reinici i el healthcheck resolen problemes diferents i es complementen. La primera et retorna un procés mort; el segon detecta un procés viu però inútil. I perquè aquell diagnòstic es tradueixi en acció cal un orquestrador, que és el destí del mòdul 6.

  1. Pràctica final: el quadre de límits d'Aurora Libros

Toca decidir números concrets, i decidir-los amb criteri. Parteix del que consumeixen de debò:

docker stats --no-stream --format "table {{.Name}}\t{{.MemUsage}}\t{{.CPUPerc}}\t{{.PIDs}}"
NAME           MEM USAGE / LIMIT     CPU %   PIDS
aurora-web     3.44MiB / 7.628GiB    0.00%   3
aurora-api     52.17MiB / 7.628GiB   0.13%   11
aurora-cache   9.91MiB / 7.628GiB    0.18%   6
aurora-db      41.83MiB / 512MiB     0.02%   7

El quadre, amb la seva justificació:

Servei Memòria Reserva CPU PIDs Política Per què
aurora-db 512m 256m 1.0 200 unless-stopped Consumeix 42 MB en repòs, però PostgreSQL necessita marge per a work_mem, memòria cau i connexions. És el servei amb estat: ha de tornar sempre, i si l'atures tu, ha de quedar-se aturat
aurora-cache 256m 0.5 100 unless-stopped Amb --maxmemory 200mb de Redis alineat per sota del límit del contenidor. No necessita CPU: gairebé tot és E/S de xarxa
aurora-api 256m 128m 1.0 100 on-failure:3 Node amb 52 MB de base; 256 MB donen aire al recol·lector d'escombraries. on-failure:3 perquè si falla tres vegades seguides, el problema és de configuració i reiniciar en bucle només ho amaga
aurora-web 128m 0.5 50 unless-stopped Nginx servint estàtics i fent de proxy: 3,4 MB reals. 128 MB és generosíssim

Dues decisions mereixen explicació a part:

Per què aurora-cache porta --maxmemory a més del límit del contenidor. Si només posessis -m 256m, Redis no sabria res d'aquell límit: continuaria acceptant claus fins que l'OOM killer el matés de cop, perdent tota la memòria cau. Amb --maxmemory 200mb --maxmemory-policy allkeys-lru, Redis gestiona el problema ell mateix: en arribar a 200 MB comença a expulsar les claus menys usades i continua funcionant. El límit del contenidor passa a ser la xarxa de seguretat, no el mecanisme habitual. Quan un servei té el seu propi control de memòria, configura'l per sota del límit del contenidor. El mateix aplica a --max-old-space-size a Node i a shared_buffers a PostgreSQL.

Per què aurora-api porta on-failure:3 i no unless-stopped. És un servei sense estat i reemplaçable, però si falla tres vegades seguides gairebé mai no és una cosa transitòria: és una variable d'entorn mal posada, una migració pendent o una fallada de dependència. Reiniciar infinitament converteix un error visible en un bucle silenciós que omple els registres i amaga el diagnòstic.

Aplica el quadre. aurora-db ja està actualitzat, així que n'hi ha prou amb la política:

docker update --restart unless-stopped --memory-reservation 256m aurora-db

Els altres tres cal recrear-los, perquè aurora-cache canvia també la seva comanda i aurora-api i aurora-web no tenien límits:

docker rm -f aurora-cache aurora-api aurora-web

docker run -d --name aurora-cache --network aurora-net \
  --label projecte=aurora-libros --label component=cache \
  --memory 256m --memory-swap 256m --cpus 0.5 --pids-limit 100 \
  --restart unless-stopped \
  redis:7-alpine redis-server --maxmemory 200mb --maxmemory-policy allkeys-lru

docker run -d --name aurora-api --network aurora-net \
  --label projecte=aurora-libros --label component=api \
  --env-file ~/aurora-libros/aurora.env \
  -p 3000:3000 \
  --memory 256m --memory-swap 256m --memory-reservation 128m \
  --cpus 1.0 --pids-limit 100 \
  --restart on-failure:3 \
  auroralibros/aurora-api:1.2.0

docker run -d --name aurora-web --network aurora-net \
  --label projecte=aurora-libros --label component=web \
  -p 8080:80 \
  --mount type=bind,src=$HOME/aurora-libros/web/index.html,dst=/usr/share/nginx/html/index.html,readonly \
  --mount type=bind,src=$HOME/aurora-libros/web/nginx.conf,dst=/etc/nginx/conf.d/default.conf,readonly \
  --memory 128m --memory-swap 128m --cpus 0.5 --pids-limit 50 \
  --restart unless-stopped --stop-signal SIGQUIT \
  nginx:alpine

Verifica:

sleep 8
docker stats --no-stream --format "table {{.Name}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.PIDs}}"
NAME           MEM USAGE / LIMIT    MEM %   PIDS
aurora-web     3.51MiB / 128MiB     2.74%   3
aurora-api     53.02MiB / 256MiB    20.71%  11
aurora-cache   10.14MiB / 256MiB    3.96%   6
aurora-db      42.11MiB / 512MiB    8.22%   7

Quatre límits reals en comptes de quatre vegades 7,628 GiB. I els percentatges són sans: entre el 3 % i el 21 %, amb marge de sobres per a pics.

docker inspect -f '{{.Name}}: mem={{.HostConfig.Memory}} cpus={{.HostConfig.NanoCpus}} pol={{.HostConfig.RestartPolicy.Name}}{{if .HostConfig.RestartPolicy.MaximumRetryCount}}:{{.HostConfig.RestartPolicy.MaximumRetryCount}}{{end}}' \
  aurora-db aurora-cache aurora-api aurora-web
curl -s http://localhost:8080/api/llibres | jq -r '.llibres | length'
/aurora-db: mem=536870912 cpus=1000000000 pol=unless-stopped
/aurora-cache: mem=268435456 cpus=500000000 pol=unless-stopped
/aurora-api: mem=268435456 cpus=1000000000 pol=on-failure:3
/aurora-web: mem=134217728 cpus=500000000 pol=unless-stopped
9

Tots quatre amb els seus límits, les seves polítiques, i els nou llibres hi continuaven sent després de destruir i recrear tres contenidors. El volum aurora-dades va fer la seva feina.

Prova final de resiliència: mata l'API a la brava i mira què passa.

docker kill aurora-api
sleep 3
docker ps --filter name=aurora-api --format "{{.Names}}: {{.Status}}"
docker inspect -f 'Reinicis: {{.RestartCount}}' aurora-api
curl -s http://localhost:8080/api/llibres | jq -r '.llibres | length'
aurora-api: Up 2 seconds
Reinicis: 1
9

S'ha aixecat sola en menys de tres segons i la web va tornar a funcionar sense que ningú toqués res. Aquesta és exactament la diferència entre un munt de contenidors i una plataforma.

  1. L'script complet, i per què no pot continuar així

Recapitula. Això és tot el que cal escriure, en l'ordre correcte, per aixecar Aurora Libros en una màquina neta:

#!/usr/bin/env bash
# aixecar-aurora.sh — Aurora Libros S.L., plataforma completa
set -euo pipefail

# ---------- 1. Xarxa ----------
docker network create --label projecte=aurora-libros aurora-net

# ---------- 2. Volum ----------
docker volume create --label projecte=aurora-libros aurora-dades

# ---------- 3. Base de dades ----------
docker run -d --name aurora-db --network aurora-net \
  --label projecte=aurora-libros --label component=base-de-dades \
  -e POSTGRES_USER=aurora \
  -e POSTGRES_PASSWORD=aurora_secreta \
  -e POSTGRES_DB=aurora_llibres \
  -p 127.0.0.1:5432:5432 \
  --mount type=volume,src=aurora-dades,dst=/var/lib/postgresql/data \
  --mount type=bind,src=$HOME/aurora-libros/db/init.sql,dst=/docker-entrypoint-initdb.d/init.sql,readonly \
  --memory 512m --memory-swap 512m --memory-reservation 256m \
  --cpus 1.0 --pids-limit 200 \
  --restart unless-stopped --stop-timeout 30 \
  postgres:16-alpine

# ---------- 4. Esperar que la base de dades accepti connexions ----------
until docker exec aurora-db pg_isready -U aurora -q; do sleep 1; done

# ---------- 5. Memòria cau ----------
docker run -d --name aurora-cache --network aurora-net \
  --label projecte=aurora-libros --label component=cache \
  --memory 256m --memory-swap 256m --cpus 0.5 --pids-limit 100 \
  --restart unless-stopped \
  redis:7-alpine redis-server --maxmemory 200mb --maxmemory-policy allkeys-lru

# ---------- 6. API ----------
docker run -d --name aurora-api --network aurora-net \
  --label projecte=aurora-libros --label component=api \
  --env-file "$HOME/aurora-libros/aurora.env" \
  -p 3000:3000 \
  --memory 256m --memory-swap 256m --memory-reservation 128m \
  --cpus 1.0 --pids-limit 100 \
  --restart on-failure:3 \
  auroralibros/aurora-api:1.2.0

# ---------- 7. Web i proxy invers ----------
docker run -d --name aurora-web --network aurora-net \
  --label projecte=aurora-libros --label component=web \
  -p 8080:80 \
  --mount type=bind,src=$HOME/aurora-libros/web/index.html,dst=/usr/share/nginx/html/index.html,readonly \
  --mount type=bind,src=$HOME/aurora-libros/web/nginx.conf,dst=/etc/nginx/conf.d/default.conf,readonly \
  --memory 128m --memory-swap 128m --cpus 0.5 --pids-limit 50 \
  --restart unless-stopped --stop-signal SIGQUIT \
  nginx:alpine

echo "Aurora Libros aixecada: http://localhost:8080"

Mira aquest script amb ulls crítics. Funciona, és correcte i conté tot el que has après en set lliçons. I és un problema:

Defecte Per què fa mal
Cinquanta línies per a quatre serveis I això és una aplicació petita. Amb deu serveis en són cent trenta
L'ordre és responsabilitat teva Aquell until pg_isready l'has escrit a mà. Si te'l saltes, l'API arrenca abans que la base de dades
No hi ha manera d'"aturar-ho tot" Necessites un altre script, amb els noms repetits i l'ordre invers
Repetició constant --network aurora-net i les etiquetes apareixen quatre vegades. Canviar el nom de la xarxa significa quatre edicions
Fràgil davant dels canvis Afegir una variable a l'API obliga a esborrar el contenidor i tornar a enganxar les seves catorze línies exactes, sense equivocar-se en el volum
No és declaratiu Descriu com arribar a l'estat desitjat, no quin és. Si algú va canviar alguna cosa a mà, l'script no ho detecta
Difícil de compartir i revisar Com revises això en una pull request? Com saps què va canviar respecte a la setmana passada?
Res no garanteix que s'hagi executat sencer Si falla el pas 5, et quedes amb mitja plataforma en marxa

I ara imagina't l'escena real: entra algú nou a l'equip d'Aurora Libros. Li passes aquest fitxer per xat, i esperes que el seu $HOME tingui la mateixa estructura, que la imatge 1.2.0 sigui la bona, que ningú no hagi canviat un valor sense dir-ho. És la versió moderna d'aquells quinze passos manuals d'onboarding de la lliçó 01-07: has reduït el problema moltíssim, però no l'has eliminat. L'has convertit en un script de bash que ningú no vol mantenir.

Existeix una resposta a exactament això, i és el mòdul que ve.

Errors Habituals i Consells

  • No posar cap límit "perquè la màquina té RAM de sobres". La té fins que un contenidor se la menja sencera i el nucli comença a matar processos a l'atzar, inclosos els del sistema.
  • Confondre --memory-swap amb "quantitat de swap". És el total de memòria més swap. -m 512m --memory-swap 512m és el que desactiva el swap.
  • Interpretar tot 137 com un docker kill. Comprova .State.OOMKilled abans de treure conclusions.
  • Fer servir --cpu-shares esperant un límit dur. Només actua en contenció. Per a un sostre real, --cpus.
  • Posar un límit de memòria sense ajustar el del servei. Redis sense --maxmemory, Node sense --max-old-space-size o la JVM sense -Xmx xocaran contra el límit del contenidor i moriran de cop en comptes de gestionar-ho.
  • Confiar en --restart always com si fos alta disponibilitat. No detecta un procés viu però penjat, i no arregla una fallada de configuració: només la repeteix més ràpid.
  • Esperar que Docker reiniciï un contenidor unhealthy. Docker Engine només el marca. Actuar és cosa d'un orquestrador.
  • Fer servir always en comptes d'unless-stopped. Amb always, un reinici del dimoni t'aixeca contenidors que havies aturat a propòsit.
  • Consell: mesura abans de limitar. Deixa el servei corrent sota càrrega real, mira docker stats, i posa el límit amb un marge del doble o el triple del pic observat.
  • Consell: docker update és el teu amic per ajustar límits i polítiques en producció sense recrear res ni tallar connexions.

Exercicis

Exercici 1: provoca i diagnostica un OOM

Crea un contenidor golafre amb 100 MB de memòria i sense swap que intenti ocupar 300 MB a /dev/shm. Després:

  1. Comprova l'estat, el codi de sortida i el valor d'OOMKilled.
  2. Localitza l'esdeveniment corresponent amb docker events.
  3. Repeteix l'experiment amb un límit de 512 MB i comprova que ara acaba bé.
  4. Repeteix el primer cas però amb --memory-swap 1g en comptes de sense swap. Explica què canvia i per què això rarament és una bona solució.

Exercici 2: compara --cpus amb --cpu-shares

Llança dos contenidors que consumeixin CPU amb un bucle infinit i mesura'n el repartiment amb docker stats en tres escenaris:

  1. contenidor-a amb --cpus 0.5 i contenidor-b sense cap límit.
  2. Tots dos amb --cpu-shares, un amb 1024 i l'altre amb 512.
  3. Només el de --cpu-shares 512, corrent en solitari.

Explica els tres resultats i respon: quina de les dues opcions faries servir per garantir a un client que el seu contenidor mai no superarà mig nucli?, i per repartir una màquina entre un servei crític i un de proves aprofitant tota la CPU disponible?

Exercici 3: posa a prova les polítiques de reinici

Dissenya un experiment que demostri la diferència entre always i unless-stopped sense reiniciar la teva màquina (pista: sudo systemctl restart docker reinicia el dimoni; a Docker Desktop, fes servir "Restart" des del menú). Crea dos contenidors idèntics, un amb cada política, atura'ls tots dos manualment i després reinicia el dimoni. Anota quins contenidors estan corrent en tornar. Finalment:

  1. Crea un tercer contenidor amb --restart on-failure:2 que falli sempre i comprova RestartCount quan es rendeixi.
  2. Amb la plataforma d'Aurora Libros, provoca que aurora-api quedi unhealthy (atura aurora-db) i demostra amb comandes que la política de reinici no hi fa res. Després arregla la situació.

Solucions

Solució a l'exercici 1

docker run -d --name golafre -m 100m --memory-swap 100m --shm-size 512m alpine:3.20 \
  sh -c 'dd if=/dev/zero of=/dev/shm/farciment bs=1M count=300; echo ACABAT'
sleep 5
docker inspect -f 'Estat: {{.State.Status}} | Codi: {{.State.ExitCode}} | OOMKilled: {{.State.OOMKilled}}' golafre
docker logs golafre
Estat: exited | Codi: 137 | OOMKilled: true

Els registres estan buits: ni tan sols va arribar a imprimir "ACABAT". SIGKILL no dona marge per a res.

docker events --since 10m --filter container=golafre
2026-08-04T23:41:02.118 container create 6d2a...  (name=golafre)
2026-08-04T23:41:02.309 container start  6d2a...  (name=golafre)
2026-08-04T23:41:04.771 container oom    6d2a...  (name=golafre)
2026-08-04T23:41:04.883 container die    6d2a...  (exitCode=137, name=golafre)

La seqüència completa en quatre línies: creació, arrencada, oom i mort amb 137. Aquest esdeveniment oom és la prova des del costat del dimoni.

3. Amb marge suficient:

docker rm golafre
docker run --name golafre -m 512m --memory-swap 512m --shm-size 512m alpine:3.20 \
  sh -c 'dd if=/dev/zero of=/dev/shm/farciment bs=1M count=300; echo ACABAT'
docker inspect -f 'Codi: {{.State.ExitCode}} | OOMKilled: {{.State.OOMKilled}}' golafre
300+0 records in
300+0 records out
ACABAT
Codi: 0 | OOMKilled: false

4. Amb swap:

docker rm golafre
docker run --name golafre -m 100m --memory-swap 1g --shm-size 512m alpine:3.20 \
  sh -c 'time dd if=/dev/zero of=/dev/shm/farciment bs=1M count=300; echo ACABAT'
docker inspect -f 'Codi: {{.State.ExitCode}} | OOMKilled: {{.State.OOMKilled}}' golafre
docker rm golafre
300+0 records in
300+0 records out
real    0m 11.84s
ACABAT
Codi: 0 | OOMKilled: false

Ara sobreviu, perquè té 100 MB de RAM més 924 MB de swap. Però mira el temps: 11,84 segons enfront de menys d'un amb memòria real. Aquest és el motiu pel qual el swap rarament és la resposta: el contenidor no mor, però funciona entre deu i cent vegades més lent. En un servei amb usuaris esperant, això és indistingible d'una caiguda, amb l'agreujant que cap alarma no es dispara: no hi ha OOM, no hi ha reinici, no hi ha codi 137. Només un servei insuportablement lent que ningú no sap explicar. És preferible una fallada ràpida i sorollosa.

Solució a l'exercici 2

docker run -d --name cpu-a --cpus 0.5 alpine:3.20 sh -c 'while true; do :; done'
docker run -d --name cpu-b alpine:3.20 sh -c 'while true; do :; done'
sleep 5
docker stats --no-stream cpu-a cpu-b --format "{{.Name}}: {{.CPUPerc}}"
docker rm -f cpu-a cpu-b
cpu-a: 49.94%
cpu-b: 99.87%

1. cpu-a respecta el seu mig nucli encara que la màquina tingui capacitat de sobres; cpu-b, sense límit, ocupa un nucli sencer. El límit dur no cedeix encara que sobri CPU.

docker run -d --name cpu-alt  --cpu-shares 1024 alpine:3.20 sh -c 'while true; do :; done'
docker run -d --name cpu-baix --cpu-shares 512  alpine:3.20 sh -c 'while true; do :; done'
sleep 5
docker stats --no-stream cpu-alt cpu-baix --format "{{.Name}}: {{.CPUPerc}}"
docker rm -f cpu-alt
sleep 5
docker stats --no-stream cpu-baix --format "{{.Name}}: {{.CPUPerc}}"
docker rm -f cpu-baix
cpu-alt: 265.31%
cpu-baix: 132.44%
cpu-baix: 398.72%

2. La proporció és 2:1, exactament 1024 enfront de 512. 3. I tan bon punt cpu-alt desapareix, cpu-baix salta al 398 %: gairebé quatre nuclis. Els shares mai no malgasten capacitat ociosa.

Les dues respostes:

  • Per garantir a un client que mai no superarà mig nucli: --cpus 0.5. És un sostre absolut, verificable i independent del que facin els altres. És el que es factura i el que es pot prometre per contracte.
  • Per repartir una màquina entre un servei crític i un de proves: --cpu-shares, per exemple 1024 i 256. Quan tots dos competeixen, el crític s'emporta quatre vegades més; quan el crític està ociós, el de proves aprofita tota la màquina en comptes de deixar-la aturada. És un repartiment de prioritats, no un sostre.

Solució a l'exercici 3

docker run -d --name pol-always         --restart always         alpine:3.20 sleep 3600
docker run -d --name pol-unless-stopped --restart unless-stopped alpine:3.20 sleep 3600

docker stop pol-always pol-unless-stopped
docker ps -a --filter name=pol- --format "{{.Names}}: {{.Status}}"
pol-always: Exited (137) 2 seconds ago
pol-unless-stopped: Exited (137) 2 seconds ago

Tots dos aturats per decisió teva. Ara reinicia el dimoni:

sudo systemctl restart docker
sleep 10
docker ps -a --filter name=pol- --format "table {{.Names}}\t{{.Status}}"
NAMES                 STATUS
pol-always            Up 8 seconds
pol-unless-stopped    Exited (137) 45 seconds ago

La diferència, en dues línies. always va ignorar la teva aturada manual i va tornar a arrencar el contenidor; unless-stopped va respectar la teva decisió i el va deixar aturat. És exactament l'escenari de l'apartat 9: si haguessis aturat aurora-api per investigar un incident, amb always te l'hauries trobada corrent un altre cop en tornar.

docker rm -f pol-always pol-unless-stopped

1. El comptador de reintents:

docker run -d --name pol-fallada --restart on-failure:2 alpine:3.20 sh -c 'sleep 1; exit 7'
sleep 12
docker inspect -f 'Estat: {{.State.Status}} | Codi: {{.State.ExitCode}} | RestartCount: {{.RestartCount}}' pol-fallada
docker rm -f pol-fallada
Estat: exited | Codi: 7 | RestartCount: 2

RestartCount: 2: l'arrencada original més dos reintents, i Docker es rendeix conservant el codi real de la fallada (7), no un de genèric. Aquest codi és el que et permet diagnosticar.

2. La política no reacciona a la salut:

docker stop aurora-db
sleep 45
docker ps --filter name=aurora-api --format "{{.Names}}: {{.Status}}"
docker inspect -f 'Salut: {{.State.Health.Status}} | Reinicis: {{.RestartCount}} | Política: {{.HostConfig.RestartPolicy.Name}}' aurora-api
curl -s -o /dev/null -w "HTTP %{http_code}\n" http://localhost:8080/api/llibres
aurora-api: Up 12 minutes (unhealthy)
Salut: unhealthy | Reinicis: 1 | Política: on-failure:3
HTTP 500

La demostració és rotunda: el contenidor fa dotze minuts que és unhealthy, l'API retorna 500 als usuaris, i RestartCount continua valent 1 —el reinici del docker kill de la pràctica, no un de nou—. La política de reinici no ha mogut un dit, perquè el procés node mai no va morir. Només va perdre la seva base de dades.

docker start aurora-db
sleep 40
docker ps --filter name=aurora-api --format "{{.Names}}: {{.Status}}"
curl -s http://localhost:8080/api/llibres | jq -r '.llibres | length'
aurora-api: Up 25 minutes (healthy)
9

I tan bon punt torna aurora-db, el healthcheck es recupera sol: /salut torna a retornar 200, l'estat passa a healthy i els nou llibres tornen. Sense reiniciar l'API, perquè mai no va caldre. Fixa't en dues coses: l'API va sobreviure a la caiguda de la seva base de dades gràcies al canvi que li vas fer a la lliçó 03-02, i el diagnòstic correcte —unhealthy sostingut amb RestartCount congelat— és la signatura inconfusible de "el procés està bé, les seves dependències no".

Conclusió

El mòdul està tancat i la plataforma és, per fi, una cosa que es pot defensar. Saps que per defecte un contenidor pot consumir tota la RAM i tota la CPU del host, i saps posar-hi setge: -m com a límit dur, --memory-reservation com a límit tou que decideix qui cedeix memòria sota pressió, i --memory-swap amb el parany que confon tothom —és el total, i amb el mateix valor que --memory es desactiva el swap, que és gairebé sempre el correcte—. Has provocat un OOM a propòsit i l'has diagnosticat amb la signatura que no admet dubtes: OOMKilled: true, codi 137 i un esdeveniment oom al dimoni, amb els registres tallats a mitja frase perquè SIGKILL no es pot capturar.

Distingeixes les tres maneres de repartir CPU: --cpus com a sostre absolut i predictible, --cpu-shares com a pes relatiu que només actua en contenció i mai no malgasta capacitat ociosa, i --cpuset-cpus per fixar nuclis concrets. Saps frenar l'E/S de disc amb --device-write-bps i, sobretot, saps que --pids-limit és la defensa més barata que existeix contra una fork bomb que, sense ell, s'emporta per davant la taula de processos del host sencer. I saps verificar-ho tot amb docker stats i inspect, i canviar-ho en calent amb docker update sense recrear ni un contenidor, perquè els recursos viuen als cgroups i no als namespaces.

Coneixes les quatre polítiques de reinici i la diferència que només s'aprecia en reiniciar el dimoni: always ignora la teva aturada manual i unless-stopped la respecta. Saps llegir RestartCount, reconèixer el backoff exponencial i detectar un contenidor que porta 847 reinicis sense que ningú se n'hagi assabentat. I t'endús la distinció que més confusió evita: una política de reinici et retorna un procés mort, un healthcheck detecta un procés viu però inútil, i Docker Engine no reinicia res per estar unhealthy; per a això cal un orquestrador. Aurora Libros ho té tot aplicat amb criteri: aurora-db amb 512 MB i unless-stopped, aurora-cache amb 256 MB i un --maxmemory 200mb de Redis alineat per sota perquè expulsi claus en comptes de morir, aurora-api amb 256 MB i on-failure:3 perquè un error de configuració no s'amagui en un bucle infinit, i aurora-web amb 128 MB de sobres. Un docker kill aurora-api i el servei va tornar sol en menys de tres segons.

I tanmateix, mira un altre cop aquell script de l'apartat 12. Cinquanta línies de bash per a quatre contenidors: una xarxa, un volum, quatre docker run de catorze línies cadascun, un bucle d'espera escrit a mà perquè l'API no arrenqui abans que PostgreSQL, --network aurora-net repetit quatre vegades, cap comanda per aturar la plataforma sencera i cap manera de saber si el que està corrent es correspon amb el que diu el fitxer. Afegir una variable d'entorn a l'API significa esborrar el contenidor i tornar a enganxar les seves catorze línies exactes sense equivocar-se en el volum. És correcte, és fràgil, i és impossible de revisar en una pull request. Has recorregut un camí enorme des dels quinze passos manuals de la lliçó 01-07, però l'últim tram continua sent un script que ningú no vol mantenir.

Aquest és exactament el problema que resol el mòdul 4, Docker Compose. Aquelles cinquanta línies de comandes imperatives es converteixen en un únic fitxer compose.yaml d'unes quaranta línies declaratives, versionat a Git al costat del codi, que descriu quin és l'estat desitjat de la plataforma —quatre serveis amb les seves imatges, variables, xarxes, volums, límits, dependències i polítiques— en comptes dels passos per arribar-hi. La xarxa i el volum es creen sols; l'ordre d'arrencada es declara amb depends_on i la seva condició de salut en comptes d'amb un bucle until; i tota la plataforma s'aixeca amb docker compose up -d i s'atura amb docker compose down. Una sola comanda, un sol fitxer, i un company nou que clona el repositori i té Aurora Libros funcionant a la seva màquina en menys d'un minut. Els quinze passos d'onboarding passaran a ser un.

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