La lliçó anterior et va donar aïllament fort a un preu alt: cada màquina virtual porta el seu propi nucli, triga mig minut a arrencar, ocupa gigabytes al disc i reserva memòria només per existir. Per a un entorn de proves és raonable. Per desplegar una aplicació i poder-la recrear en segons, és caríssim.
Aquesta lliçó construeix l'alternativa, i comença per la idea que cal tenir clara abans d'escriure el primer docker run, perquè adquirir-la malament condiciona tot el que ve després:
Un contenidor no és una màquina petita. És un procés aïllat.
No hi ha un sistema operatiu a dins. No hi ha un nucli. No hi ha una arrencada. Hi ha un procés normal de l'amfitrió, executant-se amb una vista restringida del sistema de fitxers, de la xarxa i de la taula de processos, i amb un límit de recursos. Tot el que veuràs —les imatges, els volums, les xarxes, Compose— és maquinària al voltant d'aquella idea.
I les tres peces del nucli que la fan possible ja les coneixes per separat: els grups de control (cgroups) són literalment els del MemoryMax=512M que vas posar a 05-05, les capacitats (capabilities) són les de 05-02, i els espais de noms de muntatge són l'evolució del chroot amb què vas reparar GRUB a 07-01.
Contingut
- Les tres primitives: espais de noms, grups de control i capacitats
- Espais de noms d'usuari i el sysctl que vas deixar comentat
- Instal·lar Docker, i per què el grup docker equival a root
- Imatges, contenidors i dipòsits d'imatges
- docker run i les opcions que importen
- Dockerfile: construir la imatge de Tramontana
- Construcció en diverses etapes i elecció d'imatge base
- Dades: volums, muntatges d'enllaç i tmpfs
- Xarxa i publicació de ports
- Docker Compose
- Seguretat de contenidors
- Alternatives, i quan no fer servir contenidors
Les tres primitives: espais de noms, grups de control i capacitats
Un contenidor no és una funció del nucli. No existeix una crida al sistema crear_contenidor(). És una convenció: un procés llançat amb certes restriccions activades alhora. Veure-ho directament, sense Docker pel mig, és el que fa que tota la resta encaixi.
Espais de noms: aïllar el que un procés veu
Un namespace aïlla una classe de recurs global del nucli, de manera que els processos que hi són a dins en vegin la seva pròpia instància. N'hi ha set:
| Espai de noms | Aïlla | Efecte visible |
|---|---|---|
mnt |
Punts de muntatge | El seu propi arbre de fitxers |
pid |
Identificadors de procés | El seu procés principal és el PID 1 |
net |
Interfícies, rutes, ports, tallafocs | La seva pròpia pila de xarxa |
uts |
Nom de màquina i domini | hostname propi |
ipc |
Memòria compartida, cues de missatges | Sense comunicació amb l'exterior |
user |
Correspondència d'UID i GID | Ser root a dins sense ser-ho a fora |
cgroup |
Vista de la jerarquia de grups de control | No veu la de l'amfitrió |
$ lsns
NS TYPE NPROCS PID USER COMMAND
4026531834 time 184 1 root /sbin/init
4026531835 cgroup 184 1 root /sbin/init
4026531836 pid 184 1 root /sbin/init
4026531837 user 184 1 root /sbin/init
4026531838 uts 181 1 root /sbin/init
4026531839 ipc 184 1 root /sbin/init
4026531840 net 184 1 root /sbin/init
4026531841 mnt 172 1 root /sbin/initTots els processos comparteixen els mateixos espais de noms, perquè no hi ha cap contenidor en marxa. Anem a crear-ne un a mà, sense Docker:
# Un proces amb el seu propi espai de noms de PID, UTS i muntatge
$ sudo unshare --pid --fork --mount-proc --uts --mount bash
root@srv-tramontana:/# hostname contenidor-manual
root@contenidor-manual:/# hostname
contenidor-manual
root@contenidor-manual:/# ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.1 9788 5124 pts/0 S 19:04 0:00 bash
root 12 0.0 0.0 11492 3684 pts/0 R+ 19:04 0:00 ps auxAquí està l'efecte: bash és el PID 1 i només veu dos processos. Des de fora, el mateix procés té un PID normal:
# En un altre terminal de l amfitrio
$ pgrep -a -f 'unshare --pid'
8412 sudo unshare --pid --fork --mount-proc --uts --mount bash
$ ps -ef | grep -c bash
14El procés és el mateix. L'única cosa que canvia és el que veu. I això explica per què un contenidor arrenca en mil·lisegons: no hi ha res per arrencar, només un clone() amb unes quantes banderes.
# Cada proces exposa els seus espais de noms com a enllacos a /proc
$ ls -l /proc/1/ns/
lrwxrwxrwx 1 root root 0 ago 18 19:06 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 root root 0 ago 18 19:06 ipc -> 'ipc:[4026531839]'
lrwxrwxrwx 1 root root 0 ago 18 19:06 mnt -> 'mnt:[4026531841]'
lrwxrwxrwx 1 root root 0 ago 18 19:06 net -> 'net:[4026531840]'
lrwxrwxrwx 1 root root 0 ago 18 19:06 pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 ago 18 19:06 user -> 'user:[4026531837]'
lrwxrwxrwx 1 root root 0 ago 18 19:06 uts -> 'uts:[4026531838]'
# Comparar dos processos: si els numeros coincideixen, comparteixen espai de noms
$ sudo readlink /proc/8412/ns/pid /proc/1/ns/pid
pid:[4026532198]
pid:[4026531836]L'espai de noms de xarxa és especialment il·lustratiu, perquè l'aïllament és total:
$ sudo unshare --net bash
root@srv-tramontana:/# ip addr
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
root@srv-tramontana:/# ss -tulpn
# (buit)
root@srv-tramontana:/# exitNi enp0s3, ni IP, ni rutes, ni un sol sòcol. I lo està DOWN. Aquell és el punt de partida de qualsevol contenidor de Docker abans que se li connecti una interfície virtual.
El chroot de 07-01, i per què no n'hi ha prou
A 07-01 vas fer servir chroot per entrar en un sistema instal·lat i reparar GRUB. chroot canvia l'arrel del sistema de fitxers per a un procés — és a dir, fa una part del que fa un espai de noms de muntatge.
Però només aquella part, i per això chroot no és un mecanisme de seguretat:
$ sudo chroot /mnt /bin/bash
bash-5.2# ls /
bin boot dev etc home ... # una altra arrel: aixo si que funciona
bash-5.2# ps aux | wc -l # PERO veu tots els processos de l amfitrio
185
bash-5.2# ip addr | grep enp0s3 # i tota la xarxa de l amfitrio
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> ...
bash-5.2# hostname # i el seu nom
srv-tramontanaUn procés amb privilegis dins d'un chroot en pot sortir amb relativa facilitat —hi ha tècniques conegudes i documentades— i, encara que no en sortís, té accés complet a la xarxa, als processos i als dispositius de l'amfitrió. L'espai de noms de muntatge resol l'escapada; els altres sis espais de noms resolen la resta.
Grups de control: limitar el que un procés consumeix
Els espais de noms controlen el que un procés veu; els grups de control controlen el que consumeix. I això ja ho has fet servir:
$ systemctl show tramontana.service -p MemoryMax -p TasksMax -p CPUQuotaPerSecUSec
MemoryMax=536870912
TasksMax=64
CPUQuotaPerSecUSec=infinity
# I per sota, la jerarquia de cgroups v2 a /sys/fs/cgroup
$ cat /sys/fs/cgroup/system.slice/tramontana.service/memory.max
536870912
$ cat /sys/fs/cgroup/system.slice/tramontana.service/memory.current
187432960
$ cat /sys/fs/cgroup/system.slice/tramontana.service/pids.max
64És exactament la mateixa tecnologia. Quan a 05-05 vas posar MemoryMax=512M a la unitat de systemd, estaves creant un grup de control amb memory.max. Docker fa el mateix amb --memory 512m. La diferència és l'embolcall, no el mecanisme.
$ systemd-cgtop --iterations=1 -m | head -6
Control Group Tasks %CPU Memory Input/s Output/s
/ 184 2.1 1.2G - -
system.slice 102 1.4 842.1M - -
system.slice/postgresql@16-main.… 18 0.8 412.4M - -
system.slice/tramontana.service 12 0.4 178.7M - -Capacitats: fragmentar els privilegis de root
De 05-02: en lloc de «root o no root», el nucli divideix els privilegis en una quarantena de capacitats independents.
$ capsh --print | head -3
Current: =ep
Bounding set: cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,...Això és el que permet que el procés principal d'un contenidor pugui ser «root» i tot i així no pugui fer gairebé res perillós: se li retiren gairebé totes les capacitats. Docker, per defecte, en deixa unes catorze de les quaranta.
I amb les tres primitives juntes, ja pots definir què és un contenidor sense cap mena de màgia:
Un contenidor és un procés llançat amb espais de noms propis, dins d'un grup de control amb límits, amb un conjunt reduït de capacitats, i amb l'arrel del sistema de fitxers apuntant a la imatge.
Espais de noms d'usuari i el sysctl que vas deixar comentat
L'espai de noms d'usuari és el més recent i el que més conseqüències de seguretat té. Permet que un UID de dins es correspongui amb un UID diferent a fora:
# Com a usuari NORMAL, sense sudo
$ unshare --user --map-root-user bash
root@srv-tramontana:~# id
uid=0(root) gid=0(root) groups=0(root)
root@srv-tramontana:~# cat /proc/self/uid_map
0 1000 1
root@srv-tramontana:~# touch /etc/prova
touch: cannot touch '/etc/prova': Permission denied
root@srv-tramontana:~# exitLlegeix-ho a poc a poc, perquè és contraintuïtiu: id diu uid=0(root), però no pot escriure a /etc. L'uid_map explica per què — l'UID 0 de dins és l'UID 1000 de fora. És root del seu propi espai de noms i de ningú més.
Això és el que fa possibles els contenidors sense privilegis (rootless): un usuari normal pot crear espais de noms, muntar-hi sistemes de fitxers a dins i ser root dins del seu contenidor, sense ser root a l'amfitrió.
I ara, el paràmetre que vas deixar comentat a 06-06:
$ grep -A3 unprivileged_userns /etc/sysctl.d/60-enfortiment.conf
# No permetre a usuaris sense privilegis crear espais de noms d usuari.
# ATENCIO: trenca els contenidors sense privilegis. Comentat perque a
# 07-05 el necessitaras; descomentar nomes en servidors sense contenidors.
#kernel.unprivileged_userns_clone = 0La tensió és real i val la pena entendre-la en tots dos sentits:
A favor de desactivar-lo (= 0) |
A favor de deixar-lo actiu |
|---|---|
| Els espais de noms d'usuari han estat la via de nombroses escalades de privilegis | Són la base dels contenidors sense privilegis i de l'entorn de proves aïllat |
| Donen a un usuari normal accés a superfície del nucli que abans era només de root | Sense ells, tot contenidor necessita un dimoni amb privilegis |
| En un servidor que no executa contenidors, no s'hi perd res | També els fan servir Flatpak, Snap, bwrap i els navegadors |
La decisió per a srv-tramontana: deixar-lo actiu, perquè executarà contenidors. I la nota que s'afegeix al fitxer, perquè el criteri és el que cal documentar:
$ sudo tee -a /etc/sysctl.d/60-enfortiment.conf >/dev/null <<'FI'
# DECISIO 2026-08-18: kernel.unprivileged_userns_clone es deixa ACTIU
# (valor per defecte 1). Motiu: es requisit dels contenidors sense
# privilegis (podman rootless) i de l aillament de Snap. Desactivar-lo
# reduiria superficie d atac, pero impediria la contencio que aporten
# els contenidors, que es una proteccio mes gran.
# Revisar si aquesta maquina deixa d executar contenidors.
FI
$ sysctl kernel.unprivileged_userns_clone
kernel.unprivileged_userns_clone = 1Un enfortiment descartat amb el seu motiu escrit és coneixement; descartat en silenci és una omissió.
Instal·lar Docker, i per què el grup docker equival a root
Docker no és als dipòsits d'Ubuntu amb la versió actual, així que s'instal·la des del dipòsit oficial aplicant el de 05-03: clau a /etc/apt/keyrings/ i signed-by.
$ sudo apt install ca-certificates curl
$ sudo install -m 0755 -d /etc/apt/keyrings
$ sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
-o /etc/apt/keyrings/docker.asc
$ sudo chmod a+r /etc/apt/keyrings/docker.asc
$ echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
| sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
$ sudo apt update
$ sudo apt install docker-ce docker-ce-cli containerd.io \
docker-buildx-plugin docker-compose-plugin
$ sudo docker version --format '{{.Server.Version}}'
27.1.2
$ sudo systemctl is-enabled docker
enabledFixa't que afegir un dipòsit de tercers és una decisió de seguretat, com es va dir a 05-03: a partir d'ara, qui controli aquell dipòsit pot instal·lar paquets al teu servidor. signed-by limita la clau a aquell dipòsit concret, que és el mínim exigible.
L'avís del grup docker
Aquella ordre apareix a tots els tutorials com una comoditat, i la seva implicació real gairebé mai no s'explica:
Pertànyer al grup
dockerés equivalent a tenir root sense contrasenya, i sense deixar el rastre que deixasudo.
No és cap exageració. La demostració cap en una línia:
# Com a membre del grup docker, SENSE sudo:
$ docker run --rm -v /:/amfitrio -it alpine \
cat /amfitrio/etc/shadow | head -2
root:$y$j9T$K2p...:20318:0:99999:7:::
daemon:*:20318:0:99999:7:::El dimoni de Docker corre com a root, i -v /:/amfitrio li demana muntar l'arrel de l'amfitrió dins del contenidor. Qualsevol del grup docker pot llegir /etc/shadow, escriure a /etc/sudoers.d/, o modificar /etc/tramontana/secrets/. I ho fa sense aparèixer a auth.log.
Les conseqüències pràctiques, que cal aplicar:
- El grup
dockeres tracta com el grupsudo, amb el mateix control de qui hi entra i per què. Asrv-tramontananomésoperador, i consta a la llista de verificació de 06-06. - Mai no s'afegeix a un compte de servei ni a un usuari d'aplicació. Si
svc-tramontanafos adocker, un compromís de l'aplicació seria root immediat. - L'alternativa correcta quan hi ha diverses persones és
sudo docker, amb una regla asudoers.dque registri qui executa què — coherent amb el que vas fer a 05-02 ambdesplegar-segur. - O fer servir
podman, que no té dimoni amb privilegis i resol el problema d'arrel. Es veu al final de la lliçó.
$ sudo tee /etc/sudoers.d/docker-tramontana >/dev/null <<'FI'
# Docker requereix privilegis efectius de root. Es canalitza per sudo perque
# quedi traca a auth.log de qui executa que.
Cmnd_Alias TRAMO_DOCKER = /usr/bin/docker, /usr/bin/docker compose
operador ALL=(root) TRAMO_DOCKER
FI
$ sudo visudo -c -f /etc/sudoers.d/docker-tramontana
/etc/sudoers.d/docker-tramontana: parsed OKImatges, contenidors i dipòsits d'imatges
Tres conceptes que es confonen i que convé separar d'una vegada:
| Concepte | Què és | Analogia |
|---|---|---|
| Imatge | Plantilla immutable de només lectura, en capes | El fitxer executable |
| Contenidor | Una instància en execució, amb una capa d'escriptura a sobre | El procés |
| Dipòsit d'imatges | Servidor on es publiquen i es descarreguen imatges | El dipòsit de paquets |
La propietat més important d'una imatge és que està formada per capes, cadascuna un conjunt de canvis sobre l'anterior, identificada per la seva suma criptogràfica:
$ sudo docker pull postgres:16-alpine
16-alpine: Pulling from library/postgres
c6a83fedfae6: Pull complete
a2e5cb2d5c74: Pull complete
...
Status: Downloaded newer image for postgres:16-alpine
$ sudo docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
postgres 16-alpine 8b4c1f2a9e33 2 weeks ago 274MB
alpine 3.20 a606584aa9aa 3 weeks ago 7.8MB
$ sudo docker history postgres:16-alpine --format 'table {{.Size}}\t{{.CreatedBy}}' | head -6
SIZE CREATED BY
0B CMD ["postgres"]
0B EXPOSE map[5432/tcp:{}]
0B ENTRYPOINT ["docker-entrypoint.sh"]
12.4MB RUN /bin/sh -c set -eux; ...
188MB RUN /bin/sh -c set -eux; apk add --no-cache postgresql16 ...
7.8MB /bin/sh -c #(nop) ADD file:... in /Les capes es comparteixen entre imatges: si deu imatges es basen en alpine:3.20, aquells 7,8 MB s'emmagatzemen una sola vegada. És la mateixa idea que els discos de suport de qcow2 a 07-04, aplicada al sistema de fitxers.
I la implicació de seguretat de les capes, que cal tenir present en escriure un Dockerfile: una capa no desapareix mai. Si en una capa hi copies un fitxer amb una contrasenya i a la següent l'esborres, el fitxer continua sent a la imatge i se'n pot extreure. Hi tornarem.
# Les etiquetes son MUTABLES: latest avui no es latest dema
$ sudo docker image inspect postgres:16-alpine --format '{{index .RepoDigests 0}}'
postgres@sha256:4f2b8c1e...
# El digest SI que es immutable, i es el que es fixa en produccio
$ sudo docker pull postgres@sha256:4f2b8c1e...docker run i les opcions que importen
$ sudo docker run --rm alpine:3.20 echo "hola des del contenidor"
hola des del contenidor
$ sudo docker run -d --name prova-nginx -p 8081:80 nginx:1.27-alpine
a3f1c88e2d4b...
$ sudo docker ps
CONTAINER ID IMAGE COMMAND STATUS PORTS NAMES
a3f1c88e2d4b nginx:1.27-alpine "/docker-entrypoint.…" Up 4 seconds 0.0.0.0:8081->80/tcp prova-nginxI ara la comprovació que tanca el primer apartat. Des de l'amfitrió:
$ pgrep -a nginx
9204 nginx: master process nginx -g daemon off;
9251 nginx: worker process
$ sudo ls -l /proc/9204/ns/ | awk '{print $9, $11}'
mnt mnt:[4026532412]
net net:[4026532475]
pid pid:[4026532413]
uts uts:[4026532410]
$ cat /sys/fs/cgroup/system.slice/docker-a3f1c88e2d4b*.scope/memory.current
8912896El procés de nginx és a la taula de processos de l'amfitrió, amb un PID normal, i pgrep el troba. Només té espais de noms propis i un grup de control. No hi ha cap màquina, cap nucli, cap arrencada. És un procés.
Les opcions de docker run que es fan servir de debò:
| Opció | Què fa | Nota |
|---|---|---|
-d |
En segon pla | |
--name |
Nom en lloc d'un identificador aleatori | Necessari per a la resolució per nom |
-p 8081:80 |
Publica el port: amfitrió:contenidor | -p 127.0.0.1:8081:80 el limita a local |
-v |
Munta un volum o un directori | Vegeu l'apartat de dades |
-e |
Variable d'entorn | No per a secrets (06-05) |
--rm |
Esborra el contenidor en acabar | Per a proves |
--restart unless-stopped |
Rearrenca després d'una fallada o un reinici | Producció |
--user 1000:1000 |
Executa com aquell UID | No com a root a dins |
--read-only |
Sistema de fitxers de només lectura | Amb --tmpfs per al que sigui escrivible |
--cap-drop ALL |
Retira totes les capacitats | I --cap-add només les necessàries |
--security-opt no-new-privileges |
Impedeix escalar per SUID | Equival a NoNewPrivileges de systemd |
--memory / --cpus |
Límits del grup de control | Els de 05-05, amb un altre nom |
--health-cmd |
Comprovació de salut | Igual que revisio_salut.sh |
Les de gestió del cicle de vida:
$ sudo docker logs -f --tail 20 prova-nginx
$ sudo docker exec -it prova-nginx sh
$ sudo docker inspect prova-nginx --format '{{.State.Status}} {{.NetworkSettings.IPAddress}}'
running 172.17.0.2
$ sudo docker stats --no-stream
$ sudo docker stop prova-nginx && sudo docker rm prova-nginx
$ sudo docker system df
$ sudo docker system prune -a --volumes # COMPTE: esborra el que no s usadocker stop envia SIGTERM, espera 10 segons i després SIGKILL — exactament la disciplina de senyals de 03-06. Si la teva aplicació necessita més temps per tancar netament, --time 30.
Dockerfile: construir la imatge de Tramontana
Un Dockerfile és la recepta d'una imatge. Cada instrucció que modifica el sistema de fitxers crea una capa.
# Dockerfile — Tramontana Reserves
# Construccio en dues etapes: la primera compila, la segona nomes executa.
# ---------- Etapa 1: compilacio ----------
FROM golang:1.23-alpine AS constructor
WORKDIR /origen
# Primer NOMES els fitxers de dependencies. Motiu: aquesta capa es
# reutilitza de la memoria cau mentre no canviin, encara que canvii el codi.
COPY go.mod go.sum ./
RUN go mod download
# I despres el codi, que canvia a cada commit.
COPY . .
# CGO_ENABLED=0 produeix un binari estatic: no necessita biblioteques
# del sistema, aixi que la imatge final pot ser minima.
RUN CGO_ENABLED=0 GOOS=linux go build \
-ldflags='-s -w -X main.version=3.2.1' \
-o /tramontana ./cmd/servidor
# ---------- Etapa 2: execucio ----------
FROM alpine:3.20
# Actualitzacions de seguretat i certificats arrel per validar TLS.
# --no-cache evita deixar l index de paquets dins de la imatge.
RUN apk add --no-cache ca-certificates tzdata curl \
&& addgroup -g 1002 tramontana \
&& adduser -u 997 -G tramontana -s /sbin/nologin -D svc-tramontana
WORKDIR /opt/tramontana
# Nomes el binari de l etapa anterior. El compilador de Go, el codi
# font i les dependencies NO arriben a la imatge final.
COPY --from=constructor /tramontana /opt/tramontana/tramontana
COPY --chown=svc-tramontana:tramontana plantilles/ /opt/tramontana/plantilles/
# Els mateixos identificadors numerics que al servidor: els permisos
# dels volums nomes son coherents si coincideixen.
USER svc-tramontana
ENV TRAMONTANA_PORT=8080 \
TRAMONTANA_LOG_NIVELL=info
EXPOSE 8080
# La comprovacio de salut, equivalent a revisio_salut.sh
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD curl -fsS "http://127.0.0.1:${TRAMONTANA_PORT}/salut" || exit 1
# ENTRYPOINT: l executable, sempre. CMD: arguments per defecte,
# substituibles des de la linia d ordres.
ENTRYPOINT ["/opt/tramontana/tramontana"]
CMD ["--config", "/etc/tramontana/app.conf"]Les instruccions i el que cal saber de cadascuna:
| Instrucció | Què fa | Compte |
|---|---|---|
FROM |
Imatge base | Fixa la versió, mai latest |
WORKDIR |
Directori de treball | Millor que RUN cd, que no persisteix |
COPY |
Copia fitxers | Preferible a ADD, que fa màgia amb URL i tar |
RUN |
Executa en construcció | Encadena amb &&: cada RUN és una capa |
ENV |
Variable d'entorn | Queda a la imatge: mai secrets |
USER |
Usuari d'execució | Sense això, el procés corre com a root |
EXPOSE |
Documenta el port | No publica res: això és -p |
HEALTHCHECK |
Comprovació periòdica | La fa servir depends_on de Compose |
ENTRYPOINT |
L'executable | |
CMD |
Arguments per defecte | Substituïble en executar |
La memòria cau de capes i com ordenar les instruccions
Docker reutilitza una capa si la instrucció i el seu context no han canviat. Tan bon punt una capa s'invalida, totes les següents es reconstrueixen. D'aquí la regla:
Ordena les instruccions de menys a més freqüentment canviants.
# MALAMENT: qualsevol canvi al codi invalida la descarrega de dependencies
COPY . .
RUN go mod download
RUN go build -o /tramontana ./cmd/servidor
# BE: les dependencies nomes es tornen a baixar si canvien go.mod o go.sum
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o /tramontana ./cmd/servidorA la primera versió, canviar una línia de codi obliga a tornar a descarregar totes les dependències: minuts a cada construcció. A la segona, segons.
I l'altra regla de capes, la d'encadenar:
# MALAMENT: tres capes, i l index d apk queda dins de la imatge
RUN apk update
RUN apk add curl
RUN rm -rf /var/cache/apk/*
# BE: una capa, i l esborrat passa ABANS que la capa es tanqui
RUN apk add --no-cache curlL'rm en un RUN posterior no redueix la mida: la capa anterior ja conté els fitxers, i les capes són immutables. És el mateix motiu pel qual un secret copiat i esborrat després continua sent extraïble de la imatge.
# .dockerignore: el que NO s envia al context de construccio
$ cat .dockerignore
.git
.gitignore
*.md
Dockerfile
compose.yaml
/dades
/proves
*.bak-*
.env
secrets/.dockerignore importa per dos motius: velocitat —el context s'envia sencer al dimoni— i seguretat, perquè sense ell un COPY . . pot ficar a la imatge el directori .git amb tot l'historial, o un .env amb credencials.
$ sudo docker build -t tramontana:3.2.1 .
[+] Building 24.1s (16/16) FINISHED
$ sudo docker image ls tramontana
REPOSITORY TAG IMAGE ID CREATED SIZE
tramontana 3.2.1 f4a1c88e2d4b 8 seconds ago 19.4MBConstrucció en diverses etapes i elecció d'imatge base
19,4 MB. La comparació amb una construcció d'una sola etapa explica d'on ve:
| Construcció | Contingut | Mida |
|---|---|---|
Una etapa sobre golang:1.23 |
Compilador, codi font, dependències, binari | ~850 MB |
Una etapa sobre golang:1.23-alpine |
Igual, amb base menor | ~380 MB |
Dues etapes sobre alpine:3.20 |
Només el binari i els certificats | 19,4 MB |
Dues etapes sobre scratch |
Només el binari | ~11 MB |
I el benefici no és només el disc. La imatge de 850 MB conté el compilador de Go, git, make i desenes de biblioteques: cadascuna és superfície d'atac i una font potencial de CVE. La de 19 MB no té res d'això. És el principi de superfície mínima de 06-06 aplicat a una imatge.
Les bases habituals:
| Base | Mida | Gestor | Quan |
|---|---|---|---|
ubuntu:24.04 |
~78 MB | apt |
Quan necessites l'ecosistema complet |
debian:12-slim |
~29 MB | apt |
Compromís raonable |
alpine:3.20 |
~7,8 MB | apk |
Binaris estàtics, eines |
gcr.io/distroless/static |
~2 MB | cap | Màxima seguretat; sense shell |
scratch |
0 B | cap | Binari estàtic pur |
Alpine, que es va esmentar a 01-03 en parlar de distribucions, té una peculiaritat que cal conèixer: fa servir musl en lloc de glibc. Un binari compilat contra glibc no funciona a Alpine, i algunes aplicacions —notablement en Python amb extensions natives— tenen problemes subtils de rendiment amb l'assignador de memòria de musl. Amb Go i CGO_ENABLED=0 no hi ha problema, perquè el binari és estàtic.
Les imatges distroless van un pas més enllà: no tenen shell, ni ls, ni gestor de paquets. Un atacant que aconsegueixi execució no té amb què operar. A canvi, depurar exigeix docker debug o una imatge paral·lela amb eines.
Dades: volums, muntatges d'enllaç i tmpfs
El sistema de fitxers d'un contenidor és efímer: en esborrar-lo, la capa d'escriptura desapareix. Les dades que han de sobreviure es munten des de fora, i hi ha tres formes:
| Mecanisme | On viu | Quan |
|---|---|---|
| Volum | Gestionat per Docker a /var/lib/docker/volumes/ |
Dades de l'aplicació: bases de dades, fitxers pujats |
| Muntatge d'enllaç | Una ruta concreta de l'amfitrió | Configuració, desenvolupament, integrar-se amb el sistema |
| tmpfs | Memòria de l'amfitrió | Temporals i secrets: no toquen disc |
# Volum: Docker decideix on, i el gestiona
$ sudo docker volume create tramontana-dades-bd
$ sudo docker volume inspect tramontana-dades-bd --format '{{.Mountpoint}}'
/var/lib/docker/volumes/tramontana-dades-bd/_data
$ sudo docker run -d --name bd \
-v tramontana-dades-bd:/var/lib/postgresql/data \
-e POSTGRES_PASSWORD_FILE=/run/secrets/bd_pass \
postgres:16-alpine
# Muntatge d enllac: una ruta concreta, en nomes lectura
$ sudo docker run -d --name app \
-v /etc/tramontana/app.conf:/etc/tramontana/app.conf:ro \
-v /opt/tramontana/shared/uploads:/opt/tramontana/shared/uploads \
tramontana:3.2.1
# tmpfs: en memoria, no persisteix, no toca disc
$ sudo docker run -d --name app-nomes-lectura \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
tramontana:3.2.1Aquell noexec,nosuid del tmpfs és el mateix criteri que vas aplicar a /tmp a 06-06.
La preferència pels volums davant dels muntatges d'enllaç per a dades té motius concrets: Docker els gestiona (creació, permisos, còpia amb docker run --volumes-from), són portables entre amfitrions, i no depenen que existeixi una ruta concreta. Els muntatges d'enllaç són millors per a configuració —perquè la vols editar des de l'amfitrió amb les teves eines— i per a desenvolupament.
I l'advertiment de permisos, que és la font número u de frustració amb volums: els UID són numèrics i no es tradueixen. Un fitxer propietat de l'UID 997 a l'amfitrió pertany a l'UID 997 dins del contenidor, es digui com es digui aquell usuari allà. Per això el Dockerfile de Tramontana crea svc-tramontana amb l'UID 997 i el grup amb el GID 1002: perquè coincideixin amb el servidor.
$ sudo docker run --rm -v /opt/tramontana/shared/uploads:/dades alpine:3.20 \
ls -ln /dades
total 4
drwxrws--- 2 997 1002 4096 Aug 18 17:12 fotosXarxa i publicació de ports
$ sudo docker network ls
NETWORK ID NAME DRIVER SCOPE
f2a1c88e2d4b bridge bridge local
a3c1f88e2d4c host host local
b4d1e88e2d4d none null local| Mode | Aïllament | Resolució per nom | Quan |
|---|---|---|---|
bridge (per defecte) |
Xarxa virtual pròpia amb NAT | No a la xarxa per defecte | Poques vegades directament |
| Xarxa d'usuari | Igual, però amb DNS intern | Sí | El correcte gairebé sempre |
host |
Cap: fa servir la de l'amfitrió | N/A | Rendiment extrem; perd aïllament |
none |
Total: només lo |
N/A | Processos sense xarxa |
La diferència entre la xarxa bridge per defecte i una xarxa d'usuari és la que decideix el disseny:
$ sudo docker network create tramontana-xarxa
$ sudo docker run -d --name bd --network tramontana-xarxa postgres:16-alpine
$ sudo docker run -d --name app --network tramontana-xarxa tramontana:3.2.1
# Resolucio per NOM DE SERVEI: no calen IP a la configuracio
$ sudo docker exec app getent hosts bd
172.19.0.2 bdAixò és el que permet que app.conf digui db_host=bd en lloc d'una IP que canvia a cada arrencada. A la xarxa bridge per defecte no funciona.
I la publicació de ports, amb un detall de seguretat important:
# MALAMENT: 0.0.0.0, accessible des de tota la xarxa
$ sudo docker run -d -p 8080:8080 tramontana:3.2.1
# BE: nomes des del mateix amfitrio, coherent amb escolta=127.0.0.1
$ sudo docker run -d -p 127.0.0.1:8080:8080 tramontana:3.2.1
$ sudo ss -tulpn | grep 8080
tcp LISTEN 127.0.0.1:8080 users:(("docker-proxy",pid=9841,fd=4))Avís seriós: Docker escriu regles a
nftablesi se saltaufw. Un-p 8080:8080insereix una regla a la cadenaDOCKERque s'avalua abans que les d'ufw, així que el port queda accessible des d'Internet encara queufw statusdigui que està bloquejat.
Es comprova, i cal comprovar-ho:
$ sudo ufw status | grep 8080
# (res: ufw es pensa que esta tancat)
$ sudo nft list chain ip nat DOCKER 2>/dev/null | head -4
$ sudo nmap -p 8080 10.0.2.15 -Pn | grep 8080
8080/tcp open http-proxy # <- OBERT malgrat ufwLes dues solucions: publicar sempre amb 127.0.0.1: al davant —que és el correcte quan hi ha un servidor intermediari invers al davant, com n'hi haurà a 08-01—, o desactivar la manipulació de regles de Docker:
$ sudo tee /etc/docker/daemon.json >/dev/null <<'FI'
{
"iptables": false,
"log-driver": "journald",
"log-opts": { "tag": "{{.Name}}" },
"live-restore": true
}
FI
$ sudo systemctl restart dockerCompte: amb "iptables": false els contenidors perden la sortida a Internet llevat que escriguis tu les regles de NAT. L'opció pràctica en la majoria dels casos és la primera.
Fixa't també en "log-driver": "journald": envia la sortida dels contenidors al journal, així que journalctl i el logrotate de 05-06 se'ls apliquen també. Per defecte, Docker escriu a fitxers JSON que creixen sense límit i són una causa habitual de discos plens.
Docker Compose
Un compose.yaml descriu el conjunt de serveis, xarxes i volums en un fitxer versionable:
# compose.yaml — Tramontana Reserves
name: tramontana
services:
bd:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: tramontana_reserves
POSTGRES_USER: tramontana
# El secret NO va aqui: es llegeix d un fitxer muntat.
POSTGRES_PASSWORD_FILE: /run/secrets/bd_pass
secrets:
- bd_pass
volumes:
- dades-bd:/var/lib/postgresql/data
networks:
- interna
# La comprovacio de salut es el que permet l arrencada ordenada
healthcheck:
test: ["CMD-SHELL", "pg_isready -U tramontana -d tramontana_reserves"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
# Els mateixos limits que la unitat de systemd de 05-05
deploy:
resources:
limits:
memory: 512M
security_opt:
- no-new-privileges:true
app:
build:
context: .
dockerfile: Dockerfile
image: tramontana:3.2.1
restart: unless-stopped
# Nomes accessible des de l amfitrio: el proxy invers de 08-01 anira al davant
ports:
- "127.0.0.1:8080:8080"
environment:
TRAMONTANA_DB_HOST: bd # resolucio per nom de servei
TRAMONTANA_DB_PORT: "5432"
TRAMONTANA_LOG_NIVELL: info
secrets:
- bd_pass
volumes:
- ./config/app.conf:/etc/tramontana/app.conf:ro
- pujades:/opt/tramontana/shared/uploads
networks:
- interna
# NO arrenca fins que la base de dades respon de debo.
# Sense 'condition', depends_on nomes espera que el contenidor existeixi.
depends_on:
bd:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8080/salut"]
interval: 30s
timeout: 3s
retries: 3
start_period: 10s
read_only: true
tmpfs:
- /tmp:rw,noexec,nosuid,size=64m
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
deploy:
resources:
limits:
memory: 512M
volumes:
dades-bd:
pujades:
networks:
interna:
driver: bridge
secrets:
bd_pass:
file: ./secrets/bd_pass # fitxer 0600, fora de git$ sudo docker compose config --quiet && echo "sintaxi correcta"
sintaxi correcta
$ sudo docker compose up -d
[+] Running 4/4
✔ Network tramontana_interna Created
✔ Volume "tramontana_dades-bd" Created
✔ Container tramontana-bd-1 Healthy
✔ Container tramontana-app-1 Started
$ sudo docker compose ps
NAME IMAGE STATUS PORTS
tramontana-app-1 tramontana:3.2.1 Up 12 seconds (healthy) 127.0.0.1:8080->8080/tcp
tramontana-bd-1 postgres:16-alpine Up 45 seconds (healthy) 5432/tcp
$ curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/cases
200depends_on amb condition: service_healthy és la peça que resol un problema real: sense ella, depends_on només espera que el contenidor existeixi, no que el servei de dins estigui llest. L'aplicació arrencaria, no trobaria la base de dades i moriria. Amb la condició, Compose espera el healthcheck.
$ sudo docker compose logs -f app
$ sudo docker compose exec bd psql -U tramontana -d tramontana_reserves -c '\dt'
$ sudo docker compose down # atura i esborra contenidors i xarxa
$ sudo docker compose down -v # ...I ELS VOLUMS: destrueix les dadesAquell -v mereix l'avís: docker compose down -v esborra els volums, és a dir, la base de dades. És un rm -rf amb un altre nom, i la disciplina de 02-04 s'hi aplica igual.
Seguretat de contenidors
L'aïllament d'un contenidor és més feble que el d'una VM, així que les mesures importen més. Les que cal aplicar sempre:
| Mesura | Per què |
|---|---|
| No executar com a root a dins | USER al Dockerfile o --user. Sense això, una fuga és root a l'amfitrió |
--read-only + tmpfs |
Un atacant no pot escriure la seva eina al sistema de fitxers |
--cap-drop=ALL i afegir només el necessari |
El procés no necessita 14 capacitats |
no-new-privileges |
Impedeix escalar per un binari SUID |
| Fixar la versió de la imatge | latest canvia sense avisar: no hi ha reproductibilitat |
| Límits de memòria i CPU | Un contenidor sense límit pot tombar l'amfitrió |
| Escanejar les imatges | Les bases acumulen CVE amb el temps |
No posar secrets a ENV ni a capes |
Queden a la imatge per sempre |
Sobre els secrets, la demostració de per què ENV és mala idea:
# MALAMENT: visible per a qualsevol que pugui inspeccionar la imatge o el contenidor
$ sudo docker inspect tramontana-app-1 --format '{{json .Config.Env}}' | tr ',' '\n'
"TRAMONTANA_DB_PASSWORD=Zx9K2pQ7vLm4RtWn"
# I tambe a /proc, com ja vas veure a 06-05
$ sudo tr '\0' '\n' < /proc/$(sudo docker inspect -f '{{.State.Pid}}' tramontana-app-1)/environÉs exactament el problema que vas resoldre a 06-05 amb LoadCredentialEncrypted. L'equivalent a Docker és la secció secrets del compose.yaml, que munta el fitxer en un tmpfs dins del contenidor:
$ sudo docker compose exec app ls -l /run/secrets/
-r--r----- 1 svc-tramontana tramontana 17 ago 18 19:41 bd_passI l'escaneig d'imatges:
$ sudo docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
aquasec/trivy:latest image --severity HIGH,CRITICAL tramontana:3.2.1
tramontana:3.2.1 (alpine 3.20.2)
Total: 0 (HIGH: 0, CRITICAL: 0)
$ sudo docker run --rm aquasec/trivy:latest image --severity HIGH,CRITICAL \
postgres:16-alpine | tail -4
Total: 2 (HIGH: 2, CRITICAL: 0)Fixa't en un detall que il·lustra l'advertiment del grup docker: aquella ordre munta /var/run/docker.sock dins d'un contenidor, cosa que li dona control complet del dimoni de Docker i per tant de l'amfitrió. Es fa amb imatges en què confies, i amb consciència del que implica.
Dues mesures més que tanquen el cercle amb mòduls anteriors:
# AppArmor: Docker aplica un perfil per defecte (06-06)
$ sudo docker inspect tramontana-app-1 --format '{{.AppArmorProfile}}'
docker-default
# seccomp: filtre de crides al sistema, com el SystemCallFilter de 05-05
$ sudo docker inspect tramontana-app-1 --format '{{.HostConfig.SecurityOpt}}'
[no-new-privileges:true]El perfil docker-default d'AppArmor i el filtre seccomp per defecte bloquegen unes quaranta crides al sistema perilloses. Desactivar-los amb --privileged o --security-opt seccomp=unconfined és el que converteix un contenidor en un procés amb accés gairebé total a l'amfitrió, i --privileged no ha d'aparèixer mai en producció.
Alternatives, i quan no fer servir contenidors
| Eina | Model | Avantatge |
|---|---|---|
| Docker | Dimoni amb privilegis | Ecosistema, documentació, eines |
| Podman | Sense dimoni, rootless | Sense procés privilegiat; compatible amb Docker |
| containerd | Motor de baix nivell | És el que fa servir Kubernetes per sota |
| LXC/LXD | Contenidors de sistema | S'assemblen a una VM lleugera: init complet a dins |
Podman mereix atenció pel que resol:
$ sudo apt install podman
$ podman run --rm alpine:3.20 echo "sense dimoni, sense sudo"
sense dimoni, sense sudo
# I sense root: gracies als espais de noms d usuari del segon apartat
$ podman unshare cat /proc/self/uid_map
0 1000 1
1 100000 65536podman no té dimoni: cada contenidor és un procés fill del que el llança. En mode rootless fa servir espais de noms d'usuari, així que no existeix el problema del grup docker. Les seves ordres són compatibles (alias docker=podman funciona gairebé sempre) i s'integra amb systemd generant unitats, cosa que encaixa bé amb tot el del Mòdul 5. Per a un servidor on diverses persones executen contenidors, és l'opció més defensable.
LXC és diferent en concepte: contenidors de sistema, amb init complet a dins, diversos processos i sessions. S'assembla més a una VM lleugera que a un procés aïllat.
Quan NO fer servir contenidors
L'honestedat que falta a la majoria de les guies:
| Situació | Per què no |
|---|---|
| Aïllament de seguretat fort entre inquilins | Comparteixen nucli. Per a això hi ha les VM de 07-04 |
| Una base de dades amb estat crític | Es pot, però l'emmagatzematge persistent afegeix complexitat sense benefici clar en un sol servidor |
| Aplicacions que necessiten accés profund al nucli | Mòduls, systemd complet, maquinari específic |
| Una aplicació en un únic servidor que ja funciona | Contenidoritzar afegeix una capa que cal aprendre i mantenir a canvi de poc |
| Càrregues amb requisits de latència extrems | La capa de xarxa afegeix microsegons |
| Sistemes que han de durar deu anys sense tocar-se | L'ecosistema canvia de pressa |
Aquell quart punt s'aplica a Tramontana ara mateix: srv-tramontana funciona, està enfortit, monitorat i documentat. Contenidoritzar-lo avui afegiria complexitat sense resoldre cap problema pendent. Els contenidors brillen quan hi ha diversos serveis, diversos entorns o diversos servidors — que és justament el terreny de les dues lliçons següents.
Errors Comuns i Consells
- Pensar en un contenidor com en una VM petita. Porta a ficar-hi
systemd,sshdicrona dins, i a tractar el contenidor com una màquina que s'administra. Un contenidor executa un procés i es reemplaça, no s'administra. - Afegir algú al grup
dockersense entendre el que implica. És root sense contrasenya i sense traça. Tracta'l com el grupsudo. - Fer servir
latest. La imatge canvia sense avisar, i una reconstrucció de la setmana que ve no produeix el mateix. Fixa la versió, i en producció el digest. - Posar secrets a
ENVo en una capa. Queden a la imatge per sempre, encara que els esborris després: les capes són immutables. Fes servirsecretso un fitxer muntat. - Copiar el codi abans de les dependències al
Dockerfile. Invalida la memòria cau a cada canvi i converteix una construcció de segons en una de minuts. rmen unRUNposterior per reduir mida. No funciona: la capa anterior ja conté els fitxers. Encadena amb&&dins del mateixRUN.- Oblidar
.dockerignore. UnCOPY . .pot ficar.gitsencer, o un.envamb credencials, a la imatge. - Executar com a root dins del contenidor. És el valor per defecte i és el primer que cal canviar amb
USER. - Publicar amb
-p 8080:8080i creure queufwprotegeix. Docker escriu regles que s'avaluen abans. Fes servir-p 127.0.0.1:8080:8080i verifica-ho des de fora ambnmap. - Deixar el controlador de registre per defecte. Els fitxers JSON creixen sense límit.
"log-driver": "journald"els integra ambjournalctlilogrotate. docker compose down -vsense pensar-hi. Esborra els volums, és a dir, les dades.--privilegedperquè «funcioni». Desactiva seccomp, AppArmor i les capacitats de cop. Mai en producció; busca la capacitat concreta que falta.- Consell de mètode. Un contenidor ha de poder morir i renéixer sense que es perdi res. Si el teu té estat dins de la seva capa d'escriptura, alguna cosa està mal muntada: tot el que importa va en un volum.
Exercicis
Exercici 1
Demostra, sense fer servir Docker, que un contenidor és un procés aïllat. Crea un «contenidor» a mà amb unshare que tingui el seu propi nom de màquina, la seva pròpia taula de processos i la seva pròpia xarxa, i comprova des de l'amfitrió que continua sent un procés normal. Explica què li falta a la teva construcció per ser un contenidor de debò.
Exercici 2
Revisa aquest Dockerfile que ha escrit el Luis i corregeix-lo, explicant cada problema:
FROM ubuntu:latest
RUN apt-get update
RUN apt-get install -y python3 python3-pip curl git
COPY . /app
WORKDIR /app
RUN pip3 install -r requirements.txt
ENV DB_PASSWORD=Zx9K2pQ7vLm4RtWn
EXPOSE 8080
CMD python3 servidor.pyExercici 3
La Marta pregunta si convé migrar Tramontana Reserves a contenidors. Redacta l'anàlisi, tenint en compte l'estat real del servidor i el que s'ha vist a les dues últimes lliçons.
Solucions
Solució 1
# --- El "contenidor" a ma ---
# Cada bandera d unshare crea un espai de noms. Juntes donen la major part
# de l aillament d un contenidor.
$ sudo unshare --pid --fork --mount-proc \
--uts --ipc --mount --net \
--root=/srv/minicontenidor \
/bin/bashAbans cal una arrel mínima, que és justament el que fa una imatge:
$ sudo mkdir -p /srv/minicontenidor
$ sudo docker export $(sudo docker create alpine:3.20) \
| sudo tar -x -C /srv/minicontenidor
$ ls /srv/minicontenidor
bin dev etc home lib media mnt opt proc root run sbin srv sys tmp usr varI ara, a dins:
$ sudo unshare --pid --fork --mount-proc --uts --ipc --mount --net \
chroot /srv/minicontenidor /bin/sh
/ # hostname minicontenidor
/ # hostname
minicontenidor
/ # ps aux
PID USER TIME COMMAND
1 root 0:00 /bin/sh
8 root 0:00 ps aux
/ # ip addr
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
/ # cat /etc/os-release | head -2
NAME="Alpine Linux"
ID=alpineNom propi, PID 1, dos processos visibles, sense xarxa, i una distribució diferent de la de l'amfitrió — tot sense arrencar cap nucli.
# --- Des de l amfitrio, la comprovacio que demostra la tesi ---
$ pgrep -a -f 'unshare --pid'
10412 sudo unshare --pid --fork --mount-proc --uts --ipc --mount --net chroot ...
$ pid=$(pgrep -f 'chroot /srv/minicontenidor' | head -1)
$ ps -o pid,ppid,user,comm -p "$pid"
PID PPID USER COMMAND
10415 10412 root sh
$ sudo cat /proc/$pid/status | grep -E '^Name|^Pid|^NSpid'
Name: sh
Pid: 10415
NSpid: 10415 1
$ sudo readlink /proc/$pid/ns/pid /proc/1/ns/pid
pid:[4026532398]
pid:[4026531836]
$ sudo readlink /proc/$pid/root
/srv/minicontenidorLa línia NSpid: 10415 1 és la demostració exacta: el mateix procés té el PID 10415 a l'amfitrió i el PID 1 dins del seu espai de noms. I uname -r dona el mateix nucli als dos costats:
Què li falta per ser un contenidor de debò:
| Falta | Per què importa | Com ho fa Docker |
|---|---|---|
| Grups de control | Sense límits, el procés pot consumir tota la memòria i la CPU de l'amfitrió | Crea un grup de control amb memory.max, cpu.max, pids.max |
| Capacitats reduïdes | Aquí el procés conserva les de root: podria carregar mòduls o muntar qualsevol cosa | Retira ~26 de les 40 capacitats |
| seccomp | Sense filtre, pot invocar qualsevol crida al sistema, incloses les vulnerables | Aplica un perfil que bloqueja ~44 crides |
| AppArmor / MAC | Sense perfil, el DAC és l'única barrera | Aplica docker-default |
pivot_root en lloc de chroot |
chroot és evadible; pivot_root desmunta l'arrel antiga |
Fa servir pivot_root |
| Xarxa utilitzable | L'espai de noms de xarxa és buit: només lo i caigut |
Crea un veth, el connecta a un pont i configura NAT |
| Sistema de fitxers en capes | Aquí l'arrel és un directori normal, i cada «contenidor» necessita la seva còpia completa | Fa servir overlayfs: capes de només lectura més una d'escriptura |
| Espai de noms d'usuari | El root de dins és el root de fora | Opcional a Docker; per defecte a podman rootless |
| Gestió del cicle de vida | No hi ha imatges, ni versions, ni forma de reproduir això | Dipòsit d'imatges, etiquetes, digests |
La conclusió que es demana: Docker no aporta cap primitiva nova. L'aïllament ja és al nucli des de fa més d'una dècada. El que aporta és l'empaquetat: un format d'imatge amb capes compartides, un dipòsit on publicar-les, una forma reproduïble de construir-les, i l'aplicació coherent de les vuit restriccions de la taula, que a mà serien un guió llarg i fàcil d'equivocar. Entendre això explica per què existeixen alternatives compatibles com podman: si les primitives són del nucli, qualsevol les pot orquestrar.
Solució 2
Onze problemes. La versió corregida, i després el perquè de cadascun:
# --- Etapa 1: dependencies ---
# 1. Versio FIXA, no latest. 2. Base slim en lloc de completa.
FROM python:3.12-slim-bookworm AS constructor
WORKDIR /app
# 3. Les dependencies ABANS del codi: la memoria cau es reutilitza mentre
# requirements.txt no canvii.
COPY requirements.txt .
# 4. Un sol RUN encadenat, sense deixar la memoria cau de pip a la capa.
RUN pip install --no-cache-dir --prefix=/instalat -r requirements.txt
# --- Etapa 2: execucio ---
FROM python:3.12-slim-bookworm
# 5. Un sol RUN, amb neteja DINS de la mateixa capa.
# Sense git ni pip: son de construccio, no d execucio.
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/* \
&& groupadd -g 1002 tramontana \
&& useradd -u 997 -g tramontana -s /usr/sbin/nologin -M svc-tramontana
WORKDIR /app
# 6. Nomes les dependencies instal·lades, sense l arbre de construccio
COPY --from=constructor /instalat /usr/local
# 7. Propietat correcta des del principi
COPY --chown=svc-tramontana:tramontana . /app
# 8. NO executar com a root
USER svc-tramontana
# 9. Sense secrets. Nomes configuracio no sensible.
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
TRAMONTANA_PORT=8080
EXPOSE 8080
# 10. Comprovacio de salut, que a mes habilita depends_on a Compose
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD curl -fsS "http://127.0.0.1:${TRAMONTANA_PORT}/salut" || exit 1
# 11. Forma exec, no shell: el proces es PID 1 i rep els senyals
ENTRYPOINT ["python3", "servidor.py"]I el .dockerignore que faltava del tot:
Els onze problemes, en ordre de gravetat:
| # | Problema | Conseqüència |
|---|---|---|
| 1 | ENV DB_PASSWORD=... |
El més greu. La contrasenya queda a la imatge per sempre, visible amb docker history o docker inspect, i a qualsevol dipòsit on es publiqui. I encara que s'esborri en una capa posterior, la capa original la conserva |
| 2 | FROM ubuntu:latest |
Sense reproductibilitat: la construcció d'avui i la d'aquí a un mes produeixen imatges diferents. I ubuntu complet són 78 MB davant dels 29 d'slim |
| 3 | Executa com a root | Sense USER, el procés és root a dins. Combinat amb una fuga de contenidor, és root a l'amfitrió |
| 4 | git a la imatge final |
Eina de construcció que queda en producció: superfície d'atac i CVE. pip també hi sobra |
| 5 | COPY . /app abans d'instal·lar dependències |
Qualsevol canvi de codi invalida la memòria cau i obliga a reinstal·lar-ho tot: minuts a cada construcció |
| 6 | Tres RUN separats |
Tres capes, i l'índex d'apt (~40 MB) queda dins de la imatge perquè no s'esborra mai a la mateixa capa |
| 7 | apt-get update sense install al mateix RUN |
La memòria cau pot reutilitzar l'update vell i l'install portar paquets desactualitzats. És el problema del cache busting |
| 8 | Sense --no-install-recommends |
Arrossega desenes de paquets que ningú no ha demanat |
| 9 | CMD python3 servidor.py en forma shell |
S'executa com a /bin/sh -c "python3 servidor.py", així que el PID 1 és el shell i no propaga SIGTERM a Python: docker stop acaba matant el procés als 10 segons amb SIGKILL, sense tancament net |
| 10 | Sense HEALTHCHECK |
Docker no sap si el servei funciona, i depends_on: condition: service_healthy no es pot fer servir |
| 11 | Sense .dockerignore |
COPY . /app hi fica .git sencer —amb tot l'historial, inclosos secrets ja esborrats— i qualsevol .env present |
El resultat mesurat:
$ sudo docker build -t tramontana-luis:dolent -f Dockerfile.original .
$ sudo docker build -t tramontana:bo .
$ sudo docker image ls | grep -E 'tramontana'
tramontana bo f4a1c88e2d4b 14 seconds ago 142MB
tramontana-luis dolent a3c1f88e2d4c 2 minutes ago 684MB
$ sudo docker history tramontana-luis:dolent | grep -i password
<missing> 2 minutes ago ENV DB_PASSWORD=Zx9K2pQ7vLm4RtWn 0B684 MB davant de 142, i la contrasenya recuperable amb una sola ordre per qualsevol que tingui accés a la imatge.
La nota que cal donar-li al Luis: la contrasenya que apareix en aquell Dockerfile és la de producció, així que —seguint la regla de 06-05— es considera compromesa des del moment en què es va escriure, i cal rotar-la amb el procediment crear-aplicar-verificar-retirar. No n'hi ha prou de corregir el fitxer.
Solució 3
Anàlisi: migrar Tramontana Reserves a contenidors Per a: Marta Vidal · De: Operacions de sistemes · 18 d'agost de 2026
Recomanació breu: encara no, però sí preparar-ho. Contenidoritzar avui
srv-tramontanaafegiria complexitat sense resoldre cap problema pendent. Proposo un camí intermedi amb benefici immediat i sense risc.
El punt de partida importa. No som davant d'una aplicació desplegada de qualsevol manera.
srv-tramontanaté desplegament atòmic amb reversió automàtica, servei de systemd enfortit amb la millor puntuació de seguretat que dona l'eina, secrets xifrats, registres centralitzats al journal amb rotació, còpies amb restauració provada, detecció de canvis i un tallafocs de llista blanca. Funciona, està documentat i està mesurat. Aquella és la referència contra la qual cal comparar.Què hi guanyaríem
- Desplegaments reproduïbles bit a bit. Avui
desplegar.shmou un enllaç simbòlic a un directori nou, i l'entorn —biblioteques del sistema, versions— és el que hi hagi a la màquina. Amb una imatge, el que es prova és exactament el que es desplega. Recorda que el release 3.3.0 va fallar al juliol i no vam saber per què fins aquesta setmana: una fallada d'aquell tipus és menys probable amb imatges, perquè l'entorn viatja amb l'aplicació.- Reversió encara més ràpida. Tornar a la imatge anterior és canviar una etiqueta.
- Paritat entre entorns. La mateixa imatge en proves i en producció, sense la deriva que apareix quan es configuren dues màquines per separat.
- Aïllament addicional de l'aplicació. Un compromís quedaria més contingut: sense shell útil, sense accés a la resta del sistema de fitxers, sense capacitats.
- Preparació per créixer. Si en algun moment calen dues o tres instàncies darrere d'un balancejador, amb contenidors és qüestió de minuts.
Què costaria
- Refer feina que ja està feta i validada. L'enfortiment de systemd, el perfil d'AppArmor, la gestió de secrets i la integració amb el journal s'haurien de reconstruir amb l'equivalent de contenidors. No és impossible, però són setmanes de feina per arribar on ja som.
- Una capa més per aprendre i mantenir. Imatges, dipòsits, xarxes virtuals, volums, i els seus modes de fallada propis — que són diferents i menys coneguts que els del sistema.
- Un risc de seguretat nou i concret. El dimoni de Docker corre com a root, i pertànyer al grup
dockerequival a tenir root sense deixar traça. A més, Docker escriu regles de tallafocs que se saltenufw: un port publicat queda obert encara que el tallafocs digui el contrari. Ho he verificat al laboratori. S'hauria de gestionar explícitament.- La base de dades és el punt difícil. PostgreSQL en contenidor és possible, però l'emmagatzematge persistent afegeix complexitat i no aporta res en un únic servidor. La meva recomanació seria deixar-la fora en qualsevol cas.
- No resol el nostre problema real. El nostre problema obert és que
srv-tramontanaés un únic punt de fallada. Els contenidors no el resolen: si la màquina cau, cauen els contenidors. Això és redundància, i és el tema del qual et portaré una proposta.
El que proposo, en tres passos
Pas 1 — Ara: contenidoritzar només l'entorn de proves. Benefici immediat, risc nul. Permet aixecar l'aplicació i una base de dades neta en segons per provar un canvi, i aprendre l'eina sense exposar producció. Ja està funcionant a
srv-tramontana-proves.Pas 2 — Després: construir la imatge a cada release, sense desplegar-la. Cada versió produeix a més una imatge versionada i escanejada. Guanyem reproductibilitat i detecció de vulnerabilitats a les dependències, sense canviar res en producció. És un pas reversible.
Pas 3 — Quan es compleixi alguna condició: migrar. Les condicions que ho justificarien:
Condició Per què canvia la decisió Calen dues o més instàncies de l'aplicació És on els contenidors comencen a rendir de debò Apareixen més serveis (una API, un processador de tasques) Gestionar cinc serveis a mà és on Compose guanya Hi ha més d'un entorn per mantenir sincronitzat La paritat deixa de ser un luxe Tornem a tenir una fallada per diferències d'entorn Seria la segona vegada; una és casualitat I una observació sobre l'alternativa. Si fem el pas, proposo avaluar podman en lloc de Docker: no té dimoni amb privilegis, funciona sense root, resol d'arrel el problema del grup
docker, i s'integra amb systemd generant unitats — és a dir, encaixa amb tot el que ja tenim muntat en lloc de substituir-ho. Les seves ordres són compatibles, així que el que hem après no es perd.Resum. Els contenidors són la resposta correcta a un problema d'escala i reproductibilitat entre entorns. Avui tenim un servidor, un entorn i una aplicació que funciona, així que el benefici no compensa el cost. Comencem per proves i per construir les imatges, que és on el benefici és immediat, i ho reavaluem quan canviï alguna de les condicions de la taula.
Conclusió
Tens la idea correcta, que és el que més val d'aquesta lliçó: un contenidor és un procés aïllat, no una màquina petita. Ho has vist sense intermediaris —un bash que és PID 1 a dins i té un PID normal a fora, amb NSpid: 10415 1 a /proc com a prova— i saps que Docker no inventa cap primitiva: els espais de noms, els grups de control i les capacitats són al nucli, i són literalment els mateixos que el MemoryMax=512M de 05-05 i el chroot de 07-01. El que Docker aporta és l'empaquetat: imatges en capes compartides, un dipòsit d'imatges, construcció reproduïble, i l'aplicació coherent de vuit restriccions que a mà serien un guió llarg i fràgil.
Saps construir una imatge que pesi 19 MB en lloc de 850, ordenant les instruccions per aprofitar la memòria cau i fent servir dues etapes perquè el compilador no arribi a producció — que és el principi de superfície mínima de 06-06 aplicat a una imatge. Saps que un secret en una capa no s'esborra mai, que -p 8080:8080 se salta ufw i cal verificar-ho des de fora amb nmap, que USER no és opcional, i que docker compose down -v destrueix les dades. I has resolt el kernel.unprivileged_userns_clone que vas deixar comentat a 06-06: es queda actiu, amb la decisió i el seu motiu escrits, perquè la contenció que aporten els contenidors val més que la superfície que tanca.
Fixa't ara en on ets. Tens srv-tramontana en producció, srv-tramontana-proves recreable amb cloud-init, i contenidors que aixequen l'aplicació i la seva base de dades amb una ordre. Tres formes de crear màquines i serveis. I les tres comparteixen un problema: la configuració de producció continua sent un manual d'operació en prosa i la memòria de l'administrador. Cada usuari, cada regla de sudoers, cada unitat de systemd, cada línia d'ufw, cada sysctl i cada perfil d'AppArmor dels mòduls 5 i 6 es va aplicar a mà. L'RTO de 8 hores que la Marta va aprovar depèn enterament que algú recordi l'ordre correcte, i la regla de 06-04 —«un servidor compromès es reinstal·la, no es neteja»— és fàcil de dir i cara de complir.
A la lliçó 07-06: Automatització amb Ansible això es converteix en codi. Veuràs la diferència entre un script imperatiu com desplegar.sh i un estat desitjat idempotent, entendràs per què Ansible no necessita agent —treballa sobre l'SSH que vas enfortir a 06-02— i escriuràs inventaris, playbooks amb manejadors i etiquetes, plantilles Jinja2 que generen app.conf amb variables per entorn, i rols que ho organitzen tot en peces reutilitzables. Faràs servir --check i --diff, que són el --dry-run del curs portat a la configuració sencera, guardaràs els secrets amb Vault al costat del teu pass, i provaràs cada canvi contra srv-tramontana-proves abans de tocar producció. I al final faràs la mesura que importa: quant triga a reconstruir-se el servidor des de zero. Aquell número —de vuit hores a menys d'una— és el que converteix una regla de seguretat en un procediment que de debò es pot complir.
Curs de Linux: De Principiant a Administrador de Sistemes
Mòdul 1: Introducció a Linux
- Què és Linux?
- Història de Linux
- Distribucions de Linux
- Instal·lant Linux
- Primer Contacte amb el Sistema
- Estructura del Sistema de Fitxers de Linux
Mòdul 2: Comandes Bàsiques de Linux
- Introducció a la Línia de Comandes
- Obtenir Ajuda i Documentació del Sistema
- Navegant pel Sistema de Fitxers
- Operacions amb Fitxers i Directoris
- Visualització i Edició de Fitxers
- Enllaços Durs i Simbòlics
- Permisos i Propietat dels Fitxers
Mòdul 3: Habilitats Avançades en la Línia de Comandes
- L'Entorn del Shell: Variables, Àlies i Historial
- Ús de Comodins i Expressions Regulars
- Cerca de Fitxers i Contingut: find, locate i grep
- Canonades i Redirecció
- Processament de Text: cut, sort, uniq, sed i awk
- Gestió de Processos
- Programació de Tasques amb Cron
- Comandes de Xarxa
Mòdul 4: Scripting en Shell
- Introducció al Scripting en Shell
- Variables i Tipus de Dades
- Entrada, Sortida i Arguments d'un Script
- Estructures de Control
- Funcions i Biblioteques
- Depuració i Gestió d'Errors
- Scripts de Producció: Bones Pràctiques
Mòdul 5: Administració del Sistema
- Gestió d'Usuaris i Grups
- sudo i Permisos Especials
- Gestió de Paquets
- Gestió de Discs
- systemd i la Gestió de Serveis
- Registres del Sistema: journald i syslog
- Monitoratge del Sistema i Optimització del Rendiment
- Còpies de Seguretat i Restauració
Mòdul 6: Xarxes i Seguretat
- Configuració de Xarxes
- SSH i Accés Remot
- Tallafocs i Seguretat Perimetral
- Sistemes de Detecció d'Intrusions
- Gestió de Secrets i Certificats TLS
- Assegurant Sistemes Linux
Mòdul 7: Temes Avançats
- El Procés d'Arrencada i la Recuperació del Sistema
- Diagnòstic Avançat: strace, perf i eBPF
- Optimització del Nucli de Linux
- Virtualització amb Linux
- Contenidors de Linux i Docker
- Automatització amb Ansible
- Alta Disponibilitat i Balanceig de Càrrega
