Fa sis mòduls que fas servir contenidors. Aquesta lliçó explica per què funcionen. La tesi és incòmoda i alliberadora alhora: un contenidor no existeix. No hi ha cap entitat "contenidor" al kernel de Linux. Hi ha processos normals amb tres mecanismes aplicats a sobre, i tots es poden tocar a mà.
Contingut
- La tesi: tres mecanismes i cap objecte
- Els set namespaces
- El mateix procés, dos PID diferents
/proc/<pid>/ns/,lsnsi comparar contenidors- Entrar a mà amb
nsenter - Crear un namespace des de zero amb
unshare - El user namespace i el remapatge d'UID
- Cgroups v2: la jerarquia
- Els límits d'
aurora-db, llegits del kernel - L'OOM killer des de
memory.events - OverlayFS: el muntatge real
- Copy-on-write a l'
upperdir - Capabilities i seccomp a nivell de kernel
- Què fa
runcexactament - Docker Desktop: tot això passa dins d'una VM
- La tesi: tres mecanismes i cap objecte
Quan executes docker run, no es crea cap estructura màgica. Es llança un procés corrent i se li apliquen tres coses:
| Mecanisme | Respon a | El que veus a Docker |
|---|---|---|
| Namespaces | Què veu el procés? | Aïllament de processos, xarxa, fitxers i usuaris |
| Cgroups | Quant pot consumir? | --memory, --cpus, --pids-limit |
| Sistema de fitxers de capes | Quin sistema de fitxers té? | Imatges, capes, copy-on-write |
flowchart TB
P["Procés normal de Linux<br/>(node src/server.js)"]
NS["NAMESPACES<br/>pid · net · mnt · uts<br/>ipc · user · cgroup"]
CG["CGROUPS v2<br/>memory.max · cpu.max<br/>pids.max · io.max"]
FS["OVERLAYFS<br/>lowerdir + upperdir<br/>= merged"]
P --> NS --> C(("El que anomenem<br/>«contenidor»"))
P --> CG --> C
P --> FS --> C
K["Kernel del host: UN DE SOL, compartit per tothom"] --- C
Tota la resta —imatges, registres d'imatges, Compose, healthchecks— és maquinària construïda al voltant d'aquelles tres primitives del kernel.
- Els set namespaces
Un namespace és una vista parcial d'un recurs global del sistema. Els processos d'un namespace veuen la seva versió del recurs i no la dels altres.
| Namespace | Aïlla | Opció de Docker |
|---|---|---|
pid |
Arbre de processos | --pid=host el desactiva; --pid=container:X el comparteix |
net |
Interfícies, rutes, iptables, ports |
--network (lliçó 05-01) |
mnt |
Punts de muntatge | L'arrel del contenidor i tots els -v |
uts |
Nom de host i de domini | --hostname |
ipc |
Memòria compartida, cues de missatges | --ipc=host, --shm-size |
user |
Correspondència d'UID i GID | --userns-remap, mode rootless (lliçó 05-03) |
cgroup |
Vista de la jerarquia de cgroups | --cgroupns |
Un vuitè, time (rellotge monotònic), existeix al kernel des de 5.6 però Docker encara no el fa servir.
- El mateix procés, dos PID diferents
Aquesta és la demostració que convenç més ràpid.
docker compose exec aurora-api ps -eo pid,comm
pid=$(docker inspect aurora-libros-aurora-api-1 --format '{{.State.Pid}}')
ps -o pid,ppid,user,comm -p "$pid"PID COMMAND <- des de DINS del contenidor
1 node
28 ps
PID PPID USER COMMAND <- des del HOST
48122 4791 1000 nodeÉs el mateix procés. A dins és el PID 1 —d'aquí tot el que vas aprendre a la lliçó 03-02 sobre senyals i PID 1—; al host és el 48122, fill del shim de containerd i executant-se com l'usuari 1000. No hi ha cap màquina virtual: hi ha un node a la teva llista de processos que resulta que veu un arbre diferent del teu.
NSpid ho diu tot: 48122 al namespace del host, 1 al seu. Un mateix procés amb dues identitats simultànies.
/proc/<pid>/ns/, lsns i comparar contenidors
/proc/<pid>/ns/, lsns i comparar contenidorsCada namespace és un fitxer especial l'inode del qual l'identifica:
cgroup -> cgroup:[4026532897]
ipc -> ipc:[4026532835]
mnt -> mnt:[4026532833]
net -> net:[4026532838]
pid -> pid:[4026532836]
user -> user:[4026531837]
uts -> uts:[4026532834]Comparar dos contenidors és comparar aquells números:
pdb=$(docker inspect aurora-libros-aurora-db-1 --format '{{.State.Pid}}')
for ns in net pid mnt user; do
a=$(sudo readlink /proc/$pid/ns/$ns); b=$(sudo readlink /proc/$pdb/ns/$ns)
[ "$a" = "$b" ] && echo "$ns: COMPARTIT" || echo "$ns: diferent"
done
echo "host user ns: $(sudo readlink /proc/1/ns/user)"El resultat més revelador és l'últim: aurora-api i aurora-db comparteixen el namespace d'usuari, i a més és el del host. Sense --userns-remap ni mode rootless, l'UID 0 de dins és literalment l'UID 0 del host. Aquí la tens, mesurada, l'afirmació de la lliçó 05-03 sobre per què root en un contenidor és perillós.
sudo lsns -t net -o NS,PID,COMMAND | head -4
# 4026532838 48122 node src/server.js
# 4026532901 48310 postgres
# 4026532955 48502 nginx: master process
- Entrar a mà amb
nsenter
nsenterdocker exec no és màgia: és entrar als namespaces del procés i llançar-hi una comanda. nsenter fa el mateix, i sense passar per Docker.
a91f3c8d2e10
app bin dev etc home lib proc root sys tmp usr var
eth0 UP 172.21.0.5/16
PID COMMAND
1 nodeLes banderes són exactament els namespaces: -m (mnt), -u (uts), -i (ipc), -n (net), -p (pid). Pots entrar només en alguns, que és el que fa útil la tècnica: -n tot sol et dona la xarxa del contenidor amb les eines del host —just el que feies a la lliçó 05-01— i funciona fins i tot amb imatges distroless que no tenen ni sh a dins.
- Crear un namespace des de zero amb
unshare
unsharePer veure que no hi ha màgia, construïm-ne un a mà.
sudo unshare --pid --fork --mount-proc --uts --net --mount \
sh -c 'hostname aurora-manual; ps -eo pid,comm; ip -brief addr; hostname'Sense Docker, sense imatges i sense daemon: un shell que es creu PID 1, amb el seu propi nom de host i una pila de xarxa buida. Això és l'aïllament d'un contenidor. El que Docker hi afegeix a sobre és tota la resta: el sistema de fitxers de la imatge, la configuració de xarxa amb el seu veth i el seu pont, els cgroups, les capacitats, seccomp i una API per gestionar-ho.
- El user namespace i el remapatge d'UID
El namespace d'usuari permet que un mateix UID signifiqui coses diferents dins i fora. És la base del mode rootless (lliçó 05-03).
unshare --user --map-root-user sh -c 'id -u; cat /proc/self/uid_map; cat /etc/shadow' 2>&1 | tail -3
id -uA dins ets root (id -u = 0) sense haver fet servir sudo; el mapa diu que l'UID 0 de dins correspon al 1000 de fora, amb longitud 1. A fora continues sent el 1000.
Root a dins, però el kernel comprova els permisos del fitxer contra l'UID real del host. Aquest és el mecanisme exacte que fa que en mode rootless l'escalada de la lliçó 05-03 deixi de funcionar: no és una comprovació afegida per Docker, és aritmètica d'UID al kernel.
- Cgroups v2: la jerarquia
Els control groups limiten i comptabilitzen recursos. La versió 2, unificada, és l'estàndard des del 2022 i s'exposa com un arbre de directoris a /sys/fs/cgroup.
docker info --format 'Cgroup Driver: {{.CgroupDriver}} | Version: {{.CgroupVersion}}'
ls /sys/fs/cgroup/system.slice/ | grep docker | head -1| Controlador | Fitxers clau | Què governa |
|---|---|---|
memory |
memory.max, memory.current, memory.high, memory.events |
RAM i activació de l'OOM killer |
cpu |
cpu.max, cpu.stat, cpu.weight |
Quota de CPU i estrangulament |
io |
io.max, io.stat |
Amplada de banda i IOPS de disc |
pids |
pids.max, pids.current |
Nombre de processos (defensa davant de fork bomb) |
La diferència essencial amb la v1: a la v2 un procés pertany a un únic cgroup i tots els controladors hi actuen a sobre, en lloc d'una jerarquia diferent per recurs. Això simplifica enormement el raonament.
- Els límits d'
aurora-db, llegits del kernel
aurora-db, llegits del kernelAra la comprovació que tanca el cercle amb la lliçó 03-07 i amb Compose: el que vas declarar en YAML, llegit directament del kernel.
id=$(docker inspect aurora-libros-aurora-db-1 --format '{{.Id}}')
cg=/sys/fs/cgroup/system.slice/docker-$id.scope
for f in memory.max memory.current memory.high pids.max pids.current cpu.max; do
printf '%-16s %s\n' "$f" "$(cat $cg/$f)"
donememory.max 2147483648
memory.current 432791552
memory.high max
pids.max 200
pids.current 14
cpu.max 200000 100000Traducció, línia a línia: memory.max són 2 147 483 648 bytes = 2 GiB, el limits: { memory: 2G } del compose.prod.yaml; memory.current és el que mostra docker stats; pids.max és el pids_limit: 200; i cpu.max amb 200000 100000 significa 200 ms de CPU per cada 100 ms de període, és a dir, cpus: "2.0".
docker stats --no-stream --format '{{.Name}} {{.MemUsage}}' aurora-libros-aurora-db-1
# aurora-libros-aurora-db-1 412.7MiB / 2GiBCoincidència exacta. docker stats no calcula res: llegeix aquests fitxers. I cpu.max explica per fi la semàntica de --cpus: no és un nombre de nuclis assignats, és una quota de temps per període; amb 2.0, el contenidor pot consumir 200 ms de CPU cada 100 ms, repartits entre els nuclis que hi hagi.
- L'OOM killer des de
memory.events
memory.eventsdocker run -d --name victima --memory 64m --memory-swap 64m alpine:3 \
sh -c 'x=""; while true; do x="$x$(head -c 1048576 /dev/zero | tr "\0" "a")"; done'
sleep 6
id=$(docker inspect victima --format '{{.Id}}')
cat /sys/fs/cgroup/system.slice/docker-$id.scope/memory.events 2>/dev/null || echo "(cgroup ja eliminat)"
docker inspect victima --format 'exit={{.State.ExitCode}} oom={{.State.OOMKilled}}'
docker rm -f victimaEls comptadors expliquen la història completa: max 412 són les vegades que el procés va topar amb el límit i el kernel va haver de reclamar memòria; oom 1 indica que va arribar un punt en què no en va poder reclamar més; oom_kill 1 és l'execució. El resultat, el codi de sortida 137 que coneixes des de la lliçó 03-02, és simplement 128 + 9: acabat per SIGKILL.
Un max alt amb oom_kill 0 és un senyal valuosíssim i gairebé mai vigilat: el contenidor no ha mort, però està lluitant contra el seu límit i pagant-ho en latència. És exactament el que l'alerta MemoriaAPropDelLimit de la lliçó 05-06 detecta abans que sigui tard.
- OverlayFS: el muntatge real
overlay on / type overlay (rw,relatime,lowerdir=/var/lib/docker/overlay2/l/QW3F:/var/lib/docker/overlay2/l/K7RT:/var/lib/docker/overlay2/l/P2MN,upperdir=/var/lib/docker/overlay2/9be21c/diff,workdir=/var/lib/docker/overlay2/9be21c/work)L'arrel / del contenidor és un muntatge overlay, amb els quatre directoris de la lliçó 05-02. I els lowerdir es corresponen un a un amb les capes de la imatge:
docker image inspect auroralibros/aurora-api:1.3.0 --format '{{len .RootFS.Layers}} capes'
docker compose exec aurora-api sh -c 'mount|grep " / "' | grep -o 'lowerdir=[^,]*' | tr ':' '\n' | wc -l
# 3 capes
# 3Tres capes al manifest de la imatge, tres lowerdir al muntatge. La correspondència és literal: cada capa de la imatge és un directori al disc del host, i OverlayFS les apila per ordre.
- Copy-on-write a l'
upperdir
upperdirup=$(docker inspect aurora-libros-aurora-api-1 --format '{{.GraphDriver.Data.UpperDir}}')
docker compose exec aurora-api sh -c 'echo "nota" > /tmp/nota.txt' # 1) crear
docker compose exec aurora-api sh -c 'echo "// tocat" >> /app/package.json' # 2) modificar
docker compose exec aurora-api sh -c 'rm -f /app/src/util.js' # 3) esborrar
sudo ls -l "$up/tmp/nota.txt" "$up/app/src/util.js"
docker diff aurora-libros-aurora-api-1 | head -3-rw-r--r-- 1 node node 5 ago 5 11:02 .../diff/tmp/nota.txt
c--------- 1 root root 0, 0 ago 5 11:02 .../diff/app/src/util.js
A /tmp/nota.txt
C /app/package.json
D /app/src/util.jsLes tres operacions, amb els seus tres efectes diferents a l'upperdir:
- Fitxer nou: apareix tal qual. És l'
Adedocker diff. - Fitxer modificat: el kernel fa copy-up —copia l'original sencer del
lowerdir— i hi escriu a sobre. És laC, i és l'operació cara que vas mesurar a la lliçó 05-02. - Fitxer esborrat: apareix un dispositiu de caràcters 0/0, el famós whiteout. No esborra res: amaga el fitxer de la capa inferior, que continua ocupant espai. És la
D, i és la raó que esborrar en una capa posterior no aprimi una imatge (lliçó 05-04).
Aquí la tens, en tres línies d'ls, l'explicació física de les lliçons 01-05, 05-02 i 05-04.
- Capabilities i seccomp a nivell de kernel
Les capacitats de la lliçó 05-03 són una màscara de bits al descriptor del procés.
sudo grep -E '^(CapEff|CapBnd|Seccomp|NoNewPrivs)' /proc/$pid/status
capsh --decode=$(sudo awk '/^CapEff/{print $2}' /proc/$pid/status)CapEff a zero: aurora-api corre sense cap capacitat efectiva, exactament com vas demanar amb cap_drop: [ALL]. Compara-ho amb un contenidor per defecte:
docker run -d --name perDefecte alpine:3 sleep 60
p2=$(docker inspect perDefecte --format '{{.State.Pid}}')
capsh --decode=$(sudo awk '/^CapEff/{print $2}' /proc/$p2/status) | tr ',' '\n' | head -4
docker rm -f perDefecteDocker concedeix catorze capacitats per defecte. La diferència entre aquella llista i un CapEff de zero és tota la superfície que has eliminat, i ara la veus com el que és: bits a /proc.
Seccomp: 2 significa mode filtre BPF actiu (0 seria desactivat); NoNewPrivs: 1 és el no-new-privileges:true del compose.prod.yaml. Tot el que vas configurar en YAML acaba sent dos números en un fitxer de /proc.
- Què fa
runc exactament
runc exactamentLa cadena de la lliçó 01-03, ara vista per dins: dockerd → containerd → shim → runc → procés.
runc és un binari petit que rep un directori amb dues coses: el sistema de fitxers ja muntat i un config.json amb l'especificació OCI del contenidor.
{
"ociVersion": "1.2.0",
"process": {
"user": { "uid": 1000, "gid": 1000 },
"args": ["node", "src/server.js"],
"capabilities": { "bounding": [], "effective": [], "permitted": [] },
"noNewPrivileges": true
},
"root": { "path": "rootfs", "readonly": true },
"linux": {
"namespaces": [ { "type": "pid" }, { "type": "network" }, { "type": "ipc" },
{ "type": "uts" }, { "type": "mount" }, { "type": "cgroup" } ],
"resources": {
"memory": { "limit": 536870912 },
"cpu": { "quota": 200000, "period": 100000 },
"pids": { "limit": 200 }
},
"seccomp": { "defaultAction": "SCMP_ACT_ERRNO" }
}
}Reconeixes cada línia: són les teves opcions de docker run i de Compose traduïdes a l'estàndard. Els passos de runc són: crear els namespaces (clone amb les banderes CLONE_NEW*), escriure els cgroups, pivotar l'arrel al rootfs (pivot_root), aplicar capacitats i seccomp, i finalment execve de la comanda. Aquí acaba la seva feina: runc surt, i el procés queda orfe sota el shim.
Aquest és el paper del shim (containerd-shim-runc-v2): mantenir oberts stdin/stdout/stderr, informar del codi de sortida i —el que és important— permetre que dockerd es reiniciï sense matar els teus contenidors, perquè el pare no és el daemon sinó el shim.
ps -o pid,ppid,comm -p "$pid"; ps -o pid,comm -p "$(ps -o ppid= -p $pid | tr -d ' ')"
# 48122 4791 node
# 4791 containerd-shimQue tot això sigui un estàndard obert (l'OCI Runtime Specification) és el que permet substituir runc per crun, youki o gVisor sense canviar res més. Aquell ecosistema és la lliçó 07-05.
- Docker Desktop: tot això passa dins d'una VM
Si ets a macOS o Windows, res de tot l'anterior no passa al teu sistema operatiu: namespaces i cgroups són mecanismes exclusius del kernel de Linux. Docker Desktop executa una màquina virtual Linux lleugera i el daemon viu a dins.
| Aspecte | Linux | macOS / Windows amb Docker Desktop |
|---|---|---|
| On corre el daemon | Al teu kernel | En una VM Linux |
/var/lib/docker |
Ruta del teu disc | Dins del disc virtual de la VM |
ps aux del host |
Veus els processos dels contenidors | No els veus: són a la VM |
| Bind mounts | Cost zero | Creuen la frontera VirtioFS/WSL 2: més lents |
nsenter, lsns, docker0, cgroups |
Directes | Només dins de la VM |
Per reproduir aquesta lliçó des de macOS o Windows, entra a la VM:
docker run -it --rm --privileged --pid=host justincormack/nsenter1 /bin/sh
# ja dins de la VM:
ls /sys/fs/cgroup/ && lsns -t pid | head -3I això explica de passada les lliçons anteriors: la lentitud dels bind mounts (05-02 i 04-07) no és de Docker, és la frontera entre dos sistemes de fitxers; i per això les rutes i els processos "no apareixen" on els esperes.
Errors Habituals i Consells
Creure que un contenidor és una màquina lleugera. És un procés amb tres mecanismes a sobre i un sol kernel compartit. D'aquí tota la lliçó 05-03.
Buscar cgroups v1 en un sistema modern. Des del 2022 és v2 unificat: un cgroup per procés i fitxers diferents (memory.max, no memory.limit_in_bytes). I no escriguis directament a /sys/fs/cgroup: Docker ho sobreescriu en reconciliar; fes servir docker update o Compose.
Executar nsenter sense -p esperant veure l'arbre de processos aïllat. A cada namespace s'entra per separat; sense -p veuràs els processos del host.
Suposar que un oom_kill 0 significa que tot va bé. Un max alt indica pressió de memòria constant i latència, encara que no hi hagi mort ningú.
Intentar lsns o /sys/fs/cgroup des de macOS. No existeixen fora de Linux. Entra a la VM de Docker Desktop.
Consell: quan alguna cosa no quadri, baixa un nivell. Un límit que sembla que no s'aplica es comprova a memory.max; un problema de xarxa, a /proc/<pid>/ns/net i nsenter -n; un fitxer que apareix o desapareix, a l'upperdir. El kernel no menteix i sempre es pot consultar.
Exercicis
Exercici 1. Demostra que un contenidor és un procés del host: troba el PID real d'aurora-api, comprova que a dins és el PID 1, verifica amb NSpid que és el mateix procés i entra al seu namespace de xarxa amb nsenter sense fer servir docker exec.
Exercici 2. Comprova que els límits de Compose són exactament els fitxers de cgroups: llegeix memory.max, cpu.max i pids.max d'aurora-db, tradueix-los a les opcions que els van produir, canvia'ls amb docker update i verifica que el fitxer canvia en calent.
Exercici 3. Provoca les tres operacions de copy-on-write (crear, modificar i esborrar) a aurora-api i localitza'n els tres efectes diferents a l'upperdir. Explica quina de les tres explica que esborrar fitxers no aprimi una imatge.
Solucions
Solució 1.
pid=$(docker inspect aurora-libros-aurora-api-1 --format '{{.State.Pid}}')
echo "PID al host: $pid"
docker compose exec aurora-api sh -c 'echo "PID a dins: $$"'
sudo grep NSpid /proc/$pid/status
sudo nsenter -t "$pid" -n ip -brief addr
sudo nsenter -t "$pid" -n -p -m ps -eo pid,comm | head -2PID al host: 48122
PID a dins: 1
NSpid: 48122 1
eth0 UP 172.21.0.5/16
eth1 UP 172.22.0.4/16
PID COMMAND
1 nodeNSpid: 48122 1 és la prova definitiva: un únic procés amb dues identitats, la del namespace del host i la del seu. No hi ha còpia, ni emulació, ni màquina virtual; hi ha una taula de correspondències al kernel.
I nsenter demostra la segona cosa: has obtingut exactament el que dona docker exec sense parlar amb el daemon en cap moment. Això té dues conseqüències pràctiques. La primera, de diagnòstic: si el daemon està penjat o la imatge és distroless i no té sh, nsenter continua funcionant perquè fa servir els binaris del host sobre els namespaces del contenidor. La segona, de seguretat: qui tingui root al host entra a qualsevol contenidor sense deixar rastre als logs de Docker, cosa que reforça per què l'accés root al host és la frontera que de debò importa.
Solució 2.
id=$(docker inspect aurora-libros-aurora-db-1 --format '{{.Id}}')
cg=/sys/fs/cgroup/system.slice/docker-$id.scope
printf 'memory.max %s | cpu.max %s | pids.max %s\n' \
"$(cat $cg/memory.max)" "$(cat $cg/cpu.max)" "$(cat $cg/pids.max)"| Fitxer del kernel | Valor | Opció que el va produir |
|---|---|---|
memory.max |
2 147 483 648 B = 2 GiB | deploy.resources.limits.memory: 2G |
cpu.max |
200000 / 100000 | cpus: "2.0" (200 ms per cada 100 ms) |
pids.max |
200 | pids_limit: 200 |
docker update --memory 1g --memory-swap 1g --cpus 1.5 aurora-libros-aurora-db-1
printf 'memory.max %s | cpu.max %s\n' "$(cat $cg/memory.max)" "$(cat $cg/cpu.max)"
docker stats --no-stream --format '{{.MemUsage}}' aurora-libros-aurora-db-1El canvi és instantani i sense reiniciar el contenidor: cgroups v2 permet reescriure el límit en calent, i el procés ni se n'assabenta fins que intenti superar-lo. docker stats reflecteix el nou valor perquè, literalment, llegeix aquell fitxer.
La lectura de fons és que docker run --memory i deploy.resources no són abstraccions de Docker: són una manera còmoda d'escriure un número a /sys/fs/cgroup. I la interpretació de cpu.max resol la confusió més habitual del curs: --cpus 1.5 no reserva un nucli i mig, concedeix 150 ms de temps de CPU cada 100 ms de període, que es poden repartir entre els vuit nuclis del host.
Solució 3.
up=$(docker inspect aurora-libros-aurora-api-1 --format '{{.GraphDriver.Data.UpperDir}}')
docker compose exec aurora-api sh -c 'echo hola > /tmp/nou.txt
echo "//x" >> /app/package.json
rm -f /app/src/util.js'
sudo stat -c '%n -> %F (%s bytes)' "$up/tmp/nou.txt" "$up/app/package.json" "$up/app/src/util.js"
docker diff aurora-libros-aurora-api-1 | grep -E 'nou|package.json|util.js'.../diff/tmp/nou.txt -> regular file (5 bytes)
.../diff/app/package.json -> regular file (612 bytes)
.../diff/app/src/util.js -> character special file (0 bytes)
A /tmp/nou.txt
C /app/package.json
D /app/src/util.jsTres operacions, tres representacions físicament diferents a l'upperdir:
| Operació | A l'upperdir |
docker diff |
|---|---|---|
| Crear | Fitxer normal de 5 bytes | A |
| Modificar | Fitxer normal de 612 bytes: la còpia completa, no el delta | C |
| Esborrar | Dispositiu de caràcters 0/0 de 0 bytes: un whiteout | D |
La tercera és la que explica el fenomen de la lliçó 05-04. L'esborrat no elimina res del lowerdir —és de només lectura, no es pot—: crea un marcador especial que fa que OverlayFS amagui el fitxer en resoldre la vista unificada. Dins del contenidor, ls respon No such file or directory; al disc, el fitxer original continua íntegre a la seva capa, es descarrega a cada docker pull i s'emmagatzema a cada node.
D'aquí la regla que vas aplicar amb l'&& i que ara té explicació completa: si crees i esborres dins del mateix RUN, totes dues operacions passen abans de consolidar la capa, i el fitxer no arriba mai a existir al lowerdir de ningú. I per això una credencial escrita i esborrada en instruccions diferents continua sent extraïble amb docker save: no és una fallada de Docker, és com funciona un sistema de fitxers d'unió.
I fixa't en la segona fila, que també té conseqüències: modificar un byte desa el fitxer sencer a l'upperdir. Amb package.json són 612 bytes; amb un fitxer de dades de PostgreSQL serien centenars de megabytes. Aquest és, mesurat a la lliçó 05-02 i explicat aquí, el motiu físic que les bases de dades facin servir volums.
Conclusió
Un contenidor no existeix. El que existeix és un procés normal de Linux —l'has vist amb el seu PID real al ps del host i el seu NSpid amb dues identitats— al qual el kernel aplica tres mecanismes: namespaces per al que veu, cgroups per al que consumeix i OverlayFS per al sistema de fitxers que té. Coneixes els set namespaces i quina opció de Docker es recolza en cadascun, saps llegir-los a /proc/<pid>/ns/, comparar-los amb lsns, entrar-hi a mà amb nsenter sense passar pel daemon i crear-ne un des de zero amb unshare, comprovant que l'aïllament no té res de màgic. I has descobert, comparant inodes, que sense rootless ni --userns-remap tots els teus contenidors comparteixen el namespace d'usuari del host: l'explicació exacta de per què root a dins és root a fora.
Has llegit els teus propis límits al kernel: memory.max amb els 2 GiB del compose.prod.yaml, pids.max amb els 200, i cpu.max amb 200000 100000, que revela que --cpus 2.0 és una quota de temps per període i no una reserva de nuclis. Els has canviat en calent amb docker update i has vist el fitxer moure's. Has vist l'OOM killer des de dins, a memory.events, amb el seu oom_kill 1 i el codi 137 que ja coneixies. I has tocat el copy-on-write amb les mans: un fitxer nou, una còpia completa per modificar un byte i un dispositiu de caràcters 0/0 per esborrar, que és l'explicació física que esborrar fitxers no aprimi una imatge ni elimini un secret. Tanques el cercle de la seguretat veient CapEff a zero descodificat amb capsh, Seccomp: 2 i NoNewPrivs: 1; i saps què fa runc amb el seu config.json de l'especificació OCI i per què el shim permet reiniciar dockerd sense matar res. Amb la nota imprescindible per a qui treballa a macOS o Windows: tot això passa dins d'una VM Linux, i d'aquí venen les rutes i els rendiments que no quadraven.
I amb això es tanca el mòdul 5. Hi vas entrar sabent fer servir Docker molt bé i en surts sabent què passa exactament quan l'executes. Has obert les xarxes fins als ponts, els parells veth i les regles d'iptables, amb l'avís que publicar un port salta el tallafocs del host; l'emmagatzematge fins a overlay2 i una estratègia de còpies amb restauració verificada; la seguretat fins al model d'amenaces, el mode rootless, les capacitats, seccomp, l'escaneig de CVE i la signatura amb Cosign, amb Aurora Libros endurida en un compose.prod.yaml comentat; l'optimització, que va portar l'API de 142 MB a 102 MB amb multietapa i mesurament constant; BuildKit i Buildx, amb muntatges de memòria cau, secrets sense rastre, memòria cau remota i multiarquitectura; l'observabilitat, amb logs estructurats, Loki, Prometheus, alertes per proporció i un /salut que diu la veritat; i, per fi, el kernel que ho sosté tot.
Al mòdul 6, Aurora Libros surt de la teva màquina. Prepararàs la imatge definitiva per a producció, muntaràs un pipeline de CI/CD que construeixi, escanegi, signi i publiqui amb la memòria cau remota que ja saps fer servir, i portaràs la plataforma a un clúster: primer amb Docker Swarm i les seves xarxes overlay, després amb Kubernetes —els seus objectes, el desplegament real dels quatre serveis, l'escalat i el balanceig de càrrega— i acabaràs amb les estratègies de desplegament i rollback que permeten publicar una versió nova sense que ningú deixi de poder comprar un llibre.
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
