Salta al contenuto principale

Orvieto Linux User Group | Promozione software libero a Orvieto

Docker Networking: come comunicano container, host e servizi

Docker diventa davvero interessante quando smettiamo di pensare a un singolo container e iniziamo a immaginare più servizi che devono collaborare tra loro.

Un’applicazione web, per esempio, potrebbe avere un container che esegue il frontend, uno che contiene il backend e un altro che gestisce il database. Tutti questi componenti devono comunicare, ma non necessariamente devono essere esposti direttamente alla rete locale o a Internet.

È qui che entra in gioco il Docker Networking.

In questa guida vedremo come Docker collega i container, cosa sono le network, come comunicano tra loro e soprattutto la differenza tra comunicazione interna e pubblicazione di una porta sull’host.

Se stai iniziando a conoscere Docker, ti consigliamo di partire dalla nostra introduzione a Docker su Linux, dove abbiamo visto i concetti fondamentali di immagini e container.

Perché i container hanno bisogno di una rete?

Un container non è semplicemente un processo che gira sul nostro computer.

Docker crea un ambiente isolato e assegna al container una propria interfaccia di rete e, normalmente, un indirizzo IP all’interno della rete Docker a cui è collegato.

Questo permette, per esempio, di avere:

HOST LINUX
│
├── Docker
│
├── Container web
│
├── Container database
│
└── Container applicazione

Ma avere più container non significa automaticamente che tutti debbano essere raggiungibili dall’esterno.

Possiamo infatti avere un web server accessibile dal nostro browser, mentre il database rimane raggiungibile soltanto dagli altri container.

Questa distinzione è uno dei concetti più importanti da capire quando iniziamo a utilizzare Docker in modo più strutturato.


Le network di Docker

Quando installiamo Docker vengono create alcune network predefinite. Possiamo visualizzarle con:

docker network ls

In una configurazione standard troveremo generalmente network come:

NETWORK ID     NAME      DRIVER    SCOPE
xxxxxxxxxxxx   bridge    bridge    local
xxxxxxxxxxxx   host      host      local
xxxxxxxxxxxx   none      null      local

La più importante per iniziare è bridge.

Una bridge network crea una rete virtuale attraverso la quale i container possono comunicare. Docker assegna agli elementi collegati indirizzi appartenenti alla subnet della rete e gestisce il collegamento con l’host e, normalmente, verso l’esterno.

È un concetto molto simile a quello che abbiamo incontrato parlando di reti Linux: esiste una rete, esistono degli indirizzi IP e il traffico viene instradato attraverso un’interfaccia virtuale.

Se vuoi ripassare questi concetti, puoi approfondire la nostra guida su indirizzi IP, subnet e gateway.


La rete bridge predefinita

Quando avviamo un container senza specificare una network, Docker lo collega normalmente alla rete bridge predefinita.

Possiamo fare una semplice prova:

docker run -dit --name container1 ubuntu:24.04
docker run -dit --name container2 ubuntu:24.04

I due container saranno avviati in background.

Possiamo verificare la situazione con:

docker ps

e successivamente osservare i dettagli della rete:

docker network inspect bridge

L’output è piuttosto ricco, ma tra le informazioni più interessanti troviamo i container collegati alla rete e i relativi indirizzi IP.

Questo ci permette di vedere concretamente che Docker non sta semplicemente facendo “girare due programmi”: sta anche costruendo una piccola infrastruttura di rete virtuale.

C’è però una cosa importante da sapere.

La rete bridge predefinita è utile per fare esperimenti, ma Docker raccomanda di preferire le user-defined bridge networks quando dobbiamo costruire applicazioni composte da più container. Una delle ragioni principali è il DNS integrato.

Ed è proprio qui che Docker Networking diventa molto più comodo.


Creare una rete Docker personalizzata

Possiamo creare una nostra rete con:

docker network create applicazione

Docker creerà una nuova rete di tipo bridge.

Possiamo verificarla con:

docker network ls

e analizzarne la configurazione con:

docker network inspect applicazione

A questo punto possiamo avviare due container collegandoli direttamente alla nostra rete:

docker run -dit \
  --name web \
  --network applicazione \
  nginx

e:

docker run -dit \
  --name client \
  --network applicazione \
  ubuntu:24.04

Abbiamo quindi creato una piccola rete privata Docker:

                 HOST LINUX
                     │
                  Docker
                     │
             ┌───────┴───────┐
             │  applicazione  │
             │    bridge      │
             └───────┬───────┘
                     │
             ┌───────┴───────┐
             │               │
          ┌───────┐       ┌───────┐
          │  web  │       │ client│
          │ nginx │       │ Ubuntu│
          └───────┘       └───────┘

Ma c’è una differenza fondamentale rispetto alla rete bridge predefinita.

I container possono trovarsi per nome

Nelle user-defined bridge network Docker fornisce automaticamente la risoluzione DNS tra i container. In pratica, un container può raggiungere un altro utilizzando il suo nome, senza dover conoscere a priori il suo indirizzo IP.

Questo è estremamente importante.

Immaginiamo infatti di avere un’applicazione e un database.

Non vogliamo scrivere nell’applicazione qualcosa come:

database = 172.18.0.5
 

perché l’indirizzo IP del container potrebbe cambiare.

È molto più pratico utilizzare:

database = database
 

dove database è il nome del container o, più avanti, del servizio definito con Docker Compose.

Docker si occupa della risoluzione del nome all’interno della rete.


Container-to-container: non serve pubblicare la porta

Questo è probabilmente il concetto più importante dell’articolo.

Supponiamo di avere un database PostgreSQL in un container e un’applicazione in un altro.

I due container appartengono alla stessa user-defined network.

L’applicazione può quindi comunicare con PostgreSQL direttamente attraverso la rete Docker.

Non è necessario pubblicare la porta PostgreSQL sull’host soltanto per permettere all’applicazione di raggiungerla.

Possiamo immaginare:

             Docker network
        ┌─────────────────────┐
        │                     │
        │   APP ───────── DB  │
        │          :5432      │
        │                     │
        └─────────────────────┘
                    │
                    │
                  HOST

Il database può quindi rimanere accessibile solamente all’interno della rete Docker.

Questo riduce anche la superficie esposta dell’applicazione: non tutto ciò che gira nei container deve essere necessariamente raggiungibile dalla rete del nostro computer.


E allora a cosa serve -p?

Qui arriviamo a una distinzione che spesso crea confusione.

Nella nostra guida introduttiva avevamo utilizzato:

docker run -d --name webserver -p 8080:80 nginx

La parte -p 8080:80 significa che Docker pubblica la porta 80 del container sulla porta 8080 dell’host.

Possiamo rappresentarla così:

Browser
   │
   │ localhost:8080
   ▼
HOST LINUX
   │
   │ :8080
   ▼
Docker
   │
   │ :80
   ▼
Container Nginx

Quindi:

8080:80
  │   │
  │   └── porta del container
  └────── porta dell'host

È una cosa diversa dalla comunicazione tra container.

Se app deve parlare con database, possiamo farlo attraverso la rete Docker senza pubblicare la porta del database sull’host.

Se invece vogliamo aprire Nginx dal browser del nostro computer, allora dobbiamo rendere disponibile la sua porta attraverso il meccanismo di publishing.

Docker utilizza regole di rete e NAT per effettuare questo inoltro.


Attenzione alle porte pubblicate

C’è un dettaglio importante anche dal punto di vista della sicurezza.

Quando scriviamo:

docker run -d -p 8080:80 nginx

senza specificare un indirizzo IP, la porta pubblicata viene normalmente associata agli indirizzi del sistema host e può quindi essere raggiungibile dall’esterno del computer, a seconda della rete e delle regole firewall presenti.

Se invece vogliamo che il servizio sia accessibile soltanto dal computer stesso, possiamo specificare l’indirizzo di loopback:

docker run -d \
  -p 127.0.0.1:8080:80 \
  nginx

In questo caso la porta viene pubblicata sull’interfaccia locale.

È un piccolo dettaglio sintattico che può fare una grande differenza quando iniziamo a pubblicare servizi su un server o su un homelab.


Come collegare un container a una rete già esistente

Non è necessario decidere tutto al momento della creazione.

Possiamo collegare un container già esistente a una rete con:

docker network connect applicazione container1

e verificare successivamente il risultato con:

docker network inspect applicazione

Allo stesso modo possiamo scollegarlo:

docker network disconnect applicazione container1

Le user-defined network permettono quindi di modificare la configurazione di rete dei container anche durante il loro ciclo di vita.

Questa possibilità diventa particolarmente utile quando iniziamo a costruire ambienti composti da diversi servizi.


E la rete host?

Finora abbiamo parlato soprattutto delle reti bridge, ma Docker mette a disposizione anche altre modalità.

Con:

--network host
 

il container utilizza direttamente lo stack di rete dell’host, invece di avere una propria rete isolata nello stesso modo delle bridge network.

È una modalità particolare, da utilizzare quando abbiamo un motivo preciso per farlo: cambia infatti il modo in cui il container interagisce con le porte e con la rete del sistema.

Esiste anche la rete:

none
 

che fornisce al container una configurazione di rete estremamente limitata.

Per iniziare con Docker non è necessario approfondire subito tutte queste modalità. È molto più importante capire bene il modello bridge, perché è quello che incontreremo frequentemente nelle applicazioni Docker composte da più container.


Come ragionare quando qualcosa non comunica

Quando un’applicazione Docker non riesce a raggiungere un altro servizio, conviene evitare di modificare casualmente porte e indirizzi IP.

Meglio procedere per livelli.

Prima controlliamo quali network esistono:

docker network ls

Poi osserviamo una rete specifica:

docker network inspect applicazione

Possiamo quindi verificare:

  • quali container sono collegati;
  • quali indirizzi IP hanno ricevuto;
  • quale network stanno utilizzando;
  • se il container che ci interessa è effettivamente nella stessa rete.

Dal lato Linux possiamo inoltre utilizzare gli strumenti che abbiamo già visto nella nostra guida sul networking di base su Linux, ad esempio ip, ss e ping, quando sono disponibili e appropriati all’interno del container.

La cosa importante è capire dove si interrompe la comunicazione.

Non è detto che un problema di connessione tra due container sia un problema di porte pubblicate. Anzi, quando due servizi appartengono alla stessa user-defined network, la prima cosa da verificare è proprio la rete Docker e la risoluzione del nome.


Docker Networking: la mappa mentale da ricordare

A questo punto possiamo riassumere il funzionamento con un modello molto semplice:

                         INTERNET / LAN
                               │
                               │
                         porta pubblicata
                               │
                               ▼
                        ┌─────────────┐
                        │ HOST LINUX  │
                        │ Docker      │
                        └──────┬──────┘
                               │
                        Docker bridge
                               │
              ┌────────────────┼────────────────┐
              │                │                │
          ┌───────┐        ┌───────┐        ┌───────┐
          │  WEB  │◄──────►│  APP  │◄──────►│  DB   │
          └───────┘        └───────┘        └───────┘
                 comunicazione interna
                    tramite network

La domanda da porsi è quindi sempre:

chi deve comunicare con chi?

Se un servizio deve essere raggiunto dal browser o da un altro computer della rete, probabilmente dovremo pubblicare una porta.

Se invece due container devono semplicemente collaborare tra loro, è spesso sufficiente collegarli alla stessa user-defined network.

Questo modo di ragionare diventa ancora più importante quando passiamo da due container a un’intera applicazione composta da frontend, backend, database, reverse proxy e altri servizi.


Conclusioni

Il networking Docker può sembrare complicato perché introduce interfacce virtuali, subnet, indirizzi IP, NAT e port mapping.

In realtà, una volta separati i concetti fondamentali, il modello diventa abbastanza intuitivo.

Una network Docker permette ai container di comunicare. Una user-defined bridge network aggiunge una gestione più comoda della comunicazione e della risoluzione dei nomi. La pubblicazione delle porte, invece, serve a rendere un servizio del container raggiungibile attraverso l’host.

La distinzione fondamentale da portarsi dietro è quindi questa:

comunicare tra container non significa necessariamente pubblicare una porta sull’host.

È una piccola differenza concettuale, ma rappresenta uno dei passaggi che permettono di smettere di utilizzare Docker soltanto per singoli esperimenti e iniziare a costruire veri ambienti composti da più servizi.

Ed è proprio il passo successivo che affronteremo con Docker Compose, dove potremo definire container, reti e configurazioni all’interno di un unico progetto.

LICENZA E CONDIZIONI D’USO

Questo How-to è rilasciato sotto licenza Creative Commons Attribution-NonCommercial 3.0 Italy. 

CONTATTI DELL’AUTORE

Marco Ciammella marco@orvietolinux.it

Orvieto Linux User Group info@orvietolinux.it