Continuo qui una serie di articoli che riguardano il funzionamento di Docker.
In questo post ho condiviso la comprensione del perché Docker non possa essere pensato come un virtualizzatore hardware: non viene virtualizzata una macchina completa e i processi dei container condividono il kernel del sistema operativo host; i filesystem locali vengono montati e abbiamo anche sfiorato l’argomento del networking. Possiamo al massimo dire che è una virtualizzazione a livello di sistema operativo, che la distingue dalla virtualizzazione hardware propriamente detta.
Qui voglio svilupparlo meglio perché uno degli aspetti inizialmente meno intuitivi è proprio il networking.
Il concetto di docker è fondamentale per comprendere il networking dei container.
Supponiamo di avere sul nostro computer un servizio in ascolto sulla porta 11434. Dal sistema host possiamo raggiungerlo utilizzando TCP/IP:
http://localhost:11434
Avviamo poi un’applicazione all’interno di un container Docker e proviamo a collegarci allo stesso indirizzo. La connessione fallisce.
Nel contesto di docker, ogni container opera all’interno di un proprio namespace di rete.
Quando parliamo di docker, è fondamentale comprendere il suo networking.
Perché?
La risposta breve è che localhost all’interno di un container non identifica l’host.
I container in docker possono comunicare tra loro in modo isolato e sicuro.
La risposta più interessante richiede invece di capire come Docker costruisce la rete dei container.
Sommario
1. Un container non è semplicemente un processo isolato
Un container Docker, come abbiamo già imparato, non è una macchina virtuale: i processi eseguiti nei container utilizzano lo stesso kernel Linux dell’host.
Il kernel, tuttavia, mette a disposizione un meccanismo chiamato namespace, che permette di fornire a gruppi diversi di processi una visione differente delle risorse del sistema. Quindi un processo eseguito dal processore all’interno di un certo namespace, con un abuso di linguaggio diremo che “è eseguito all’interno del container” ma è chiaro da quello che abbiamo detto quello che si intende.
Esistono namespace non solo per i processi, ma anche per diversi tipi di risorse.
Per il networking ci interessa in particolare il network namespace.
Un processo eseguito all’interno di un container può quindi avere una propria visione di:
- interfacce di rete;
- indirizzi IP;
- tabella di routing;
- porte;
- interfaccia di loopback.
Possiamo schematizzare la situazione così:
HOST
Network namespace dell'host
|
+-- lo
+-- eth0 / wlan0
+-- ...
CONTAINER
Network namespace del container
|
+-- lo
+-- eth0
Questo spiega immediatamente un comportamento che spesso sorprende chi utilizza Docker per la prima volta.
2. localhost è locale al network namespace
Ogni container in docker ha il suo network namespace.
Utilizzando docker, possiamo gestire e orchestrare facilmente applicazioni basate su microservizi.
Normalmente associamo:
localhost
all’indirizzo:
Utilizzando docker, possiamo facilmente gestire i container e il loro networking.
127.0.0.1
ma 127.0.0.1 indica l’interfaccia di loopback del network namespace nel quale si trova il processo che effettua la connessione.
Se eseguiamo:
$ curl http://localhost:11434
sull’host, stiamo chiedendo di contattare la porta 11434 dell’host.
Se eseguiamo lo stesso comando dentro un container (es: container0):
$ docker exec -it container0 curl http://localhost:11434
stiamo invece chiedendo di contattare la porta 11434 di quel container.
Non stiamo uscendo dal container per raggiungere l’host.
Questo principio è fondamentale:
localhostnon significa “il computer fisico sul quale sto lavorando”.Significa “questo network namespace”.
Due container differenti hanno quindi due localhost differenti, e anche l’host ne possiede uno proprio.
3. Come fa allora un container a comunicare con l’esterno?
Docker deve costruire un collegamento fra il network namespace del container e quello dell’host.
Nella configurazione più comune questo viene realizzato mediante una coppia di interfacce virtuali chiamata veth pair (virtual ethernet pair).
Possiamo immaginarla come un cavo Ethernet virtuale con due estremità:
CONTAINER HOST +----------------+ +----------------------+ | | | | | eth0 <===========> veth0 | | | | | +----------------+ +----------------------+
In docker, due container possono comunicare tra loro attraverso il network namespace.
Un’estremità viene collocata nel network namespace del container e normalmente appare come eth0.
L’altra rimane nel network namespace dell’host.
Ma questo, da solo, non basta: se abbiamo molti container dobbiamo collegare fra loro tutte queste interfacce.
Entra quindi in gioco il bridge.
4. Il bridge Docker
Nella configurazione standard Docker crea sull’host un bridge virtuale, normalmente associato all’interfaccia:
docker0
Possiamo pensarlo, con una certa approssimazione, come uno switch Ethernet virtuale.
Le estremità host delle varie coppie veth vengono collegate al bridge:
HOST
+----------------+
| docker0 |
| bridge |
+---+--------+---+
| |
veth1 veth2
| |
| |
+----+--+ +--+----+
| | | |
| C1 | | C2 |
| eth0 | | eth0 |
+-------+ +-------+
Qui c’è una piccola sottigliezza terminologica importante: dentro il container l’estremità della veth pair viene normalmente rinominata eth0. Quindi eth0 del container è, di fatto, una delle due estremità della coppia veth.
Ed è questo che rende molto concreta l’analogia:
- veth pair → cavo Ethernet virtuale;
docker0bridge → switch virtuale;eth0del container → scheda di rete virtuale collegata a quel cavo.
Naturalmente tutto avviene nel kernel Linux: non vengono emulati davvero una scheda Ethernet, un cavo e uno switch fisici. Ma dal punto di vista dello stack di rete il modello è sorprendentemente simile a una piccola LAN Ethernet.
Docker assegna inoltre ai container degli indirizzi appartenenti alla subnet della rete Docker.
Per esempio potremmo avere:
docker0 172.17.0.1 container C1 172.17.0.2 container C2 172.17.0.3
Gli indirizzi effettivi dipendono naturalmente dalla configurazione.
A questo punto i container possiedono una vera connettività IP.
5. Il container ha anche una propria tabella di routing
Supponiamo che un container abbia:
eth0 = 172.17.0.2
e che Docker abbia configurato come gateway:
172.17.0.1
La sua tabella di routing sarà concettualmente simile a:
172.17.0.0/16 -> eth0 default -> 172.17.0.1
Se il container deve raggiungere un indirizzo appartenente alla propria rete Docker, il traffico passa attraverso eth0 e il bridge.
Se deve raggiungere Internet, utilizza invece il gateway.
Il traffico passa quindi attraverso l’host, che provvede a inoltrarlo.
In forma semplificata:
container | eth0 | veth | docker0 | HOST | eth0 / wlan0 | Internet
Docker configura automaticamente gran parte di questa infrastruttura.
È proprio questo uno dei motivi per cui normalmente possiamo eseguire:
docker run ...
e scoprire che il container può immediatamente accedere a Internet.
Con docker, possiamo facilmente configurare le reti per applicazioni scalabili.
6. NAT: come esce un container verso Internet
C’è però ancora un problema.
Un indirizzo come:
172.17.0.2
è un indirizzo privato della rete Docker e non è normalmente instradabile su Internet.
Quando un container apre una connessione verso Internet, l’host effettua quindi una traduzione degli indirizzi, tramite meccanismi di NAT (Network Addreess Translation).
Semplificando:
172.17.0.2
|
| richiesta
v
Docker host
|
| NAT
v
IP dell'host
|
v
Internet
Dal punto di vista del server remoto, la connessione proviene dall’host, non direttamente dall’indirizzo privato del container.
Il principio è molto simile a ciò che avviene normalmente in una rete domestica, dove molti dispositivi con indirizzi privati accedono a Internet attraverso l’indirizzo pubblico del router.
7. Ma allora a cosa serve -p 5678:5678?
Qui nasce un’altra frequente fonte di confusione.
Consideriamo:
docker run -p 5678:5678 ...
La sintassi significa:
HOST:5678 -> CONTAINER:5678
Stiamo quindi pubblicando sull’host un servizio che si trova dentro il container.
Se il container possiede, per esempio:
172.17.0.2:5678
Il networking in docker è un elemento chiave per la sua funzionalità.
Docker configura il sistema affinché una connessione diretta a:
HOST:5678
possa essere inoltrata al servizio del container.
La direzione concettuale è dunque:
ESTERNO | v HOST:5678 | v CONTAINER:5678
Il port mapping non serve, in generale, a consentire al container di uscire verso l’esterno.
Serve a rendere raggiungibile dall’esterno un servizio che gira all’interno del container.
Questa distinzione è importante.
Esempio: comunicare con due server MySQL
Un esempio applicativo è il seguente: ho un processo MySQL che gira sull’host ed è in ascolto sulla porta standard 3306. Posso avere contemporaneamente un altro processo MySQL che “gira dentro un container” — metto le virgolette per ricordare che si tratta di un abuso di linguaggio — anch’esso in ascolto sulla porta 3306, ma questa volta nel network namespace del container.
Posso pubblicare la sua porta mediante:
3307:3306
in modo da potermi collegare anche a questo secondo MySQL utilizzando, per esempio, DBeaver che gira sull’host.
Potrò quindi configurare due connessioni in DBeaver:
MySQL host localhost:3306
MySQL container localhost:3307
In entrambi i casi localhost indica esattamente la stessa cosa: l’host, perché il processo che apre la connessione, DBeaver, gira sull’host.
Ciò che cambia è la porta di destinazione:
DBeaver
|
+---- localhost:3306 ------> MySQL sull'host
|
+---- localhost:3307 ------> Docker port publishing
|
v
container:3306
|
v
MySQL
Il significato di
localhostdipende da dove gira il client, non da dove gira il server che vogliamo raggiungere.
8. Container diversi non condividono localhost
Supponiamo ora di avere due container:
container A
servizio sulla porta 8000
container B
client
Se dal container B utilizziamo:
http://localhost:8000
non raggiungiamo A.
Stiamo cercando un servizio sulla porta 8000 del container B.
La situazione è infatti:
CONTAINER A CONTAINER B
127.0.0.1 127.0.0.1
| |
v v
A B
Le due interfacce di loopback appartengono a network namespace differenti.
Per comunicare, B deve raggiungere A attraverso la rete Docker.
9. Le reti Docker definite dall’utente
Nelle applicazioni reali è generalmente preferibile creare una rete Docker dedicata invece di utilizzare direttamente il bridge predefinito.
Per esempio:
$ docker network create mynetwork
Possiamo poi collegare diversi container alla stessa rete.
Uno dei vantaggi più importanti è che Docker fornisce un meccanismo DNS interno.
Supponiamo di avere:
n8n python-service database
collegati alla stessa rete Docker.
Da n8n possiamo raggiungere il servizio Python utilizzando qualcosa come:
http://python-service:8000
e il database utilizzando:
database:3306
Non è necessario conoscere preventivamente gli indirizzi IP assegnati ai container.
Docker risolve i nomi attraverso il proprio DNS.
Questo è particolarmente importante perché gli indirizzi IP dei container non dovrebbero normalmente essere considerati permanenti.
10. Docker Compose rende questo modello molto naturale
Consideriamo una configurazione semplificata:
services:
n8n:
image: docker.n8n.io/n8nio/n8n
ports:
- "5678:5678"
python-service:
build: ./python-service
ports:
- "8000:8000"
Docker Compose crea normalmente una rete per il progetto e collega entrambi i servizi a quella rete.
Dal container n8n, quindi, possiamo raggiungere:
http://python-service:8000
Il nome del servizio diventa un nome DNS utilizzabile nella rete Docker.
Possiamo rappresentare la situazione così:
Docker network
+---------------------------+
| |
+---+---+ +---+---+
| n8n | |Python |
| | |service|
+-------+ +-------+
| |
+---------------------------+
n8n -> python-service:8000
Questo è molto più robusto che utilizzare direttamente gli indirizzi IP dei container.
11. E se il servizio gira sull’host?
Arriviamo ora a un caso particolarmente interessante.
Supponiamo di avere:
HOST
|
+-- Ollama :11434
|
+-- Docker
|
+-- n8n
Ollama non gira in un container.
Gira direttamente sull’host.
Dal sistema host possiamo verificare:
curl http://localhost:11434
Organizzare i container con docker è un approccio vincente.
e ottenere:
Ollama is running
Potremmo allora essere tentati di configurare n8n utilizzando:
http://localhost:11434
Utilizzare docker rende la gestione dei servizi molto più efficiente.
ma ora sappiamo perché non può funzionare.
Dal punto di vista di n8n:
Con docker, possiamo eseguire e gestire facilmente le applicazioni.
localhost
significa:
container n8n
non:
host Docker
Con docker, la gestione del networking diventa un processo molto più semplice.
La situazione reale è:
HOST namespace +---------------------------------------------+ | | | localhost:11434 | | | | | Ollama | | | | docker0 | | | | | | veth | | | | | +-----+--------------------------------+ | | | NETWORK NAMESPACE n8n | | | | | | | | localhost | | | | | | | | | n8n | | | +--------------------------------------+ | | | +---------------------------------------------+
Il problema non è quindi che Docker “blocchi localhost”.
È molto più semplice: stiamo indicando l’host sbagliato.
12. Raggiungere l’host da un container
Docker mette a disposizione il nome:
host.docker.internal
Con docker, possiamo avere più service che girano all’interno dei container.
per consentire ai container di riferirsi all’host.
Su Docker Desktop questo meccanismo è disponibile normalmente in modo automatico.
Su Docker Engine in ambiente Linux può invece essere necessario configurare esplicitamente il mapping verso il gateway dell’host, per esempio:
extra_hosts: - "host.docker.internal:host-gateway"
A quel punto, dal container, un servizio dell’host può essere indicato come:
http://host.docker.internal:11434
Il percorso concettuale diventa:
n8n | | eth0 v Docker network | v host | v Ollama:11434
C’è però un’ulteriore condizione importante: il servizio sull’host deve essere in ascolto su un indirizzo raggiungibile dalla rete Docker.
Un processo che ascolta esclusivamente su:
127.0.0.1:11434
accetta connessioni soltanto attraverso il loopback dell’host.
Il container può raggiungere l’host attraverso la rete Docker, ma non entra magicamente nel suo loopback.
È quindi necessario distinguere due problemi differenti:
- qual è l’indirizzo dell’host visto dal container?
- su quali interfacce il servizio dell’host è effettivamente in ascolto?
Sono due aspetti indipendenti e confonderli rende spesso difficile diagnosticare i problemi di networking Docker.
13. Il nostro esempio completo: n8n, servizio Python e Ollama
Possiamo ora mettere insieme tutti i pezzi.
Supponiamo di avere:
HOST
|
+-- Ollama :11434
|
+-- Docker
|
+-- n8n :5678
|
+-- python-service :8000
Esistono tre tipi differenti di comunicazione.
Browser → n8n
Dal browser dell’host utilizziamo:
http://localhost:5678
Il port publishing:
5678:5678
inoltra la connessione verso il container n8n.
Percorso:
Browser | HOST:5678 | v n8n:5678
n8n → python-service
Entrambi sono container appartenenti alla stessa rete Docker.
n8n può quindi utilizzare:
http://python-service:8000
Percorso:
n8n | Docker network | python-service:8000
In questo caso non è necessario passare attraverso localhost dell’host.
n8n → Ollama
Ollama gira invece sull’host.
n8n dovrà quindi utilizzare un indirizzo che rappresenti l’host dal punto di vista del container, per esempio:
http://host.docker.internal:11434
quando la configurazione Docker lo rende disponibile.
Il percorso è:
n8n | Docker network | HOST | Ollama:11434
Tre comunicazioni apparentemente simili seguono quindi tre percorsi di rete completamente differenti.
14. Un modello mentale utile
Quando una connessione Docker non funziona, prima ancora di controllare firewall, porte o configurazioni, conviene porsi tre domande:
Dove gira il client?
La comprensione del networking in docker è essenziale per sfruttarne tutto il potenziale.
Host o container?
Dove gira il server?
Host, stesso container oppure un altro container?
Che cosa significa l’indirizzo utilizzato dal punto di vista del client?
In particolare:
localhost
va sempre interpretato dal punto di vista del processo che apre la connessione.
Una piccola tabella riassume gran parte del problema:
| Client | Server | Indirizzo tipico |
|---|---|---|
| Host | Host | localhost:porta |
| Host | Container | localhost:porta_pubblicata |
| Container A | stesso container A | localhost:porta |
| Container A | Container B sulla stessa rete | nome-servizio:porta |
| Container | Host | host.docker.internal:porta* |
* A seconda della piattaforma e della configurazione Docker.
Conclusione
In docker, ogni container può essere visto come un nodo all’interno di una rete virtuale.
Il networking Docker diventa molto meno misterioso quando smettiamo di considerare un container semplicemente come “un programma che gira sul nostro computer”.
Il processo gira effettivamente sullo stesso kernel dell’host, ma Docker lo colloca in un network namespace distinto.
Il networking in docker è essenziale per la corretta comunicazione tra i componenti.
Da questa scelta discende quasi tutto il resto:
Adottare docker per il networking dei container offre grande flessibilità.
network namespace
|
+--> localhost separato
|
+--> interfaccia eth0 virtuale
|
+--> veth pair
|
+--> bridge Docker
|
+--> routing e NAT
|
+--> comunicazione con altre reti
Il bridge collega i container, il routing permette loro di raggiungere altre reti, il NAT consente l’accesso verso l’esterno, il port publishing permette all’esterno di raggiungere i servizi containerizzati e il DNS Docker permette ai container di trovarsi per nome.
Soprattutto, una volta compreso questo modello, una frase apparentemente strana come:
«Da n8n
localhost:11434non raggiunge Ollama che gira sullo stesso computer»
non è più strana.
n8n e Ollama girano sullo stesso computer fisico, ma non stanno guardando la rete dallo stesso namespace.
La tecnologia docker è diventata uno standard nel settore.
Ed è proprio questa distinzione che rende possibile l’isolamento di rete dei container Docker.
Bibliografia
- With a little help from my friend ChatGPT (OpenAI).
- With a little help from my friend Claude (Anthropic).
- Tanenbaum et al., Reti di calcolatori, Pearson 2022
- https://docs.docker.com/get-started/

Commenti recenti