Docker Compose: reti, DNS e porte spiegati
Scopri rete bridge predefinita, DNS tramite nome del servizio, host mode, reti condivise tra progetti e perché una porta pubblicata può ignorare UFW.
Cosa crea Compose prima dell'avvio dell'applicazione
La rete di Docker Compose segue una regola fondamentale: docker compose up crea una rete privata per il progetto, collega tutti i servizi a questa rete e consente ai servizi di raggiungersi tramite il nome del servizio. Non è necessario scrivere una sola riga networks: per ottenere questo comportamento. La maggior parte della confusione sulle reti di Compose deriva dal fatto che la rete predefinita è già disponibile.
Ecco un file semplice. 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 controlla cosa ha creato Docker:
docker compose up -d
docker network lsL'elenco ora contiene una rete chiamata shop_default. Compose la denomina <project>_default e, per impostazione predefinita, usa come nome del progetto il nome della directory convertito in lettere minuscole. Puoi sostituirlo con docker compose -p myproject up -d oppure con un elemento name: myproject di primo livello nel file. Il relativo driver è bridge, che corrisponde a 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 durante l'uscita.
docker compose down elimina nuovamente la rete. Per questo un container obsoleto di un vecchio progetto può mantenere aperta una 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 conosci Compose, ti consigliamo di 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 è la parte 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. Pertanto, web raggiunge il database al 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 né l'altro servizio. I client Postgres lo indicano 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 risparmiare tempo in seguito. I nomi vengono risolti negli indirizzi di ciò che è in esecuzione in quel momento. Pertanto, docker compose up -d --scale web=3 restituisce un nome con tre indirizzi, mentre 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 offre alcuna risoluzione dei nomi. Per questo le indicazioni sui collegamenti tra container del 2016 non corrispondono a ciò che si osserva.
Non serve ports: per connettere due servizi
ports: pubblica una porta del container sull'host. Serve per il traffico in arrivo dall'esterno di Docker. Non ha nulla a che vedere con il traffico tra servizi, che funziona già su tutto l'intervallo di porte della rete del progetto.
Di conseguenza, ports: - "5432:5432", che molti aggiungono al servizio del database, è inutile e crea un rischio concreto: espone Postgres sull'interfaccia pubblica del server. Eliminalo. Se vuoi renderlo raggiungibile dal laptop per una migrazione, esegui il binding sull'interfaccia di loopback con "127.0.0.1:5432:5432" e raggiungilo tramite un tunnel SSH. La differenza tra un socket in ascolto, una porta pubblicata e una regola firewall è descritta in come funzionano le porte e i servizi in ascolto su Linux.
Con Compose, expose: serve solo come documentazione. Non apre nulla, perché tra i container sulla stessa rete non è stato chiuso nulla.
Quando network_mode host è appropriato 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 vedere il traffico broadcast o multicast sulla rete locale, ad esempio per il rilevamento dei dispositivi da parte di un media server o di un hub di automazione domestica, non può vederlo 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 aspetto importante 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, mentre il container si associa alle porte richieste dal proprio 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 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 si associa a 0.0.0.0 all'interno di un container in modalità host è in ascolto su ogni interfaccia del server, inclusa quella pubblica, esattamente come un pacchetto installato con apt. Questo comporta anche un vantaggio: il 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 solo dalla versione 4.34 e solo dopo averla abilitata, con gli ulteriori limiti per cui i container non possono associarsi agli indirizzi IP dell'host e vengono gestiti solo TCP e UDP. Se metà del team usa server Linux e l'altra metà Docker Desktop, lo stesso file può comportarsi in modo diverso.
Usate la modalità host quando dovete accedere alle interfacce dell'host. Non usatela 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. Per questo un reverse proxy in proxy/compose.yaml non può 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. Lato 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: trueLato 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 intatta su 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 per la rete nel file e un altro nome sull'host.
Se la rete non esiste, Compose rifiuta di avviarsi e segnala che la rete è stata dichiarata esterna ma non è stata trovata. Creararla prima.
Notare cosa fa il file dell'applicazione con internal. Il database si trova solo su quella rete locale del progetto, quindi il proxy non può raggiungerlo e solo app può farlo. Aggiungere internal: true a una rete elimina anche completamente il relativo percorso verso l'esterno. Questa è una buona impostazione predefinita per un database, ma prima di applicarla occorre considerare un costo: un container su una rete interna non può scaricare nulla, quindi un entrypoint che esegue apt-get update o pip install all'avvio si bloccherà e poi terminerà con un timeout.
Per una configurazione completa con regole di instradamento e certificati, vedere eseguire diverse 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, e il traffico destinato a una porta pubblicata del 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 applicate prima di quelle di UFW.
La soluzione 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 al loopback. La porta è quindi raggiungibile dal server stesso e tramite un tunnel SSH, ma da nessun'altra origine. Posiziona 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
Inizia verificando 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 da questo elenco si trova su un'altra rete, è in modalità host oppure non è in esecuzione.
Verifica la risoluzione dei nomi da un container temporaneo collegato alla stessa rete. In questo modo non ti servono strumenti nelle immagini che usi:
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 riesce 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 riguarda l'indirizzo di binding dell'applicazione, non Docker.
Esiste un altro errore che può sembrare un problema di Docker. Se i container comunicano tra loro ma non riescono a raggiungere una macchina sulla rete dell'ufficio o sulla rete VPN, è probabile che la subnet Docker si sovrapponga a quella rete. Per impostazione predefinita, Docker assegna gli indirizzi a partire da 172.17.0.0/16. Modifica il pool in /etc/docker/daemon.json:
{
"default-address-pools": [
{ "base": "10.200.0.0/16", "size": 24 }
]
}Esegui quindi sudo systemctl restart docker e ricrea le reti interessate, perché una rete esistente conserva la subnet assegnata al momento della creazione.
FAQ
Perché i miei container non riescono a raggiungersi usando il nome del servizio?
Non si trovano sulla stessa rete. Compose inserisce automaticamente ogni servizio in <project>_default, ma quando aggiungi un elenco networks: a un servizio, quell’elenco diventa l’insieme completo delle reti del servizio e la rete predefinita non viene più aggiunta implicitamente. Esegui docker network inspect <network> e verifica che entrambi i container compaiano nel blocco Containers. Verifica anche che nessuno dei due servizi utilizzi network_mode: host, perché un container in modalità host non si trova su alcuna 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, tutte le porte di ogni container sono raggiungibili dagli altri container collegati alla stessa rete. ports: serve solo a esporre un container al traffico proveniente dall’esterno di Docker, mentre expose: è solo documentazione. Pubblicare la porta di un database è una pratica comune e rischiosa, perché espone il database sull’interfaccia pubblica del server.
Qual è la differenza tra la rete bridge e la rete host?
La rete bridge assegna al container il proprio namespace di rete e un indirizzo su uno switch virtuale, con risoluzione automatica dei nomi tra container e traduzione del traffico in uscita. La rete host collega direttamente il container allo stack di rete dell’host: non assegna un indirizzo separato, non consente la risoluzione tramite nome del servizio, non richiede la pubblicazione delle porte e non isola il container dagli altri processi in ascolto sull’host. La rete bridge è quella predefinita ed è la scelta corretta, salvo che il processo debba utilizzare direttamente le interfacce dell’host.
Come posso collegare container appartenenti a due file Compose diversi?
Crea una rete condivisa con docker network create edge, quindi dichiarala in entrambi i file con external: true e collega i servizi che devono comunicare. Compose non la creerà né la eliminerà. Se ometti il passaggio di creazione, Compose rifiuta l’avvio 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 inoltro che Docker aggiunge a iptables. Queste regole vengono applicate prima di quelle di UFW e il traffico inoltrato non passa comunque attraverso la chain filtrata da UFW. Associa il lato host all’interfaccia di loopback con "127.0.0.1:8080:80" e colloca tutto ciò che deve essere pubblico dietro un reverse proxy sulle porte 80 e 443.