La plataforma funciona. curl http://localhost:3000/llibres retorna els vuit títols i la web carrega al navegador. Però hi ha una esquerda als fonaments amb la qual ja has ensopegat dues vegades sense donar-li importància: cada vegada que recrees aurora-db, el catàleg desapareix i has de tornar a carregar init.sql a mà amb docker cp. En una màquina de desenvolupament això és una molèstia. Al servidor on viu el catàleg real d'una llibreria, és una catàstrofe.

Aquesta lliçó resol el segon gran problema dels contenidors, després de la xarxa: les dades. Demostraràs la pèrdua amb un experiment que fa una mica de por, coneixeràs els tres tipus de muntatge que ofereix Docker i quan fer servir cadascun, entendràs on viuen de debò els volums i per què no els has de tocar a mà, i donaràs a PostgreSQL un volum amb nom perquè el catàleg d'Aurora Libros sobrevisqui a la destrucció del seu contenidor. Al final repetiràs l'experiment del principi i el resultat serà el contrari.

Contingut

  1. El problema, demostrat
  2. Els tres tipus de muntatge
  3. Volums gestionats per Docker
  4. Volums anònims i el problema dels orfes
  5. Bind mounts: fitxers de la teva màquina dins del contenidor
  6. -v enfront de --mount
  7. Muntatges de només lectura
  8. tmpfs: dades en memòria
  9. Pràctica: aurora-dades i la prova de destrucció
  10. Còpies de seguretat i restauració
  11. Bones pràctiques

  1. El problema, demostrat

Comencem per mirar el desastre a la cara. Afegeix un llibre nou al catàleg:

docker exec aurora-db psql -U aurora -d aurora_llibres -c \
  "INSERT INTO llibres (titol, autor, isbn, preu) VALUES
   ('El Aleph', 'Jorge Luis Borges', '978-84-206-3311-4', 15.20);"
docker exec aurora-db psql -U aurora -d aurora_llibres -t -c "SELECT COUNT(*) FROM llibres;"
curl -s http://localhost:3000/llibres | jq -r '.llibres | length'
INSERT 0 1
     9
9

Nou llibres. L'API els serveix. Ara simula el que passa en qualsevol operació normal: actualitzar la imatge de PostgreSQL, canviar una variable d'entorn, afegir un límit de memòria... tot això obliga a recrear el contenidor, com saps des de la lliçó 03-01.

docker rm -f aurora-db

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 \
  postgres:16-alpine

sleep 6
docker exec aurora-db psql -U aurora -d aurora_llibres -c "SELECT COUNT(*) FROM llibres;"
ERROR:  relation "llibres" does not exist
LINE 1: SELECT COUNT(*) FROM llibres;
                             ^

No és que falti El Aleph: és que ja no existeix la taula. Tot el catàleg d'Aurora Libros s'ha evaporat. I l'API, coherent:

curl -s http://localhost:3000/llibres | jq -c
{"error":"No s'ha pogut obtenir el catàleg","detall":"relation \"llibres\" does not exist"}

Això no és una fallada de Docker: és el seu disseny, i ja el coneixies des de la lliçó 01-05. Un contenidor escriu en una capa d'escriptura que neix amb ell i mor amb ell. Les nou files eren en un volum anònim que Docker va crear automàticament —ho vas descobrir a l'exercici 2 de la lliçó 03-03— i que en recrear el contenidor va ser substituït per un altre nou i buit.

flowchart TB
    subgraph SENSE["Sense volum: les dades moren amb el contenidor"]
        direction TB
        S1["docker run postgres"] --> S2["Capa d'escriptura<br/>+ volum anònim nou"]
        S2 --> S3["INSERT ... 9 llibres"]
        S3 --> S4["docker rm -f"]
        S4 --> S5["Tot destruït<br/>o orfe i inabastable"]
    end
    subgraph AMB["Amb volum amb nom: les dades són independents"]
        direction TB
        V1["docker volume create aurora-dades"] --> V2["docker run -v aurora-dades:/var/lib/postgresql/data"]
        V2 --> V3["INSERT ... 9 llibres"]
        V3 --> V4["docker rm -f"]
        V4 --> V5["El volum HI CONTINUA SENT"]
        V5 --> V6["docker run ... mateix volum<br/>→ els 9 llibres tornen"]
    end

La solució és treure les dades del contenidor. Anem-hi.

  1. Els tres tipus de muntatge

Docker ofereix tres maneres que una ruta dins del contenidor apunti a alguna cosa que no és a la seva capa d'escriptura:

Volum Bind mount tmpfs
On viuen les dades En una àrea gestionada per Docker (/var/lib/docker/volumes/) En una ruta que tries tu de la teva màquina A la memòria RAM del host
Qui el gestiona Docker Tu El nucli
Sobreviu a docker rm? Sí (és el teu fitxer) No, s'evapora
Sobreviu a reiniciar el host? No
Portabilitat entre màquines Alta (mateixa comanda a tot arreu) Baixa (depèn de les rutes del host) Alta
Rendiment a Docker Desktop Bo Pitjor (creua la frontera amb la VM) El millor
Còpies de seguretat Amb docker run + tar, o docker volume Amb les eines del host No aplica
Cas d'ús natural Dades de producció: bases de dades, pujades d'usuaris Desenvolupament i fitxers de configuració Secrets i fitxers temporals sensibles
Sintaxi --mount type=volume type=bind type=tmpfs
flowchart LR
    subgraph HOST["Host"]
        VOL["/var/lib/docker/volumes/aurora-dades/_data<br/>(gestionat per Docker)"]
        DIR["~/aurora-libros/db/init.sql<br/>(fitxer teu)"]
        RAM["Memòria RAM"]
    end
    subgraph CONT["Contenidor aurora-db"]
        M1["/var/lib/postgresql/data"]
        M2["/docker-entrypoint-initdb.d/init.sql"]
        M3["/tmp/sensible"]
    end
    VOL -- "volum" --> M1
    DIR -- "bind mount :ro" --> M2
    RAM -- "tmpfs" --> M3

La regla de decisió, en una frase: si les dades importen i les gestiona l'aplicació, un volum; si el fitxer és teu i el vols editar des del teu editor, un bind mount; si no ha de tocar el disc mai, tmpfs.

  1. Volums gestionats per Docker

docker volume create aurora-dades
docker volume ls
aurora-dades
DRIVER    VOLUME NAME
local     aurora-dades
local     b41f7c9e2a8d05f31c6e8b4a2d97f0c5e3a1b8d6f4c2e0a9b7d5f3c1e8a6b4d2

Aquest segon nom il·legible és un volum anònim orfe, resta d'algun dels aurora-db que has esborrat. Hi tornarem.

Comanda Què fa
docker volume create NOM Crea un volum
docker volume ls Els llista tots
docker volume ls -f dangling=true Només els que no fa servir cap contenidor
docker volume inspect NOM Mostra la seva ruta real i les seves metadades
docker volume rm NOM L'esborra (falla si està en ús)
docker volume prune Esborra tots els volums sense fer servir

On viuen realment?

docker volume inspect aurora-dades
[
  {
    "CreatedAt": "2026-08-04T22:03:11+02:00",
    "Driver": "local",
    "Labels": {},
    "Mountpoint": "/var/lib/docker/volumes/aurora-dades/_data",
    "Name": "aurora-dades",
    "Options": {},
    "Scope": "local"
  }
]

Mountpoint és la ruta al host. I aquí va l'avís més important de l'apartat:

No hi entris a tocar fitxers. Aquesta ruta és propietat del dimoni de Docker: pertany a root, els seus permisos i el seu context de seguretat els gestiona Docker, i a Docker Desktop (macOS i Windows) ni tan sols existeix a la teva màquina, sinó dins d'una màquina virtual Linux a la qual no tens accés directe. Escriure-hi a mà és una manera excel·lent de corrompre una base de dades.

La manera correcta de mirar dins d'un volum és amb un contenidor:

docker run --rm -v aurora-dades:/dades alpine:3.20 ls -la /dades
total 8
drwxr-xr-x    2 root     root          4096 Aug  4 22:03 .
drwxr-xr-x    1 root     root          4096 Aug  4 22:05 ..

Buit, acabat de crear. Aquest patró —un contenidor efímer d'Alpine que munta el volum— és la navalla suïssa per inspeccionar, copiar i restaurar volums, i el faràs servir a l'apartat 10.

Etiquetes en volums

docker volume create --label projecte=aurora-libros aurora-copies
docker volume ls --filter label=projecte=aurora-libros
aurora-copies
DRIVER    VOLUME NAME
local     aurora-copies

Igual que amb els contenidors, etiquetar permet netejar de manera selectiva:

docker volume prune --filter "label=projecte=aurora-libros" -f

  1. Volums anònims i el problema dels orfes

Un volum anònim és el que Docker crea pel seu compte, sense que li ho demanis, en dues situacions:

  1. Quan la imatge declara VOLUME /ruta al seu Dockerfile (ho fan postgres, redis, mysql, mongo…).
  2. Quan muntes amb -v /ruta indicant només la destinació, sense origen.
docker run -d --name demo-anonim -v /dades alpine:3.20 sleep 60
docker inspect -f '{{range .Mounts}}{{.Type}} | {{.Name}} | {{.Destination}}{{end}}' demo-anonim
volume | e83f1c7a92b40d651fe8a3c9d7b2f4e6a0c8b1d5f3a7c9e1b8d6f4a2c0e8b6d4 | /dades

Aquest nom de 64 caràcters és el volum anònim. I aquí hi ha el seu problema:

docker rm -f demo-anonim
docker volume ls -f dangling=true --format "{{.Name}}" | head -3
demo-anonim
b41f7c9e2a8d05f31c6e8b4a2d97f0c5e3a1b8d6f4c2e0a9b7d5f3c1e8a6b4d2
e83f1c7a92b40d651fe8a3c9d7b2f4e6a0c8b1d5f3a7c9e1b8d6f4a2c0e8b6d4

El volum hi continua sent, orfe. Sense contenidor que el faci servir, amb un nom que ningú no pot recordar, i ocupant disc indefinidament. Cada postgres que has esborrat en aquest mòdul en va deixar enrere un, amb les seves dades a dins, irrecuperables a la pràctica.

Volum anònim Volum amb nom
Com es crea Automàticament (VOLUME del Dockerfile o -v /desti) docker volume create o -v nom:/desti
Nom Un hash de 64 caràcters El que triïs tu
Es reutilitza en recrear el contenidor No, se'n crea un altre de buit , és el mateix
L'esborra docker rm -v No
L'esborra docker volume prune si està orfe Sí si està orfe
Recomanació Evitar-los per a dades que importin Sempre per a dades que importin

Neteja els orfes ara, comprovant abans què t'enduràs per davant:

docker volume ls -f dangling=true
docker volume prune -f
Deleted Volumes:
b41f7c9e2a8d05f31c6e8b4a2d97f0c5e3a1b8d6f4c2e0a9b7d5f3c1e8a6b4d2
e83f1c7a92b40d651fe8a3c9d7b2f4e6a0c8b1d5f3a7c9e1b8d6f4a2c0e8b6d4

Total reclaimed space: 47.83MB

Compte amb aquesta comanda: docker volume prune esborra dades de debò i no hi ha paperera. Un volum "sense fer servir" pot ser el d'una base de dades el contenidor de la qual està simplement aturat. Des de Docker 23, prune només toca els anònims per defecte; per incloure els que tenen nom cal --all, cosa que és una xarxa de seguretat que convé no desactivar a la lleugera.

  1. Bind mounts: fitxers de la teva màquina dins del contenidor

Un bind mount connecta una ruta concreta del teu host amb una ruta del contenidor. Ja el vas fer servir a la lliçó 01-06 amb el teu index.html.

mkdir -p ~/aurora-libros/proves
echo "Contingut des del host" > ~/aurora-libros/proves/nota.txt

docker run --rm -v ~/aurora-libros/proves:/dades alpine:3.20 cat /dades/nota.txt
docker run --rm -v ~/aurora-libros/proves:/dades alpine:3.20 \
  sh -c 'echo "Escrit des del contenidor" >> /dades/nota.txt'
cat ~/aurora-libros/proves/nota.txt
Contingut des del host
Contingut des del host
Escrit des del contenidor

És el mateix fitxer vist des de dos llocs: els canvis van en les dues direccions i en temps real. Aquest és el seu superpoder per a desenvolupament —edites al teu IDE i el contenidor ho veu a l'instant— i el seu perill en producció.

Regles i paranys:

Regla Detall
Ruta absoluta obligatòria -v ./web:/html no funcionava en versions antigues. Fes servir -v "$(pwd)/web":/html o ~/... (el shell expandeix la titlla)
Si la ruta del host no existeix, Docker la crea I la crea com a directori buit i propietat de root. És la causa del clàssic "vaig muntar el meu fitxer i apareix una carpeta"
El muntatge amaga el que hi hagués a la destinació Muntar un directori buit sobre /usr/share/nginx/html deixa Nginx servint un 403
Els permisos són els del host I aquí comença el problema dels UID

El xoc d'UID

Aquest és el mal de cap clàssic dels bind mounts:

docker run --rm -u node --entrypoint sh -v ~/aurora-libros/proves:/dades \
  auroralibros/aurora-api:1.2.0 -c 'id; ls -ln /dades'
uid=1000(node) gid=1000(node) groups=1000(node)
-rw-r--r--    1 1000     1000            52 Aug  4 22:11 nota.txt

En aquest cas funciona per casualitat: el teu usuari del host té UID 1000 i l'usuari node de la imatge també. Però prova amb un usuari diferent:

docker run --rm -u 1500 -v ~/aurora-libros/proves:/dades alpine:3.20 \
  sh -c 'touch /dades/prova.txt || echo "SENSE PERMÍS"'
touch: /dades/prova.txt: Permission denied
SENSE PERMÍS

El nucli compara números, no noms. Dins del contenidor no existeix cap "usuari del host": només hi ha UID i GID, i els permisos del fitxer són els que tingui a la teva màquina. Les tres solucions habituals:

Solució Com Quan
Ajustar l'UID del contenidor -u $(id -u):$(id -g) Desenvolupament, la més pràctica
Ajustar els permisos al host chmod 644 fitxer / chown Fitxers de configuració de només lectura
Fer servir un volum en comptes d'un bind mount -v aurora-dades:/ruta Dades de producció: Docker gestiona els permisos

I d'aquí surt una regla que convé memoritzar: els bind mounts són per a desenvolupament i per a fitxers de configuració; les dades de producció van en volums.

  1. -v enfront de --mount

Docker ofereix dues sintaxis per al mateix:

# Sintaxi -v: compacta, posicional, ambigua
docker run -v aurora-dades:/var/lib/postgresql/data postgres:16-alpine

# Sintaxi --mount: verbosa, explícita, amb clau=valor
docker run --mount type=volume,source=aurora-dades,target=/var/lib/postgresql/data postgres:16-alpine
-v / --volume --mount
Llegibilitat Compacta però críptica Verbosa i autoexplicativa
Si l'origen no existeix El crea (un directori buit en bind mounts) Falla amb error
Tipus de muntatge Es dedueix de la sintaxi Explícit amb type=
Opcions avançades (tmpfs, propagació, drivers) Limitades Completes
Ús a Docker Compose i Swarm Suportada La forma nativa
Recomanació Correcta per a exemples i comandes ràpides Preferida, sobretot en scripts

Aquest "si l'origen no existeix" és la diferència pràctica més important. Amb -v, un error de teclat a la ruta et deixa un directori buit i un contenidor que arrenca sense les seves dades; amb --mount, obtens un error clar i el contenidor no arrenca.

docker run --rm --mount type=bind,source=/ruta/que/no/existeix,target=/dades alpine:3.20 ls /dades
docker: Error response from daemon: invalid mount config for type "bind":
bind source path does not exist: /ruta/que/no/existeix

Taula d'equivalències per tenir-la a mà:

Objectiu Amb -v Amb --mount
Volum amb nom -v dades:/var/lib/data --mount type=volume,src=dades,dst=/var/lib/data
Volum anònim -v /var/lib/data --mount type=volume,dst=/var/lib/data
Bind mount -v /host/ruta:/cont/ruta --mount type=bind,src=/host/ruta,dst=/cont/ruta
Bind mount de només lectura -v /host/f:/cont/f:ro --mount type=bind,src=/host/f,dst=/cont/f,readonly
tmpfs --tmpfs /tmp --mount type=tmpfs,dst=/tmp

src i source són sinònims, igual que dst, destination i target.

  1. Muntatges de només lectura

Un fitxer de configuració no té per què ser escrivible pel contenidor. Marcar-lo com a només lectura és una defensa barata i eficaç:

docker run --rm -v ~/aurora-libros/proves/nota.txt:/config/nota.txt:ro alpine:3.20 \
  sh -c 'cat /config/nota.txt; echo "intento escriure" >> /config/nota.txt || echo "BLOQUEJAT"'
Contingut des del host
Escrit des del contenidor
sh: can't create /config/nota.txt: Read-only file system
BLOQUEJAT

El nucli ho impedeix, no Docker. És el mateix mecanisme que vas fer servir amb index.html i nginx.conf a la lliçó anterior. Fes-lo servir sempre que muntis:

  • Fitxers de configuració (nginx.conf, redis.conf, postgresql.conf).
  • Scripts d'inicialització (init.sql).
  • Certificats i claus públiques.
  • Contingut estàtic que l'aplicació no ha de modificar.

  1. tmpfs: dades en memòria

Un muntatge tmpfs crea un sistema de fitxers a la RAM del host. Res no toca el disc i tot desapareix en aturar el contenidor:

docker run --rm --mount type=tmpfs,dst=/sensible,tmpfs-size=16m alpine:3.20 \
  sh -c 'echo "token-temporal-abc123" > /sensible/token; df -h /sensible; cat /sensible/token'
Filesystem                Size      Used Available Use% Mounted on
tmpfs                    16.0M      4.0K     16.0M   0% /sensible
token-temporal-abc123
Opció Per a què
tmpfs-size Mida màxima. Sense límit, pot omplir la RAM del host
tmpfs-mode Permisos del directori, p. ex. 0700

Casos d'ús reals: secrets desencriptats en temps d'execució, fitxers de sessió, directoris temporals d'aplicacions que s'executen amb el sistema de fitxers arrel en només lectura, i qualsevol dada que per normativa no hagi de quedar escrita en disc. Només funciona a Linux.

  1. Pràctica: aurora-dades i la prova de destrucció

És el moment d'arreglar aurora-db d'una vegada.

El pla

Ruta al contenidor Tipus de muntatge Origen Per què
/var/lib/postgresql/data Volum aurora-dades Gestionat per Docker Són dades de producció: han de sobreviure al contenidor
/docker-entrypoint-initdb.d/init.sql Bind mount :ro ~/aurora-libros/db/init.sql És un fitxer teu, versionat a Git, que el contenidor només llegeix

La segona fila fa servir una convenció de la imatge oficial de PostgreSQL: tot el que hi hagi a /docker-entrypoint-initdb.d/ (fitxers .sql, .sql.gz o .sh) s'executa automàticament, en ordre alfabètic, la primera vegada que s'inicialitza la base de dades. És exactament la feina que fa dues lliçons que fas a mà amb docker cp.

Recrear aurora-db com cal

docker rm -f aurora-db
chmod 644 ~/aurora-libros/db/init.sql

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 \
  postgres:16-alpine

El chmod 644 no és decoratiu: el procés de PostgreSQL corre com l'usuari postgres (UID 70), i si el fitxer només fos llegible pel teu usuari, l'script no es podria executar. És el xoc d'UID de l'apartat 5, aplicat.

Comprova què ha passat a l'arrencada:

sleep 8
docker logs aurora-db 2>&1 | grep -A2 "initdb.d"
docker exec aurora-db psql -U aurora -d aurora_llibres -t -c "SELECT COUNT(*) FROM llibres;"
/usr/local/bin/docker-entrypoint.sh: running /docker-entrypoint-initdb.d/init.sql
CREATE TABLE
CREATE INDEX
INSERT 0 8
     8

Els vuit llibres s'han carregat sols. Sense docker cp, sense psql -f, sense recordar-se de res.

Verifica els muntatges:

docker inspect -f '{{range .Mounts}}{{.Type}} | {{if .Name}}{{.Name}}{{else}}{{.Source}}{{end}} → {{.Destination}} | rw={{.RW}}{{println}}{{end}}' aurora-db
volume | aurora-dades → /var/lib/postgresql/data | rw=true
bind | /home/junior/aurora-libros/db/init.sql → /docker-entrypoint-initdb.d/init.sql | rw=false

Un volum escrivible per a les dades i un bind mount de només lectura per a l'script. Exactament el pla.

La prova de destrucció

Afegeix un altre cop El Aleph:

docker exec aurora-db psql -U aurora -d aurora_llibres -c \
  "INSERT INTO llibres (titol, autor, isbn, preu) VALUES
   ('El Aleph', 'Jorge Luis Borges', '978-84-206-3311-4', 15.20);"
curl -s http://localhost:3000/llibres | jq -r '.llibres | length'
INSERT 0 1
9

I ara, destrueix el contenidor. Sense por:

docker stop --time 30 aurora-db
docker rm aurora-db
docker ps -a --filter name=aurora-db -q
docker volume ls --filter name=aurora-dades
aurora-db
aurora-db

DRIVER    VOLUME NAME
local     aurora-dades

El contenidor ja no existeix. El volum hi continua sent. Recrea'l amb exactament la mateixa comanda d'abans:

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 \
  postgres:16-alpine

sleep 6
docker exec aurora-db psql -U aurora -d aurora_llibres -c \
  "SELECT id, titol FROM llibres WHERE titol = 'El Aleph';"
curl -s http://localhost:3000/llibres | jq -r '.llibres | length'
 id |  titol
----+----------
  9 | El Aleph
(1 row)

9

El Aleph hi continua sent. Has esborrat el contenidor de la base de dades per complet i el catàleg ha sobreviscut intacte, amb els seus nou llibres. Compara-ho amb l'apartat 1 d'aquesta mateixa lliçó, on el mateix experiment va destruir fins i tot la taula.

I hi ha un detall als registres que mereix una mirada:

docker logs aurora-db 2>&1 | grep -c "initdb.d"
docker logs aurora-db 2>&1 | head -2
0
PostgreSQL Database directory appears to contain a database; Skipping initialization

L'init.sql no s'ha tornat a executar. La imatge detecta que el directori de dades ja conté una base de dades i se salta la inicialització. Això és exactament el que vols: si s'executés sempre, el CREATE TABLE IF NOT EXISTS no faria mal, però un script menys curós podria esborrar dades reals a cada arrencada. I la conseqüència pràctica: si canvies init.sql, no n'hi haurà prou amb reiniciar; hauràs d'esborrar el volum perquè es torni a inicialitzar des de zero.

# Així es comença de zero, quan de debò vols perdre les dades
docker rm -f aurora-db && docker volume rm aurora-dades

(No ho executis ara: ho necessites per a la lliçó següent.)

aurora-web, ja muntat

El contenidor de Nginx que vas crear a la lliçó anterior ja fa servir bind mounts; ara pots llegir aquella comanda amb altres ulls:

docker rm -f aurora-web
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 \
  --stop-signal SIGQUIT \
  nginx:alpine

curl -s http://localhost:8080/api/llibres | jq -r '.llibres | length'
9

Bind mounts de només lectura per a dos fitxers que viuen al teu repositori de Git. Edita index.html al teu editor, recarrega el navegador, i el canvi hi és sense reconstruir cap imatge: aquest és el flux de desenvolupament que es converteix en el protagonista de la lliçó 04-07.

  1. Còpies de seguretat i restauració

Un volum que no es copia és un volum que es perdrà. Hi ha dues estratègies i serveixen per a coses diferents.

Còpia a nivell de fitxers amb un contenidor auxiliar

El patró universal: un contenidor efímer que munta el volum i un directori del host, i executa tar.

mkdir -p ~/aurora-libros/copies

# 1) Aturar la base de dades perquè la còpia sigui COHERENT
docker stop --time 30 aurora-db

# 2) Empaquetar el volum
docker run --rm \
  --mount type=volume,src=aurora-dades,dst=/dades,readonly \
  --mount type=bind,src=$HOME/aurora-libros/copies,dst=/copia \
  alpine:3.20 \
  tar czf /copia/aurora-dades-2026-08-04.tar.gz -C /dades .

# 3) Tornar a arrencar
docker start aurora-db
ls -lh ~/aurora-libros/copies/
aurora-db
-rw-r--r-- 1 junior junior 8.4M Aug  4 22:41 aurora-dades-2026-08-04.tar.gz

Què fa cada part:

Fragment Motiu
docker stop abans de copiar PostgreSQL manté dades a la memòria intermèdia. Copiar en calent pot donar una còpia corrupta
--rm El contenidor auxiliar s'autodestrueix
readonly al volum d'origen El procés de còpia no pot fer malbé l'original
-C /dades . Empaqueta el contingut, no el directori, perquè la restauració sigui directa
alpine:3.20 8 MB amb tar a dins; no cal res més

La restauració és l'operació inversa:

docker stop aurora-db
docker run --rm \
  --mount type=volume,src=aurora-dades,dst=/dades \
  --mount type=bind,src=$HOME/aurora-libros/copies,dst=/copia,readonly \
  alpine:3.20 \
  sh -c 'rm -rf /dades/* /dades/..?* && tar xzf /copia/aurora-dades-2026-08-04.tar.gz -C /dades'
docker start aurora-db
sleep 6
docker exec aurora-db psql -U aurora -d aurora_llibres -t -c "SELECT COUNT(*) FROM llibres;"
     9

El ..?* del rm és per incloure els fitxers ocults; sense ell quedarien restes de la instal·lació anterior barrejades amb la còpia.

Còpia lògica amb pg_dump

Per a una base de dades, gairebé sempre és millor una còpia lògica: un fitxer SQL amb l'esquema i les dades.

docker exec aurora-db pg_dump -U aurora -d aurora_llibres --clean --if-exists \
  > ~/aurora-libros/copies/aurora_llibres-2026-08-04.sql
ls -lh ~/aurora-libros/copies/*.sql
head -20 ~/aurora-libros/copies/aurora_llibres-2026-08-04.sql | tail -4
-rw-r--r-- 1 junior junior 4.2K Aug  4 22:45 aurora_llibres-2026-08-04.sql
DROP TABLE IF EXISTS public.llibres;
CREATE TABLE public.llibres (
    id integer NOT NULL,

I la restauració, amb -i perquè psql llegeixi de l'entrada estàndard:

docker exec -i aurora-db psql -U aurora -d aurora_llibres \
  < ~/aurora-libros/copies/aurora_llibres-2026-08-04.sql
docker exec aurora-db psql -U aurora -d aurora_llibres -t -c "SELECT COUNT(*) FROM llibres;"
     9

Comparació de les dues estratègies:

Còpia amb tar del volum Còpia lògica amb pg_dump
Què copia Tots els bytes del directori de dades Sentències SQL que reconstrueixen les dades
Requereix aturar el servei , perquè sigui coherent No, pg_dump és transaccional
Mida Gran (8,4 MB aquí) Petita (4,2 kB)
Portable entre versions de PostgreSQL No: el format de dades és específic de la versió major
Llegible i inspeccionable No , és text
Serveix per a qualsevol volum , és genèrica No, només bases de dades
Quan fer-la servir Volums d'aplicacions sense eina pròpia Bases de dades, sempre que sigui possible

La regla: fes servir l'eina nativa del servei si en té (pg_dump, mysqldump, redis-cli BGSAVE), i el truc del contenidor amb tar per a tota la resta.

  1. Bones pràctiques

Pràctica Motiu
Un volum amb nom per cada estat que importi aurora-dades, aurora-pujades, aurora-copies. Mai anònims per a dades reals
Noms descriptius amb prefix de projecte aurora-dades diu què és; data no diu res en una màquina amb vint volums
Etiqueta els volums --label projecte=aurora-libros permet prune selectiu
Bind mounts només per a configuració i desenvolupament Depenen de rutes del host: no són portables
Tot el que el contenidor no hagi d'escriure, en :ro Configuració, scripts, certificats, contingut estàtic
Prefereix --mount en scripts Falla sorollosament si l'origen no existeix, en comptes de crear un directori buit
No escriguis mai dins de /var/lib/docker/volumes És territori del dimoni, i a Docker Desktop ni tan sols és a la teva màquina
Còpia de seguretat abans de qualsevol prune docker volume prune no té desfer
Prova la restauració, no només la còpia Una còpia que mai no s'ha restaurat no és una còpia, és una esperança

Estat actual de l'emmagatzematge d'Aurora Libros:

docker volume ls --format "table {{.Name}}\t{{.Driver}}"
docker system df --format "table {{.Type}}\t{{.TotalCount}}\t{{.Size}}\t{{.Reclaimable}}"
NAME           DRIVER
aurora-dades   local

TYPE            TOTAL     SIZE      RECLAIMABLE
Images          11        1.021GB   612.4MB (59%)
Containers      4         3.87MB    0B (0%)
Local Volumes   1         48.2MB    0B (0%)
Build Cache     52        1.847GB   1.847GB (100%)

Un sol volum, amb nom, i 0 B recuperables: ni un orfe. Aquest és l'objectiu.

El que aquesta lliçó no cobreix i on es veu: els storage drivers del dimoni (overlay2 i companyia), els drivers de volum per a NFS, SMB o emmagatzematge al núvol, i les opcions avançades de rendiment s'estudien a la lliçó 05-02. Com es declaren els volums en un fitxer compose.yaml, a la lliçó 04-02.

Errors Habituals i Consells

  • Confiar en la capa d'escriptura per a dades que importen. Mor amb el contenidor, i recrear un contenidor és una operació rutinària.
  • Fer servir volums anònims sense saber-ho. Tota imatge amb VOLUME al seu Dockerfile en crea un. Si no li poses nom, les teves dades depenen d'un hash que ningú no recorda.
  • Executar docker volume prune sense mirar. Esborra dades reals sense confirmació per segona vegada i sense paperera. Llista abans amb docker volume ls -f dangling=true.
  • Escriure a mà a /var/lib/docker/volumes/.... Permisos incorrectes, corrupció, i a Docker Desktop ni tan sols existeix aquella ruta.
  • Rutes relatives en un bind mount. Fes servir rutes absolutes o $(pwd); amb -v, una ruta mal escrita es converteix en un directori buit creat com a root.
  • Muntar un directori buit sobre un que tenia contingut. El muntatge amaga el de la imatge: Nginx retornarà 403 i PostgreSQL es reinicialitzarà.
  • Esperar que init.sql s'executi a cada arrencada. Només corre quan el directori de dades està buit. Si el canvies, esborra el volum.
  • Copiar un volum de base de dades en calent amb tar. Pot sortir corrupte. Atura el contenidor, o fes servir pg_dump.
  • Consell: escriu el docker run dels teus serveis amb dades en un fitxer de notes. És llarg i cal reproduir-lo exactament per no perdre el volum... fins al mòdul 4, on això es resol del tot.
  • Consell: anomena les còpies amb la data i automatitza'n la rotació. I restaura almenys una vegada, per saber que funciona.

Exercicis

Exercici 1: comprova les tres persistències

Crea un contenidor d'alpine:3.20 anomenat tres-muntatges amb sleep 600 i tres muntatges alhora: un volum amb nom prova-vol a /vol, un bind mount de ~/aurora-libros/proves a /bind, i un tmpfs de 8 MB a /mem. Escriu un fitxer diferent a cada ruta. Després:

  1. Reinicia el contenidor amb docker restart i comprova quins dels tres fitxers hi continuen.
  2. Esborra'l amb docker rm -f (sense -v), crea'n un de nou amb els mateixos muntatges i comprova de nou els tres.
  3. Explica els resultats i digues quin muntatge faries servir per a: el catàleg de llibres, el nginx.conf, i un token de sessió que no ha de tocar el disc.

Exercici 2: migra aurora-cache a un volum amb nom

Redis desa la seva instantània a /data, que la imatge oficial declara com a VOLUME i per tant avui és un volum anònim. Arregla-ho:

  1. Comprova amb docker inspect que l'aurora-cache actual fa servir un volum anònim.
  2. Recrea'l amb un volum amb nom aurora-cache-dades i activant la persistència amb la comanda redis-server --appendonly yes.
  3. Escriu una clau, força un desament, destrueix el contenidor i recrea'l. Comprova si la clau sobreviu.
  4. Respon: té sentit persistir una memòria cau? Argumenta a favor i en contra.

Exercici 3: simula un desastre i recupera't

Amb la plataforma funcionant i els nou llibres al seu lloc:

  1. Fes una còpia de seguretat lògica amb pg_dump i una altra de física amb tar, anotant la mida de cadascuna.
  2. Provoca el desastre: esborra el contenidor aurora-db i el volum aurora-dades.
  3. Comprova què respon l'API en aquell moment.
  4. Recupera't pel camí més ràpid i explica quin has triat i per què. Verifica que els nou llibres —inclòs El Aleph, que no és a init.sql— tornen a estar disponibles a través de curl http://localhost:8080/api/llibres.

Solucions

Solució a l'exercici 1

docker volume create prova-vol
docker run -d --name tres-muntatges \
  --mount type=volume,src=prova-vol,dst=/vol \
  --mount type=bind,src=$HOME/aurora-libros/proves,dst=/bind \
  --mount type=tmpfs,dst=/mem,tmpfs-size=8m \
  alpine:3.20 sleep 600

docker exec tres-muntatges sh -c 'echo volum > /vol/f.txt; echo bind > /bind/f.txt; echo memoria > /mem/f.txt'
docker exec tres-muntatges sh -c 'cat /vol/f.txt /bind/f.txt /mem/f.txt'
volum
bind
memoria

1. Després de docker restart:

docker restart tres-muntatges && sleep 1
docker exec tres-muntatges sh -c 'cat /vol/f.txt 2>&1; cat /bind/f.txt 2>&1; cat /mem/f.txt 2>&1'
volum
bind
cat: can't open '/mem/f.txt': No such file or directory

El tmpfs ja s'ha perdut amb el simple reinici: vivia a RAM i el muntatge es recrea buit a cada arrencada.

2. Després d'esborrar i recrear:

docker rm -f tres-muntatges
docker run -d --name tres-muntatges \
  --mount type=volume,src=prova-vol,dst=/vol \
  --mount type=bind,src=$HOME/aurora-libros/proves,dst=/bind \
  --mount type=tmpfs,dst=/mem,tmpfs-size=8m \
  alpine:3.20 sleep 600
docker exec tres-muntatges sh -c 'cat /vol/f.txt 2>&1; cat /bind/f.txt 2>&1; cat /mem/f.txt 2>&1'
docker rm -f tres-muntatges && docker volume rm prova-vol
volum
bind
cat: can't open '/mem/f.txt': No such file or directory

3. Resultats i decisions:

Muntatge Sobreviu a restart? Sobreviu a rm + recrear? Per què
Volum prova-vol És un objecte independent del contenidor, gestionat per Docker
Bind ~/aurora-libros/proves És un directori de la teva màquina; Docker només l'ensenya
tmpfs /mem No No Viu a RAM i es descarta amb el procés

I les tres decisions: el catàleg de llibres → volum amb nom (dada de producció que ha de sobreviure i de la qual Docker gestiona els permisos); el nginx.conf → bind mount de només lectura (fitxer versionat a Git que vols editar al teu IDE i que el contenidor només llegeix); el token de sessió → tmpfs (no ha de quedar escrit en disc ni un instant, i s'ha d'evaporar en aturar el contenidor).

Solució a l'exercici 2

docker inspect -f '{{range .Mounts}}{{.Type}} | nom={{.Name}} | {{.Destination}}{{end}}' aurora-cache
volume | nom=a97c2f1e8b45d306... | /data

Volum anònim confirmat: nom de 64 caràcters, creat pel VOLUME /data del Dockerfile oficial de Redis.

docker volume create aurora-cache-dades
docker rm -f aurora-cache
docker run -d --name aurora-cache --network aurora-net \
  --label projecte=aurora-libros --label component=cache \
  --mount type=volume,src=aurora-cache-dades,dst=/data \
  redis:7-alpine redis-server --appendonly yes
docker exec aurora-cache redis-cli SET llibre:destacat "Rayuela"
docker exec aurora-cache redis-cli BGREWRITEAOF
sleep 2
docker exec aurora-cache ls /data
docker rm -f aurora-cache
docker run -d --name aurora-cache --network aurora-net \
  --label projecte=aurora-libros --label component=cache \
  --mount type=volume,src=aurora-cache-dades,dst=/data \
  redis:7-alpine redis-server --appendonly yes
sleep 2
docker exec aurora-cache redis-cli GET llibre:destacat
OK
Background append only file rewriting started
appendonlydir  dump.rdb
aurora-cache
"Rayuela"

La clau sobreviu. Nota que van caldre dues coses: el volum amb nom (perquè els fitxers persisteixin) i --appendonly yes (perquè Redis hi escrigui). Un volum sense persistència activada desaria un fitxer que mai no s'actualitza.

4. Té sentit persistir una memòria cau?

A favor:

  • Arrencada en calent. Després d'un reinici, la memòria cau ja té dades i no hi ha una allau de peticions contra PostgreSQL (el fenomen conegut com a thundering herd).
  • Si Redis es fa servir també per a sessions d'usuari o cues de treball, allà ja no és una memòria cau: són dades que perdre-les té conseqüències visibles.

En contra:

  • Una memòria cau, per definició, ha de poder perdre's sense conseqüències. Si el teu sistema no funciona sense ella, no és una memòria cau: és una base de dades disfressada, i s'hauria de tractar com a tal.
  • La persistència costa: E/S de disc a cada escriptura i una arrencada més lenta.
  • Les dades persistides poden quedar obsoletes: en recuperar-se, Redis serveix un catàleg antic fins que expiri el TTL.

Per a Aurora Libros, amb un TTL de 60 segons i un catàleg petit, no compensa: és més net deixar aurora-cache sense persistència i que s'ompli sola a la primera petició. Aquest és el criteri que s'aplicarà al compose.yaml del mòdul 4.

docker rm -f aurora-cache && docker volume rm aurora-cache-dades
docker run -d --name aurora-cache --network aurora-net \
  --label projecte=aurora-libros --label component=cache redis:7-alpine

Solució a l'exercici 3

1. Les dues còpies:

mkdir -p ~/aurora-libros/copies
docker exec aurora-db pg_dump -U aurora -d aurora_llibres --clean --if-exists \
  > ~/aurora-libros/copies/desastre.sql

docker stop --time 30 aurora-db
docker run --rm \
  --mount type=volume,src=aurora-dades,dst=/dades,readonly \
  --mount type=bind,src=$HOME/aurora-libros/copies,dst=/copia \
  alpine:3.20 tar czf /copia/desastre.tar.gz -C /dades .
docker start aurora-db
ls -lh ~/aurora-libros/copies/desastre.*
-rw-r--r-- 1 junior junior 4.3K Aug  4 23:02 desastre.sql
-rw-r--r-- 1 junior junior 8.4M Aug  4 23:03 desastre.tar.gz

Dues mil vegades més petita la lògica que la física, per exactament els mateixos nou llibres.

2 i 3. El desastre:

docker rm -f aurora-db
docker volume rm aurora-dades
curl -s http://localhost:8080/api/llibres | jq -c
{"error":"No s'ha pogut obtenir el catàleg","detall":"getaddrinfo ENOTFOUND aurora-db"}

Interessant: l'error no és de base de dades, sinó de DNS. En no existir el contenidor, el nom aurora-db no es resol a aurora-net. És el mateix ENOTFOUND de la lliçó 03-04, ara amb una causa diferent: allà faltava la xarxa, aquí falta el contenidor.

4. La recuperació:

# Recrear el contenidor: el volum es crea buit i init.sql s'executa tot sol
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 \
  postgres:16-alpine
sleep 8
docker exec aurora-db psql -U aurora -d aurora_llibres -t -c "SELECT COUNT(*) FROM llibres;"
     8

Vuit, no nou. L'init.sql va restaurar el catàleg original, però El Aleph no hi és: es va inserir després. Aquí és on la còpia de seguretat deixa de ser un tràmit i passa a ser l'única cosa que et salva:

docker exec -i aurora-db psql -U aurora -d aurora_llibres < ~/aurora-libros/copies/desastre.sql
docker exec aurora-db psql -U aurora -d aurora_llibres -t -c "SELECT COUNT(*) FROM llibres;"
curl -s http://localhost:8080/api/llibres | jq -r '.llibres[] | select(.titol=="El Aleph") | .titol'
     9
El Aleph

He triat la còpia lògica, i per tres motius:

  1. És més ràpida d'aplicar: un psql de 4 kB enfront d'aturar el servei, buidar el volum i desempaquetar 8,4 MB.
  2. No exigeix aturar res: es restaura sobre la base de dades en marxa, mentre que la còpia física obliga a aturar el contenidor.
  3. És portable: si en recuperar-me hagués aprofitat per pujar a PostgreSQL 17, el tar del volum no hauria servit —el format del directori de dades és específic de cada versió major— i el .sql sí.

La còpia física continua tenint el seu paper: és l'única opció per a volums d'aplicacions que no tenen una eina de bolcat pròpia, i és una còpia bit a bit que inclou configuració i estat que pg_dump no recull.

Conclusió

El segon gran problema està resolt. Has vist amb els teus propis ulls que un contenidor destruït s'emporta les seves dades —la primera vegada ni tan sols quedava la taula llibres— i saps per què: la capa d'escriptura neix i mor amb el contenidor, i els volums anònims que creen pel seu compte imatges com postgres o redis no es reutilitzen, sinó que queden orfes amb les seves dades a dins, inabastables a la pràctica.

Coneixes els tres tipus de muntatge i quan fer servir cadascun: volums gestionats per Docker per a les dades que importen, bind mounts per a configuració i desenvolupament, i tmpfs per al que no ha de tocar el disc. Saps on viuen realment els volums —/var/lib/docker/volumes/…, territori del dimoni, invisible a Docker Desktop— i que la manera correcta de mirar-hi dins és un contenidor efímer d'Alpine, mai el teu editor. Entens el xoc d'UID dels bind mounts, perquè el nucli compara números i no noms, i tens les tres maneres de resoldre'l. I prefereixes --mount a -v en scripts per una raó molt concreta: falla sorollosament en comptes de crear un directori buit que arruïna l'arrencada en silenci.

Aurora Libros ha canviat de categoria. aurora-db desa el seu estat al volum amb nom aurora-dades i rep init.sql com a bind mount de només lectura a /docker-entrypoint-initdb.d/, així que el catàleg es carrega sol la primera vegada i mai més no es trepitja. La prova definitiva va sortir com havia de sortir: vas inserir El Aleph, vas destruir el contenidor sencer, el vas recrear amb la mateixa comanda i els nou llibres hi continuaven sent. I aurora-web serveix index.html i nginx.conf des del teu repositori en :ro, editables des del teu IDE sense reconstruir res. A més saps copiar i restaurar: amb un contenidor auxiliar i tar per a qualsevol volum, i amb pg_dump —més petita, en calent i portable entre versions— per a la base de dades.

Queden dos riscos drets, i són els últims d'aquest mòdul. El primer el vas veure a docker stats a la lliçó 03-04: cap dels teus contenidors no té límit de memòria, així que una consulta desbocada a PostgreSQL o una fuita a Node poden deixar sense RAM tota la màquina. El segon és que si un contenidor mor de matinada, ningú no l'aixeca. A la lliçó següent, Límits de Recursos i Polítiques de Reinici, posaràs setge a tots dos: límits de memòria i CPU amb les seves taules de combinacions, l'OOM killer provocat a propòsit per veure'l amb OOMKilled: true i el seu codi 137, --pids-limit com a defensa davant d'una fork bomb, i les quatre polítiques de reinici amb la diferència subtil entre always i unless-stopped. I en acabar, tindràs al davant la llista completa de comandes que cal per aixecar Aurora Libros des de zero... i entendràs per què existeix Docker Compose.

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