git clone --filter=blob:none --sparse https://github.com/calmict/book_labs.git cd book_labs git sparse-checkout set docker/ed1
Il viaggio comincia smontando la prima illusione: quella di aver acceso una piccola macchina. In questo laboratorio costruisci un container senza Docker, con il solo comando unshare, e dimostri con i tuoi occhi ciò che il capitolo annuncia — un container non è una nave a sé, è un normalissimo processo Linux a cui il kernel ha raccontato una realtà ristretta. Prima di imbarcarti sul motore di Docker, tocca con mano di cosa è fatta la stiva.
Completa i TODO nei file di partenza, poi lancia il test della soluzione:
cd docker/ed1/cap01/solution ./run.sh
Nel capitolo 1 hai staccato un processo dall'elenco degli altri con un solo flag, e quel processo si è ritrovato PID 1 di un mondo tutto suo. Ma quel flag era una porta su un'intera famiglia di isolamenti. In questo laboratorio le apri quasi tutte insieme — hostname, processi, mount, rete, e la mappatura degli utenti — e dimostri, stanza per stanza, che l'isolamento di un container non è una parete unica, ma la somma di più viste che il kernel accetta di non far vedere.
Completa i TODO nei file di partenza, poi lancia il test della soluzione:
cd docker/ed1/cap02/solution ./run.sh
I namespace del capitolo 2 decidono *cosa* un processo vede. Ma manca l'altra metà dell'isolamento, e senza di essa un container sarebbe un vicino pericoloso: *quanto* può consumare. In questo laboratorio imponi a mano un tetto di memoria a un processo e lo costringi a sfondarlo, per vedere con i tuoi occhi l'OOM killer intervenire — e leggere quell'exit code 137 che ti tornerà davanti in produzione. Tutto rootless, perché la delega di systemd (§3.7) ti dà un pezzo dell'albero dei cgroup senza bisogno di sudo.
Completa i TODO nei file di partenza, poi lancia il test della soluzione:
cd docker/ed1/cap03/solution ./run.sh
Ci resta un'ultima illusione da smontare, la più convincente: quando entri in un container e digiti ls, vedi bin, etc, usr — sembra un'altra installazione Linux. Nel capitolo 2 hai intuito il come (un MNT namespace con una radice diversa); qui monti a mano il pezzo che rende la cosa *efficiente*: il Copy-on-Write con OverlayFS. Impilerai layer in sola lettura, ci scriverai sopra, e vedrai la magia — si copia una sola pagina, solo quando la scrivi. Tutto rootless, dentro i namespace del capitolo 2.
Completa i TODO nei file di partenza, poi lancia il test della soluzione:
cd docker/ed1/cap04/solution ./run.sh
Nella Parte 1 hai azionato a mano namespace, cgroup e overlay. Ora apri il cofano dello strumento che li aziona per te — e la prima sorpresa è che «Docker» non è un programma solo, ma una catena di componenti che si passano il lavoro. In questo laboratorio segui una richiesta dal socket fino al kernel: parli al demone a mani nude, mappi la catena, e dimostri che il genitore del container è lo shim, non il demone. È la prova tecnica dietro una promessa importante — puoi aggiornare Docker senza uccidere i tuoi container.
Completa i TODO nei file di partenza, poi lancia il test della soluzione:
cd docker/ed1/cap05/solution ./run.sh
Nel capitolo 5 hai visto una catena fatta di anelli distinti. Ma perché tanta frammentazione? La risposta è una parola: standard. In questo laboratorio scendi all'ultimo anello e costruisci ed esegui un container OCI a mani nude con runc — senza Docker nel ciclo. Genererai il config.json, la ricetta esatta in cui tutta la Parte 1 si condensa, e vedrai che runc non fa altro che eseguirla alla lettera. Cambierai la ricetta e il container cambierà: perché il config.json è il container.
Completa i TODO nei file di partenza, poi lancia il test della soluzione:
cd docker/ed1/cap06/solution ./run.sh
Chiudiamo l'architettura del motore seguendo un container dalla nascita alla morte — e quasi tutte le trappole hanno la stessa radice: quel PID 1 che hai incontrato «a mani nude» nel capitolo 1. In questo laboratorio confronti due container di fronte a docker stop: uno il cui PID 1 ignora SIGTERM, e uno con un vero init al posto giusto. Misuri la differenza — dieci secondi contro un istante — e capisci, con il cronometro alla mano, perché tanti container «ci mettono sempre dieci secondi» a fermarsi.
Completa i TODO nei file di partenza, poi lancia il test della soluzione:
cd docker/ed1/cap07/solution ./run.sh
Chiusa l'architettura del motore, comincia l'artigianato: le immagini. E la prima sorpresa è che un'immagine non è un blocco monolitico ma una pila di strati più una lista di carico che li elenca — proprio come una nave non è un guscio pieno alla rinfusa, ma container impilati e un manifesto che dice cosa c'è e in che ordine. In questo laboratorio dissezioni un'immagine a mani nude: ne conti i layer, ne leggi il digest sha256 che la sigilla, e dimostri perché due immagini diverse condividono gli stessi strati senza copiarli.
Completa i TODO nei file di partenza, poi lancia il test della soluzione:
cd docker/ed1/cap08/solution ./run.sh
Nel capitolo 8 hai smontato un'immagine nei suoi strati; ora la costruisci tu, con intenzione. Il Dockerfile è il piano di carico della nave: un elenco ordinato di istruzioni che dicono da quale scafo partire, cosa imbarcare, dove metterlo e quale comando eseguire alla partenza. In questo laboratorio completi un Dockerfile con le istruzioni fondamentali — COPY, ENV, CMD — e verifichi che l'immagine costruita si comporti esattamente come l'hai dichiarata: il file al posto giusto, la variabile impostata, il comando di default che parte da solo.
Completa i TODO nei file di partenza, poi lancia il test della soluzione:
cd docker/ed1/cap09/solution ./run.sh
Hai dato un comando di default con CMD; ma chi comanda davvero alla partenza? Un container ha un solo processo al posto d'onore — il PID 1 che hai incontrato nel capitolo 7 — e due istruzioni decidono chi è e cosa esegue: ENTRYPOINT e CMD. La metafora è quella del comandante e degli ordini: ENTRYPOINT è il comandante fisso della nave, CMD sono gli ordini di default, che si possono cambiare alla partenza. In questo laboratorio li combini, vedi come gli argomenti passati a docker run sovrascrivono CMD ma non ENTRYPOINT, e verifichi che la forma esatta con cui li scrivi decide se il tuo processo è PID 1 o finisce avvolto in una shell.
Completa i TODO nei file di partenza, poi lancia il test della soluzione:
cd docker/ed1/cap10/solution ./run.sh
Sai costruire un'immagine; ora impari a costruirla veloce e a spedirla prima è la cache: Docker riusa uno strato finché l'istruzione che lo produce e tutto ciò che sta sotto non cambiano — quindi l'ordine delle istruzioni decide quanto lavoro rifai a ogni build. La seconda è il Multi-Stage: usi uno stage come magazzino dove assembli con tutti gli attrezzi, e poi imbarchi sulla nave finale — leggera — solo la merce finita. In questo laboratorio le combini: metti ciò che cambia raramente prima di ciò che cambia spesso, e spedisci solo l'artefatto.
Completa i TODO nei file di partenza, poi lancia il test della soluzione:
cd docker/ed1/cap11/solution ./run.sh
Un'immagine che funziona sulla tua macchina non è ancora un'immagine da mettere in mare. La Parte 3 si chiude portando l'idea fino in fondo: in produzione la nave deve essere leggera e ben sorvegliata — solo l'equipaggio necessario, niente chiavi di troppo. La chiave di troppo, quasi sempre, è root: per default un container gira come root, e un processo compromesso che è root dentro il container è molto più pericoloso di uno che non lo è. In questo laboratorio costruisci un'immagine di produzione che gira come utente non privilegiato, proprietario solo di ciò che gli serve — e verifichi, permesso alla mano, che non può scrivere dove non deve.
Completa i TODO nei file di partenza, poi lancia il test della soluzione:
cd docker/ed1/cap12/solution ./run.sh
Un container è una nave: parte, fa il suo viaggio, e prima o poi viene demolita. Tutto ciò che scrivi nella sua stiva — lo strato scrivibile che hai conosciuto nel capitolo 8 — va giù con lei quando la rimuovi. È la sorpresa che coglie tutti la prima volta: fai girare un database, lo popoli, rimuovi il container per aggiornarlo e i dati non ci sono più. La Parte 4 risponde a questa domanda — se il container è effimero, dove vivono i dati? — e la risposta è: a terra. In questo laboratorio vedi con mano che lo strato del container muore con lui, e che un volume, custodito dal demone a terra, sopravvive alla nave.
Completa i TODO nei file di partenza, poi lancia il test della soluzione:
cd docker/ed1/cap13/solution ./run.sh
Nel capitolo 13 hai visto che i dati vanno tenuti a terra, non nella stiva del container. Ma «a terra» ha tre indirizzi diversi, e sceglierli male costa caro. Il volume è il magazzino del porto: lo gestisce il demone, è portabile e fatto per i dati. Il bind mount è un molo condiviso con l'host: monti una cartella della tua macchina dentro il container, e ciò che scrivi si vede da entrambe le parti — comodo in sviluppo, delicato coi permessi. Il tmpfs è l'armadietto veloce di bordo: vive in memoria, non tocca il disco, e si svuota all'arrivo. In questo laboratorio li usi tutti e tre e ne verifichi il tratto che li distingue.
Completa i TODO nei file di partenza, poi lancia il test della soluzione:
cd docker/ed1/cap14/solution ./run.sh
Hai imparato a girare come utente non-root (capitolo 12) e a montare dati condivisi (capitolo 14). Metti insieme le due cose e inciampi nel classico: il container non-root prova a scrivere nel volume e si sente rispondere «permesso negato». La ragione è che sul confine di un mount i permessi non si leggono per nome ma per numero: conta l'UID: un badge numerico. Se il numero del container non possiede i file montati, non scrive — punto. In questo laboratorio riproduci il mismatch, lo risolvi facendo girare il container con l'UID giusto, e verifichi che il numero attraversa il confine tale e quale: l'UID N dentro è l'UID N sull'host.
Completa i TODO nei file di partenza, poi lancia il test della soluzione:
cd docker/ed1/cap15/solution ./run.sh
Finora ogni container era un'isola di processi e di dati; ora scopri che è anche un'isola di rete. La Parte 5 apre i labirinti del networking, e la prima verità è che «dare la rete a un container» non è magia: Docker usa gli stessi mattoni del kernel Linux. Ogni container riceve il proprio network namespace — uno stack di rete tutto suo, con le sue interfacce, il suo IP, la sua tabella di routing — e viene collegato al mondo da un cavo virtuale, la veth pair: un'estremità dentro il container (eth0), l'altra sull'host, attaccata alla centralina condivisa, il bridge docker0. In questo laboratorio lo verifichi con mano: due container, due stack, due indirizzi, ciascuno col suo cavo.
Completa i TODO nei file di partenza, poi lancia il test della soluzione:
cd docker/ed1/cap16/solution ./run.sh
Nel capitolo 16 hai visto la centralina condivisa: il bridge docker0, dove ogni container ha un numero (un IP) ma nessun nome. Va bene per capire il meccanismo, male per costruirci sopra: gli IP cambiano a ogni riavvio, e tutti i container finiscono sullo stesso centralino pubblico. La soluzione è aprire un centralino privato — una rete bridge definita da te. Lì Docker aggiunge due cose che cambiano tutto: una rubrica (un DNS integrato, così i container si chiamano per nome) e una linea isolata (i container di una rete non vedono quelli di un'altra). In questo laboratorio confronti i due mondi: sul bridge di default i nomi non funzionano, sul tuo bridge sì, e chi è fuori dalla rete resta fuori.
Completa i TODO nei file di partenza, poi lancia il test della soluzione:
cd docker/ed1/cap17/solution ./run.sh
Bridge di default, bridge custom: finora ogni container aveva il suo stack di rete, isolato e connesso. Ma non è l'unico modo. Ci sono due estremi, e sceglierli è una decisione di progetto. Da un lato il driver host: il container non ha una rete sua, è attaccato direttamente alla presa dell'host — condivide il suo stack, le sue interfacce, le sue porte. Nessun isolamento, nessun NAT, massima velocità, massima esposizione. Dall'altro il driver none: il container ha il suo namespace ma è staccato — solo loopback, nessun cavo verso il mondo. In questo laboratorio tocchi i tre driver a confronto e vedi cosa cambia: chi condivide lo stack dell'host, chi non ha rete affatto, e il bridge nel mezzo.
Completa i TODO nei file di partenza, poi lancia il test della soluzione:
cd docker/ed1/cap18/solution ./run.sh
Fin qui i container vivevano dietro il centralino: un IP privato, il NAT a fare da filtro tra loro e la rete vera. Va bene per la maggior parte dei casi, ma a volte serve altro: che il container appaia sulla rete fisica come una macchina a sé, con un suo indirizzo e un suo MAC, senza mediazioni. È il driver macvlan — il container sulla banchina, non più dietro il vetro. In questo laboratorio dai a due container un indirizzo diretto sulla rete di un'interfaccia parent e verifichi che ciascuno ha il suo MAC e che si parlano sullo stesso segmento. Poi guardi oltre l'orizzonte del singolo host: ipvlan (la variante che condivide il MAC) e overlay (la rete che attraversa più host), il ponte verso l'orchestrazione.
Completa i TODO nei file di partenza, poi lancia il test della soluzione:
cd docker/ed1/cap19/solution ./run.sh
Finora hai comandato una nave alla volta: docker run, docker network, docker volume, un pezzo per volta. Ma un'applicazione vera è una flotta — un web, un database, una cache — e coordinarla a mano, comando su comando, è fragile e irripetibile. La Parte 6 introduce lo strumento che descrive l'intera flotta in un foglio solo: Docker Compose. In un file dichiari i servizi, e Compose fa il resto — crea per te una rete d'applicazione dove i servizi si trovano per nome (come il bridge custom del capitolo 17, ma senza scriverlo), rispetta le dipendenze, e avvia o ferma tutto con un comando. In questo laboratorio progetti un'app a due servizi e verifichi che si parlano per nome e che partono nell'ordine giusto.
Completa i TODO nei file di partenza, poi lancia il test della soluzione:
cd docker/ed1/cap20/solution ./run.sh
Nel capitolo 20 hai fatto partire web dopo db con depends_on. Ma «partito» non è «pronto»: il container di un database può essere avviato mentre il database dentro sta ancora caricando, e web che si connette in quell'istante trova la porta chiusa. depends_on, da solo, aspetta che il container esista, non che il servizio sia pronto ad accettare traffico. Serve un segnale di via libera. In Docker quel segnale è l'healthcheck: il servizio dichiara come si capisce che è davvero pronto, e chi dipende da lui può aspettare quel via libera invece di indovinare con uno sleep. In questo laboratorio dai a db un healthcheck che diventa verde solo dopo un ritardo, e fai aspettare web finché db non è healthy — non solo partito.
Completa i TODO nei file di partenza, poi lancia il test della soluzione:
cd docker/ed1/cap21/solution ./run.sh
Un'applicazione non è solo servizi e reti: è anche configurazione. L'indirizzo del database, il livello di log, la chiave dell'API — e, tra questi, cose che non devono finire in chiaro da nessuna parte. Docker Compose offre tre strumenti che è facile confondere. Le variabili d'ambiente configurano il comportamento del servizio. Il file .env tiene i valori fuori dal compose e fuori dal repository. E i secrets sono la cassaforte: dati sensibili montati nel container come file a permessi ristretti, non scritti nell'ambiente dove chiunque ispezioni il container li vedrebbe. In questo laboratorio configuri un servizio con una variabile presa da .env e gli dai una password come secret — e verifichi che il segreto è nel file giusto e non trapela nell'ambiente.
Completa i TODO nei file di partenza, poi lancia il test della soluzione:
cd docker/ed1/cap22/solution ./run.sh
Apriamo la Parte 7 — sicurezza e day-2 — dalla domanda che sta sotto a tutto: chi è root, davvero? Nel Docker classico il demone gira come root sull'host, e chi può parlargli (il gruppo docker) è root a tutti gli effetti. Un container che gira come root è root sull'host per i file che monta, e un'evasione è un'evasione da root. La modalità rootless ribalta il quadro usando lo USER namespace del capitolo 2: il demone e i container girano dentro un namespace dove sei «root», ma quel root è mappato a un utente non privilegiato sull'host. Sei re nella tua stanza, un utente qualunque fuori. In questo laboratorio tocchi con mano la mappatura: dentro sei uid 0 con tutte le capability, fuori sei il tuo utente, e quel «root» non può fare nulla di privilegiato sull'host.
Completa i TODO nei file di partenza, poi lancia il test della soluzione:
cd docker/ed1/cap23/solution ./run.sh
Nel capitolo 23 hai visto chi è root; ora vedi che root non è un blocco unico. I poteri di root sono spezzati dal kernel in tante chiavi separate — le capabilities: il permesso di aprire socket raw, di legare porte basse, di montare filesystem, di cambiare proprietari. Un container non ha bisogno quasi mai di tutte. Il principio è lo stesso di una cassaforte ben fatta: dai a ciascuno solo la chiave che gli serve. E sopra le capabilities ci sono altri due strati — seccomp, che filtra le syscall, e AppArmor o SELinux, che confinano cosa un processo può toccare. Difesa in profondità. In questo laboratorio tocchi le capabilities con mano: togli tutto, e la stessa operazione fallisce; ridai la chiave giusta, e riprende — senza restituire tutte le altre.
Completa i TODO nei file di partenza, poi lancia il test della soluzione:
cd docker/ed1/cap24/solution ./run.sh
La sicurezza previene; l'osservabilità fa vedere. Quando un servizio in produzione si comporta male, la prima domanda è sempre la stessa: cosa sta facendo? Docker risponde con due strumenti. Il diario di bordo sono i log: tutto ciò che il container scrive su standard output e standard error viene catturato dal demone e reso disponibile con docker logs — anche a posteriori, anche dopo che il processo è morto. I quadranti sono le metriche: docker stats mostra in tempo reale CPU, memoria, rete di ogni container. In questo laboratorio leggi il diario di un container — sia stdout sia stderr — scopri dove Docker lo conserva (il logging driver) e leggi i suoi consumi dal vivo.
Completa i TODO nei file di partenza, poi lancia il test della soluzione:
cd docker/ed1/cap25/solution ./run.sh
Prima o poi arriva il container che non parte, non dice niente, e magari continua a ripartire da solo. I log sono vuoti — muto — e l'istinto è arrendersi. Ma un container non è mai davvero muto: anche quando non scrive una riga, lascia una scatola nera. docker inspect racconta com'è morto — l'exit code, che come hai visto nel capitolo 7 è già una diagnosi — e quante volte è ripartito prima di arrendersi, il segno del crash loop. In questo laboratorio prendi un container che crasha in silenzio, con una restart policy che lo fa ripartire, e ne ricostruisci la storia senza una sola riga di log: dall'exit code e dal contatore dei riavvii.
Completa i TODO nei file di partenza, poi lancia il test della soluzione:
cd docker/ed1/cap26/solution ./run.sh
Ogni viaggio lascia dei residui. Container fermati e mai rimossi, immagini vecchie che nessuno usa più, volumi rimasti orfani quando il loro container è sparito: col tempo la stiva si riempie e il disco si esaurisce. Il day-2 — la vita dopo il primo deploy — è fatto anche di questo: sapere cosa occupa spazio e recuperarlo, ma con giudizio. Perché su una macchina condivisa un docker system prune dato alla leggera cancella anche il lavoro degli altri. In questo laboratorio pulisci in sicurezza — solo le risorse che porti l'etichetta tua — e poi alzi lo sguardo: dove finisce Docker su un host solo, e dove comincia l'orizzonte dell'orchestrazione.
Completa i TODO nei file di partenza, poi lancia il test della soluzione:
cd docker/ed1/cap27/solution ./run.sh