Docker e container: la rivoluzione dello sviluppo software
C'era un tempo in cui "funziona sul mio computer" era la risposta più temuta che uno sviluppatore poteva dare a un collega o al proprio team operativo. L'applicazione web funzionava perfettamente sulla macchina dello sviluppatore — Python 3.8, libreria X versione 2.3, libreria Y versione 1.7 — ma si rompeva misteriosamente sul server di produzione con Python 3.9, libreria X versione 2.5 e libreria Y versione 2.0. Questo problema aveva un nome: dependency hell. Docker non l'ha inventato, ma lo ha risolto in modo così elegante e definitivo da trasformare radicalmente il modo in cui il software viene sviluppato, testato e distribuito in tutto il mondo.
Il problema che Docker ha risolto: dependency hell
Il dependency hell si manifesta in molte forme. Una applicazione Node.js richiede Node 16, un'altra richiede Node 18: non possono coesistere facilmente sullo stesso sistema senza strumenti di gestione delle versioni (nvm, asdf). Un'applicazione Python richiede una versione specifica di NumPy che è incompatibile con quella richiesta da un'altra applicazione: i virtual environment aiutano ma non eliminano il problema a livello di sistema. Un'applicazione legacy richiede OpenSSL 1.0 mentre il sistema ha già installato OpenSSL 3.0 e aggiornarla richiederebbe mesi di lavoro.
La soluzione tradizionale al dependency hell erano le macchine virtuali: ogni applicazione ottiene il suo sistema operativo completo, con le sue librerie, la sua versione del runtime, il suo kernel. Ma le VM hanno un costo elevato: gigabyte di spazio su disco per ogni istanza, minuti per l'avvio, overhead di memoria significativo per far girare sistemi operativi completi, complessità nella gestione delle immagini.
I container offrono un'alternativa che cattura i vantaggi dell'isolamento delle VM eliminandone i costi. Il concetto chiave è la differenza tra virtualizzazione dell'hardware (VM) e virtualizzazione del sistema operativo (container).
Namespace e cgroups: le fondamenta Linux dei container
I container non sono magia: sono una caratteristica del kernel Linux che esiste da molto prima di Docker. Le due tecnologie fondamentali sono i namespace e i cgroups.
I namespace Linux creano viste isolate delle risorse di sistema per gruppi di processi. Esistono diversi tipi di namespace:
- PID namespace: Ogni container ha la propria numerazione dei processi, isolata da quella del sistema host. Il primo processo nel container è PID 1, anche se sul sistema host ha un PID diverso.
- Network namespace: Ogni container ha le proprie interfacce di rete, indirizzo IP, tabella di routing. Può avere una
eth0virtuale completamente separata dalle altre. - Mount namespace: Ogni container vede il proprio filesystem separato, montato sopra un layer di immagine Docker.
- UTS namespace: Ogni container può avere il proprio hostname separato.
- IPC namespace: Isolamento dei meccanismi di Inter-Process Communication.
- User namespace: Mappatura degli UID/GID tra container e host.
I cgroups (Control Groups) permettono di limitare, isolare e monitorare le risorse utilizzate da gruppi di processi. Con i cgroups si può dire a un container: "puoi usare al massimo 2 CPU core e 512MB di RAM". Il kernel si assicura che questi limiti vengano rispettati, impedendo a un container di consumare risorse degli altri.
Namespace + cgroups = isolamento senza overhead di virtualizzazione hardware. I container condividono il kernel del sistema host, il che li rende molto più leggeri delle VM: l'avvio di un container richiede millisecondi invece di minuti, e un container occupa solo le librerie della propria applicazione invece di un sistema operativo completo.
La storia: Solomon Hykes e Docker Inc. nel 2013
Sebbene la tecnologia sottostante (namespace, cgroups, LXC) esistesse già, fu Solomon Hykes a trasformarla in uno strumento pratico e accessibile. Hykes era il fondatore di dotCloud, una piattaforma PaaS (Platform as a Service) che usava internamente container LXC per isolare i deployment dei clienti.
Nel marzo 2013, alla conferenza PyCon, Hykes presentò Docker in una lightning talk di 5 minuti come un progetto interno open source di dotCloud. La risposta della community fu travolgente: in poche ore il repository GitHub aveva migliaia di stelle. Docker Inc. (il nuovo nome di dotCloud, dopo aver pivotato interamente sul progetto Docker) lanciò Docker 1.0 nel giugno 2014, con la promessa di stabilità dell'API.
Nel 2017, Docker contribuì il proprio runtime (containerd) alla Cloud Native Computing Foundation (CNCF), segnando una transizione verso un ecosistema più aperto. Kubernetes, il sistema di orchestrazione di container di Google, diventò lo standard de facto per gestire container in produzione su larga scala. Nel 2022, Docker Inc. (più volte acquisita e ristrutturata) rimane rilevante principalmente attraverso Docker Desktop e Docker Hub.
Immagini vs container: la distinzione fondamentale
Una delle confusioni più comuni tra i principianti è la differenza tra immagini e container Docker.
Un'immagine Docker è un template immutabile, a sola lettura, che contiene tutto il necessario per eseguire un'applicazione: il codice, il runtime, le librerie, le variabili d'ambiente, i file di configurazione. È analoga a un file ISO o a un template di VM. Le immagini sono composte da layer sovrapposti: ogni istruzione nel Dockerfile crea un nuovo layer, e Docker usa un meccanismo di copy-on-write per evitare duplicazioni. Se due immagini condividono gli stessi layer base (es. entrambe partono da ubuntu:22.04), quel layer viene memorizzato una sola volta su disco.
Un container è un'istanza in esecuzione di un'immagine. È come un processo: prende l'immagine (immutabile), aggiunge un layer scrivibile sopra, ed esegue il processo specificato. Si possono avere dieci container dello stesso nginx, tutti creati dalla stessa immagine ma indipendenti l'uno dall'altro. Quando un container viene eliminato, il layer scrivibile viene cancellato (per questo i dati persistenti devono essere in volumi).
Il Dockerfile: definire l'immagine come codice
Il Dockerfile è il file che descrive come costruire un'immagine Docker. È testo puro, versionabile con Git, modificabile con qualsiasi editor. Ogni riga è un'istruzione che crea un nuovo layer.
Un Dockerfile per un'applicazione web Python tipica:
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:application"]
Le best practice per scrivere Dockerfile ottimali includono:
- Usare immagini base
-slimo-alpineper ridurre la dimensione finale. - Ordinare le istruzioni dal meno al più soggetto a cambiamenti: copiare prima i file delle dipendenze (requirements.txt, package.json) e installarle, poi copiare il codice sorgente. In questo modo Docker può riusare il layer delle dipendenze dalla cache quando si cambia solo il codice.
- Usare il multi-stage build per ridurre l'immagine finale: si compila il codice in uno stage con tutti i tool di build, e si copia solo il binario prodotto in uno stage finale minimale.
- Non eseguire processi come root: aggiungere un utente non privilegiato con
USER.
Docker Hub e i registry
Docker Hub (hub.docker.com) è il registry pubblico di immagini Docker: un repository centrale dove la community pubblica e condivide immagini pronte all'uso. Migliaia di immagini ufficiali sono disponibili per le applicazioni più comuni: nginx, postgres, redis, node, python, wordpress, e centinaia di altre.
Usare un'immagine da Docker Hub è immediato:
docker pull nginx:1.25-alpinedocker run -d -p 80:80 nginx:1.25-alpine
Per ambienti aziendali o di produzione, si usano registry privati: Quay.io (Red Hat), AWS ECR (Elastic Container Registry), Google Artifact Registry, Azure Container Registry, o soluzioni self-hosted come Harbor o una semplice istanza registry di Docker.
Docker Compose: orchestrazione multi-container
Le applicazioni reali raramente sono composte da un singolo container. Un'applicazione web tipica ha almeno un container per l'applicazione, uno per il database, uno per il cache (Redis), forse uno per un worker in background. Docker Compose permette di definire e gestire questo ecosistema multi-container in un unico file YAML.
Un esempio di docker-compose.yml per WordPress:
version: "3.8"
services:
db:
image: mariadb:10.11
environment:
MYSQL_ROOT_PASSWORD: rootpassword
MYSQL_DATABASE: wordpress
MYSQL_USER: wpuser
MYSQL_PASSWORD: wppassword
volumes:
- db_data:/var/lib/mysql
restart: unless-stopped
wordpress:
image: wordpress:6.4-php8.2-apache
ports:
- "8080:80"
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_NAME: wordpress
WORDPRESS_DB_USER: wpuser
WORDPRESS_DB_PASSWORD: wppassword
volumes:
- wp_data:/var/www/html
depends_on:
- db
restart: unless-stopped
volumes:
db_data:
wp_data:
Con docker compose up -d Docker crea la rete virtuale tra i container, avvia MariaDB, aspetta che sia pronto, e poi avvia WordPress. Con docker compose down tutto viene fermato. Con docker compose down -v vengono eliminati anche i volumi (attenzione: cancella i dati).
Volumi e persistenza dei dati
I container sono effimeri per design: quando viene eliminato, tutto il suo layer scrivibile scompare. Per i dati che devono persistere tra i riavvii e le ricreazioni del container, Docker offre i volumi.
Esistono tre meccanismi di persistenza:
- Named volumes: Gestiti da Docker, memorizzati in una directory di Docker (
/var/lib/docker/volumes/). La scelta raccomandata per la maggior parte dei casi. - Bind mounts: Si mappa una directory del sistema host direttamente nel container. Utile per lo sviluppo (modifiche al codice visibili immediatamente nel container) e per configurazioni specifiche del sistema.
- tmpfs mounts: Dati temporanei in memoria RAM, non persistono tra riavvii. Utile per dati sensibili temporanei.
Networking Docker: come i container comunicano
Docker offre diversi driver di rete:
- Bridge (default): Docker crea una rete virtuale privata con bridge sulla macchina host. I container nella stessa rete bridge si vedono tramite nome del container come hostname. È la modalità usata da Docker Compose.
- Host: Il container condivide lo stack di rete dell'host senza isolamento. Massime prestazioni di rete, ma senza isolamento. Utile per applicazioni ad alta intensità di rete.
- None: Nessuna rete. Il container è completamente isolato dalla rete.
- Overlay: Per reti multi-host in Docker Swarm o Kubernetes. Permette a container su host diversi di comunicare come se fossero sulla stessa rete locale.
Sicurezza dei container: i rischi da conoscere
I container non sono intrinsecamente sicuri: condividendo il kernel con l'host, una vulnerabilità nel kernel o una misconfiguration può permettere a un processo nel container di "uscire" e compromettere l'host. Le best practice di sicurezza includono:
- Eseguire come utente non-root: Usare l'istruzione
USERnel Dockerfile. Un processo root nel container è root sull'host se il container viene compromesso. - Read-only filesystem:
--read-onlyper prevenire modifiche al filesystem del container. - Limitare le capabilities:
--cap-drop ALL --cap-add NET_BIND_SERVICEper rimuovere tutti i privilegi tranne quelli strettamente necessari. - Scanning delle immagini: Strumenti come Trivy, Snyk o Grype analizzano le immagini Docker per vulnerabilità note nelle dipendenze.
- Aggiornare le immagini base: Usare Watchtower o Renovate per tenere le immagini aggiornate con le ultime patch di sicurezza.
- Usare immagini da sorgenti fidate: Preferire le immagini Docker Official o quelle di fornitori verificati (icona verde su Docker Hub).
Podman: l'alternativa rootless
Podman è un'alternativa a Docker sviluppata da Red Hat, inclusa di default in RHEL, Fedora e CentOS Stream. La differenza fondamentale è l'architettura: Docker richiede un daemon in background con privilegi root (dockerd); Podman è daemonless e rootless.
Con Podman, ogni utente può eseguire container nel proprio namespace senza privilegi root sul sistema. Questo è un vantaggio di sicurezza significativo: un container compromesso non può scalare i privilegi al sistema host attraverso il daemon di Docker. La sintassi di Podman è quasi identica a Docker (podman run invece di docker run), e supporta anche docker-compose attraverso podman-compose.
L'ecosistema: Portainer, Watchtower, Traefik
Portainer è un'interfaccia web grafica per gestire container Docker, immagini, volumi e reti senza usare la CLI. Particolarmente utile per chi gestisce un homeserver e preferisce una UI visuale. La versione Community Edition è gratuita.
Watchtower è un container che monitora automaticamente i container in esecuzione e li aggiorna quando vengono rilasciate nuove versioni delle immagini su Docker Hub o altri registry. Particolarmente utile per homeserver dove si vuole applicare automaticamente gli aggiornamenti di sicurezza.
Traefik è un reverse proxy e load balancer cloud-native che si integra nativamente con Docker: legge automaticamente le label dei container per configurare routing, SSL e middleware, senza bisogno di ricaricare la configurazione manualmente. È diventato lo standard per chi gestisce molti servizi containerizzati.
In dieci anni, Docker ha cambiato il modo in cui il software viene scritto, distribuito e operato. Il modello container è diventato l'unità fondamentale di deployment nel cloud computing moderno, dalla piccola applicazione sul Raspberry Pi di un hobbista ai milioni di container che girano su Google Cloud, AWS e Azure ogni secondo.