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.
Il DNS per 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 correnti 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 si trovano sulla 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 identifica quel container, non l'host e neppure 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. Pertanto, docker compose up -d --scale web=3 restituisce un nome con tre indirizzi e un client che memorizza il DNS nella cache per sempre rimane associato a un container non più attivo. Inoltre, la rete legacy bridge usata da un semplice docker run senza --network non offre alcuna risoluzione dei nomi. Per questo le indicazioni sui collegamenti tra container del 2016 non corrispondono al comportamento osservato.
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 costi comporta
La modalità host elimina lo spazio dei nomi 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 ricevere traffico broadcast o multicast dalla rete locale, ad esempio per il rilevamento dei dispositivi in un media server o in un hub di domotica, non può visualizzarlo dietro un bridge, perché il bridge non inoltra questo traffico al container. Un agente di monitoraggio che legge i contatori delle interfacce dell'host deve accedere alle interfacce dell'host. Inoltre, si elimina il passaggio di traduzione degli indirizzi, un aspetto 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 esegue il binding sulle porte richieste 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 tramite il nome del servizio non è più disponibile in nessuna delle due direzioni. Il container non si trova sulla rete del progetto, quindi non può risolvere db e gli altri servizi non possono risolverlo. Può raggiungerli solo tramite le porte pubblicate sull'host, in genere su 127.0.0.1.
L'isolamento viene meno. Un processo che esegue il binding su 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. Questo ha anche un vantaggio: il traffico segue il normale percorso di input, quindi le regole di UFW vengono applicate, cosa che non avviene per le porte pubblicate.
La modalità host è una funzionalità di Linux Docker Engine. Docker Desktop la supporta solo dalla versione 4.34 e solo dopo averla abilitata. Inoltre, i container non possono eseguire il binding sugli indirizzi IP dell'host e vengono gestiti solo i protocolli 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 in quattro comandi
Iniziare verificando a quale rete è realmente collegato ogni container:
docker network inspect shop_defaultIl blocco Containers elenca tutti i container collegati e il relativo indirizzo. Un servizio assente dall'elenco si trova su un'altra rete, è in 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 all'interno delle proprie immagini:
docker run --rm --network shop_default busybox nslookup db
docker run --rm --network shop_default busybox nc -zv db 5432Un errore in nslookup indica un problema di risoluzione dei nomi o di appartenenza alla rete. Se nslookup ha esito positivo mentre nc fallisce, il servizio è in esecuzione ma non è in ascolto su quella porta, oppure è in ascolto su 127.0.0.1 all'interno del proprio container invece che su 0.0.0.0. Questo caso è comune con i server di sviluppo. La correzione va applicata all'indirizzo di bind dell'applicazione, non a Docker.
Esiste un altro errore che può sembrare un problema di Docker. Se i container comunicano tra loro ma non riescono a raggiungere un computer sulla rete dell'ufficio o sulla rete VPN, è probabile che la subnet Docker si sovrapponga a quella rete. Per impostazione predefinita, Docker assegna le subnet 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 }
]
}Quindi eseguire 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.