Networking in Docker Compose: reti, DNS e porte
Scopri la rete bridge predefinita, il DNS tramite nome del servizio, quando usare host, come condividere reti tra progetti e perché una porta pubblicata può ignorare UFW.
Cosa crea Compose prima dell'avvio dell'applicazione
Il networking di Docker Compose segue una regola fondamentale: docker compose up crea una rete privata per il progetto, collega a essa tutti i servizi e consente ai servizi di raggiungersi usando il nome del servizio. Non è necessario scrivere una sola riga networks: per ottenere questo comportamento. Gran parte della confusione sul networking di Compose deriva dal fatto che la rete predefinita è già disponibile.
Ecco un file di esempio. Salvalo come compose.yaml in una directory chiamata shop.
services:
web:
image: nginx:1.27
ports:
- "8080:80"
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: exampleAvvia il progetto e verifica ciò che Docker ha creato:
docker compose up -d
docker network lsL'elenco ora include una rete chiamata shop_default. Compose la denomina <project>_default e, per impostazione predefinita, usa come nome del progetto quello della directory, convertito in minuscolo. Puoi sostituirlo con docker compose -p myproject up -d oppure con un name: myproject di primo livello nel file. Il suo driver è bridge, cioè uno switch virtuale all'interno dell'host. Ogni container riceve un indirizzo su una subnet privata e il traffico in uscita viene tradotto nell'indirizzo dell'host prima di uscire.
docker compose down elimina nuovamente quella rete. Per questo un container obsoleto di un progetto precedente può impedire l'eliminazione della rete: Docker restituisce error while removing network: network shop_default has active endpoints e la soluzione consiste nell'arrestare o rimuovere il container ancora collegato alla rete.
Se non hai mai usato Compose, ti conviene leggere prima struttura del file Compose e comandi del ciclo di vita, perché tutto ciò che segue presuppone che tu sappia avviare e arrestare un progetto.
La risoluzione DNS tramite nome del servizio è l'aspetto che i principianti trascurano
Su qualsiasi rete definita dall'utente, Docker esegue un server DNS integrato che ogni container vede all'indirizzo 127.0.0.11. Risolve i nomi dei servizi negli indirizzi attuali dei container. Di conseguenza, web raggiunge il database usando il nome host db, sulla porta 5432, senza alcuna configurazione.
docker compose exec web getent hosts dbIl comando stampa una riga simile a 172.18.0.2 db. Se non stampa nulla, i due servizi non sono collegati alla stessa rete.
L'errore che quasi tutti commettono almeno una volta consiste nell'usare localhost nella configurazione dell'applicazione. All'interno di un container, localhost indica quel container, non l'host e non l'altro servizio. I client PostgreSQL lo segnalano chiaramente:
could not connect to server: Connection refused
Is the server running on host "localhost" (127.0.0.1) and accepting
TCP connections on port 5432?La stringa di connessione deve essere postgresql://postgres:example@db:5432/postgres. La parte relativa all'host è il nome del servizio.
Due dettagli consentono di evitare problemi in seguito. I nomi vengono risolti in base a ciò che è in esecuzione in quel momento, quindi docker compose up -d --scale web=3 restituisce un nome con tre indirizzi. Un client che memorizza il DNS nella cache senza scadenza rimane associato a un container non più attivo. Inoltre, la rete legacy bridge, usata da un semplice docker run senza --network, non è in grado di risolvere i nomi dei container. I container collegati a questa rete possono trovarsi tra loro soltanto tramite indirizzo IP e risolvono i nomi host esterni usando una copia di /etc/resolv.conf dell'host. Per questo motivo, le indicazioni sui container link del 2016 non corrispondono a ciò che si osserva oggi.
Non serve ports: per collegare due servizi
ports: pubblica una porta del container sull'host. Serve per il traffico proveniente dall'esterno di Docker. Non ha alcun effetto sul traffico tra servizi, che funziona già sull'intero intervallo di porte della rete del progetto.
Di conseguenza, il ports: - "5432:5432" che molti aggiungono al servizio del database non serve a nulla e introduce un rischio concreto: espone Postgres sull'interfaccia pubblica del server. Rimuovilo. Se devi raggiungerlo dal laptop per una migrazione, associarlo al loopback con "127.0.0.1:5432:5432" e accedi tramite un tunnel SSH. La differenza tra un socket in ascolto, una porta pubblicata e una regola del firewall è descritta in come funzionano le porte e i servizi in ascolto su Linux.
expose: ha soltanto una funzione documentale in Compose. Non apre alcuna porta, perché tra i container connessi alla stessa rete non viene bloccato nulla.
Quando usare network_mode host e quali sono i costi
La modalità host elimina il namespace di rete del container e consente al processo di usare direttamente le interfacce dell'host.
services:
probe:
image: alpine:3.20
network_mode: host
command: sleep infinityEsistono motivi concreti per usarla. Un processo che deve vedere il traffico broadcast o multicast della rete locale, ad esempio per il rilevamento dei dispositivi di un media server o di un hub domotico, non può farlo dietro un bridge, perché il bridge non inoltra quel traffico al container. Un agente di monitoraggio che legge i contatori delle interfacce dell'host deve usare le interfacce dell'host. Inoltre, si elimina il passaggio di traduzione degli indirizzi, un vantaggio rilevante con frequenze elevate di pacchetti.
I costi sono specifici.
ports: smette di funzionare. Docker avvisa che le porte pubblicate vengono ignorate quando si usa la modalità di rete host e che il container utilizza le associazioni configurate dal processo. Due container in modalità host che richiedono la porta 8080 entrano in conflitto e il secondo termina con bind: address already in use.
La risoluzione dei nomi dei servizi non è più disponibile in nessuna delle due direzioni. Il container non appartiene alla rete del progetto, quindi non può risolvere db e gli altri servizi non possono risolvere il suo nome. Può comunque raggiungerli tramite le porte pubblicate sull'host, in genere all'indirizzo 127.0.0.1. Su Linux può anche raggiungere direttamente un container collegato a un bridge usando l'indirizzo IP del container su qualsiasi porta in ascolto, perché l'host ha accesso diretto ai container sulle proprie reti bridge e un container in modalità host condivide lo stack di rete dell'host. Questi indirizzi cambiano quando i container vengono ricreati, quindi una porta pubblicata sull'interfaccia loopback è il percorso stabile.
L'isolamento non è più disponibile. Un processo che esegue il bind di 0.0.0.0 all'interno di un container in modalità host è in ascolto su ogni interfaccia del server, compresa quella pubblica, esattamente come un pacchetto installato con apt. Esiste però un vantaggio: questo traffico segue il normale percorso di input, quindi le regole UFW si applicano, cosa che non avviene per le porte pubblicate.
La modalità host è una funzionalità del Docker Engine su Linux. Docker Desktop la supporta soltanto dalla versione 4.34 e solo dopo averla abilitata. Inoltre, i container non possono eseguire il bind degli indirizzi IP dell'host e vengono gestiti soltanto TCP e UDP. Se metà del team usa server Linux e l'altra metà Docker Desktop, lo stesso file può comportarsi in modo diverso.
Usare la modalità host quando è necessario accedere alle interfacce dell'host. Non usarla per risolvere un problema di connessione, perché di solito sostituisce un problema con uno più difficile.
Collegare due progetti Compose con una rete esterna
Una rete creata da un progetto non è visibile agli altri progetti. Per questo un reverse proxy in proxy/compose.yaml non riesce a raggiungere un'applicazione in app/compose.yaml, anche se si trovano sullo stesso server. La soluzione consiste nell'usare una rete che non appartiene a nessuno dei due progetti.
Crearla una sola volta, manualmente:
docker network create edgeDichiararla quindi come esterna in ciascun progetto. Il lato del proxy:
services:
proxy:
image: traefik:v3.1
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- edge
networks:
edge:
name: edge
external: trueIl lato dell'applicazione:
services:
app:
image: nginx:1.27
networks:
- edge
- internal
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: example
networks:
- internal
networks:
edge:
name: edge
external: true
internal:external: true indica a Compose di collegarsi a una rete esistente invece di crearne una e di lasciarla disponibile in docker compose down. La chiave separata name: è più importante di quanto sembri: senza di essa Compose cerca una rete con il nome esatto edge; con essa è possibile usare un nome nel file e un altro sull'host.
Se la rete non esiste, Compose rifiuta di avviarsi e segnala che la rete è stata dichiarata esterna ma non è stata trovata. Crearla prima.
Notare cosa fa il file dell'applicazione con internal. Il database si trova soltanto sulla rete locale del progetto, quindi il proxy non può raggiungerlo e può farlo soltanto app. Aggiungendo internal: true sotto una rete si impedisce inoltre qualsiasi accesso alla rete esterna. Per un database è una buona impostazione predefinita, ma prima di applicarla è importante conoscerne una conseguenza: un container collegato a una rete interna non può scaricare nulla, quindi un entrypoint che esegue apt-get update o pip install all'avvio si blocca e termina con un timeout.
Per una configurazione completa con regole di routing e certificati, vedere eseguire più applicazioni dietro una singola istanza di Traefik.
Le porte pubblicate bypassano UFW
Questa è la parte del networking di Compose che può trasformarsi in un incidente di sicurezza. Pubblichi una porta, verifichi che UFW sia attivo e neghi tutto tranne SSH, ma il servizio è comunque raggiungibile da Internet.
sudo ufw status
curl http://203.0.113.10:8080UFW indica che la porta è bloccata. Il curl restituisce comunque la pagina. Non c'è alcun malfunzionamento. Docker scrive direttamente in iptables le proprie regole di traduzione degli indirizzi e di inoltro. Il traffico destinato alla porta pubblicata di un container viene inoltrato al container invece di essere consegnato all'host. Di conseguenza, non attraversa la catena gestita da UFW per il traffico destinato localmente. Inoltre, le regole di Docker vengono valutate prima di quelle di UFW.
La correzione più semplice consiste nel pubblicare la porta solo dove serve:
ports:
- "127.0.0.1:8080:80"In questo modo il lato host viene associato all'interfaccia di loopback. La porta è quindi raggiungibile dal server stesso e tramite un tunnel SSH, ma da nessun'altra posizione. Colloca il punto di ingresso pubblico dietro un reverse proxy che pubblichi intenzionalmente le porte 80 e 443. La spiegazione completa, inclusa la catena DOCKER-USER per i casi in cui devi filtrare una porta pubblicata, è disponibile in perché Docker pubblica direttamente oltre UFW e come risolvere il problema.
Come eseguire il debug con quattro comandi
Per prima cosa verificare a quale rete è realmente collegato ogni container:
docker network inspect shop_defaultIl blocco Containers elenca tutti i container collegati, con il relativo indirizzo. Un servizio assente dall'elenco appartiene a un'altra rete, usa la modalità host oppure non è in esecuzione.
Testare la risoluzione dei nomi da un container temporaneo collegato alla stessa rete. In questo modo non servono strumenti nelle immagini utilizzate:
docker run --rm --network shop_default busybox nslookup db
docker run --rm --network shop_default busybox nc -zv db 5432Un errore nslookup indica un problema di risoluzione dei nomi o di appartenenza alla rete. Se nslookup ha esito positivo mentre nc fallisce, il nome e la rete sono corretti e il problema riguarda la connessione stessa. Se nc restituisce Connection refused, la causa più comune è che il servizio non sia in ascolto su quella porta oppure sia in ascolto su 127.0.0.1 all'interno del proprio container invece che su 0.0.0.0. Quest'ultimo caso è comune nei server di sviluppo. La correzione riguarda l'indirizzo di bind dell'applicazione, non Docker. Se nc rimane in attesa fino al timeout, verificare la presenza di qualcosa che blocca il traffico lungo il percorso, ad esempio una regola firewall, oppure controllare se un container è in pausa o non risponde.
Esiste un altro problema che può sembrare un bug di Docker. Se i container comunicano tra loro ma non riescono a raggiungere un computer della rete dell'ufficio o della VPN, è probabile che la subnet Docker si sovrapponga a quella rete. Per impostazione predefinita, Docker assegna le reti a partire da 172.17.0.0/16. Spostare il pool in /etc/docker/daemon.json:
{
"default-address-pools": [
{ "base": "10.200.0.0/16", "size": 24 }
]
}Eseguire quindi sudo systemctl restart docker e ricreare le reti interessate, perché una rete esistente conserva la subnet assegnata al momento della creazione.
FAQ
Perché i miei container non riescono a raggiungersi tramite il nome del servizio?
Non si trovano sulla stessa rete. Compose inserisce automaticamente ogni servizio in <project>_default, ma quando si aggiunge un elenco networks: a un servizio, quell'elenco diventa l'insieme completo delle reti a cui il servizio appartiene e la rete predefinita non viene più aggiunta implicitamente. Eseguire docker network inspect <network> e verificare che entrambi i container compaiano nel blocco Containers. Controllare anche che nessuno dei due servizi usi network_mode: host, perché un container in modalità host non appartiene a nessuna rete Docker e non può risolvere i nomi dei servizi.
Devo pubblicare le porte perché un servizio possa raggiungerne un altro?
No. Su una rete Compose, ogni porta di ogni container è raggiungibile dagli altri container collegati alla stessa rete. ports: serve soltanto a esporre un container al traffico esterno a Docker, mentre expose: è documentazione. Pubblicare la porta di un database è un'abitudine comune e rischiosa, perché espone il database sull'interfaccia pubblica del server.
Qual è la differenza tra la rete bridge e la rete host?
Bridge assegna al container un proprio namespace di rete e un indirizzo su uno switch virtuale, con risoluzione automatica dei nomi tra container e traduzione del traffico in uscita. Host assegna invece direttamente al container lo stack di rete dell'host: non ci sono un indirizzo separato, la risoluzione tramite nome del servizio, la pubblicazione delle porte né l'isolamento dagli altri listener dell'host. Bridge è la modalità predefinita e la scelta corretta, salvo che il processo debba usare le interfacce dell'host.
Come posso collegare container appartenenti a due file Compose diversi?
Creare una rete condivisa con docker network create edge, quindi dichiararla in entrambi i file con external: true e collegare i servizi che devono comunicare. Compose non la crea né la elimina. Se si omette il passaggio di creazione, Compose rifiuta di avviarsi e segnala che la rete è dichiarata come esterna ma non è stata trovata.
Perché il mio container è raggiungibile da Internet anche se UFW blocca la porta?
Perché una porta pubblicata viene gestita dalle regole di forwarding che Docker aggiunge a iptables. Queste regole vengono valutate prima di quelle di UFW e il traffico inoltrato non passa comunque dalla chain filtrata da UFW. Associare il lato host all'interfaccia di loopback con "127.0.0.1:8080:80" e pubblicare i servizi esposti tramite un reverse proxy sulle porte 80 e 443.