Pratiche migliori per il Networking in Docker
Docker è diventato un pilastro dello sviluppo software moderno, consentendo agli sviluppatori di creare, distribuire ed eseguire applicazioni in ambienti isolati. Sebbene Docker semplifichi molti aspetti del deployment, la gestione della rete nei container può diventare complessa, soprattutto in ambienti multi-container. Adottare migliori pratiche per il networking in Docker è fondamentale per garantire prestazioni, scalabilità e sicurezza delle applicazioni.
Comprendere le Modalità di Rete di Docker
Offre diverse opzioni di rete, ciascuna adatta a casi d’uso specifici. Conoscerle è il primo passo per scegliere quella più appropriata:
- Bridge (la default e quella con più limite): Consente ai container di comunicare tra loro sullo stesso host tramite una rete virtuale. È ideale per ambienti di sviluppo o per container che non necessitano di essere esposti direttamente.
- Host: Il container condivide la rete con l’host, eliminando la virtualizzazione della rete. Questo approccio può migliorare le prestazioni, ma riduce l’isolamento.
- Overlay: Utile in ambienti multi-host, consente ai container di comunicare tra loro tramite una rete distribuita gestita da Docker Swarm o Kubernetes.
- Macvlan: Consente ai container di ottenere un proprio indirizzo IP nella rete fisica, simulando una scheda di rete virtuale.
- IPvlan - Una subnet virtuale di livello 2 dell’host con propri indirizzi IP nella tua LAN, oppure un’interfaccia virtuale di livello 3
- None: Disattiva completamente la rete del container, rendendolo isolato.
Comunicazione tra Container
La caratteristica più importante di una rete bridge definita dall’utente è che abilita la risoluzione dei nomi DNS e la comunicazione tra i container al suo interno. Ad esempio, se hai il container del tuo container-app
e il container container-db
sulla rete bridge definita dall’utente, puoi configurare container-app
per utilizzare container-db
:5432 come host del database, invece di dover usare l’IP dell’host ed esporre le porte.
Inoltre, puoi controllare la dimensione della rete e stabilire se può comunicare con le reti più ampie LAN/WAN. Uno degli effetti positivi di questa configurazione è che non devi più preoccuparti di conflitti di porte. Ad esempio, puoi avere 50 container sulla stessa rete bridge, tutti che utilizzano la porta 80, senza problemi. Solo il reverse proxy (se non sai cos’è un reverse proxy ne ho parlato qui:) deve esporre le porte 80 e 443 all’host.
Puoi creare reti bridge usando la CLI di Docker docker network create
, ma Docker Compose si occupa automaticamente della maggior parte del lavoro, quindi useremo quest’ultimo per gli esempi di seguito.
Fondamenti del Networking in Docker Compose
Se non definisci alcuna rete nel tuo progetto Compose, Docker creerà automaticamente una rete predefinita chiamata <nome progetto>_default
con impostazioni standard, e collegherà ad essa tutti i container del progetto.
Se vuoi andare oltre le reti predefinite, puoi configurare qualcosa di più personalizzato.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
services:
container-app:
image: docker.io/container-app:latest
container_name: container-app
[...]
networks:
- proxy
- backend
container-db:
image: docker.io/postgres:15-alpine
container_name: container-db
[...]
networks:
- backend
swag:
image: docker/proxy-app:latest
container_name: nginx
[...]
networks:
- proxy
ports:
- 443:443
- 80:80
networks:
proxy:
name: proxy
ipam:
config:
- subnet: 172.18.0.0/24
backend:
name: backend
internal: true
ipam:
config:
- subnet: 172.18.1.0/29
Come esempio, in questo scenario abbiamo:
- container che deve comunicare con tutti gli altri (
container-app
), - container che non ha bisogno di connessioni in uscita (
container-db
), - container che deve essere esposto all’host (
proxy-app
, che può essere swag, nginx o traefik qualsiasi reverse proxy sceglierete, ma per l’esempio chiameremo proxy).
Il container container-app
è connesso sia alla rete proxy
(che contiene anche il nostro reverse proxy), sia a una rete privata che chiameremo nell’esempio backend
include anche il database Postgres container-db
.
Solo il container proxy espone delle porte, poiché tutte le altre comunicazioni avvengono attraverso le reti bridge. La rete backend
è stata contrassegnata come interna, il che significa che nessun traffico può entrare o uscire da quella rete. Anche esponendo le porte sul container container-db
, non sarebbe possibile accedervi dall’host o dalla LAN.
Abbiamo inoltre assegnato un subnet a ciascuna rete:
- Subnet /24 (254 indirizzi utilizzabili) per la rete
proxy
, ipotizzando che non sappiamo quanti container potrebbero aver bisogno di passare attraverso il reverse proxy. - Subnet /29 (6 indirizzi utilizzabili) per la rete
backend
, poiché è sufficiente per i nostri scopi.
Abbiamo fatto questa scelta perché la subnet predefinita per una rete bridge è /16 (65.534 indirizzi utilizzabili), che è eccessiva nella quasi totalità dei casi e può portare a esaurire il pool di indirizzi disponibili.
Se non ti interessa questo aspetto, puoi omettere tutto eccetto il parametro name:
e Docker utilizzerà i valori predefiniti.
Se ti aiuta a visualizzare la configurazione, qui sotto metto un output fatto con Graphviz per mostrare le reti che abbiamo preso in esempio e i collegamenti tra loro.
Se si vuole aggiungere un container sia alla rete default che a un’altra rete personalizzata, devi specificare entrambe. Ad esempio, se nulla nel tuo progetto Compose è connesso alla rete default, questa non verrà creata.
1
2
3
4
5
6
7
8
9
10
swag:
image: docker/proxy-app:latest
container_name: nginx
[...]
networks:
- default
- proxy
ports:
- 443:443
- 80:80
Se non specifichi un
name:
Compose prefiggerà il nome del progetto Compose al nome della rete. Ad esempio, se la cartella del tuo progetto Compose si chiamahomelab
, verrà creata una rete Docker chiamatahomelab_proxy
, ma potrai comunque riferirti a essa comeproxy
all’interno di questo progetto Compose. Da altri progetti Compose, invece, dovrai fare riferimento a essa comehomelab_proxy
.
1
2
3
networks:
proxy: <-- questo è la referenza del nome della rete che verrà usato all'interno del progetto compose
name: proxy <-- nome reale della network che verrà creata
non è possibile comunicare tra container su reti bridge diverse, poiché, come detto, queste funzionano come piccole reti LAN separate. Sebbene sia tecnicamente possibile farlo con alcune regole creative di iptables, il DNS non funzionerebbe, quindi ci sono pochi vantaggi nel farlo
Cambiare il pool di indirizzi default
puoi modificare l’assegnazione della subnet predefinita per Docker nel file di configurazione delle opzioni daemon.json
(situato in /etc/docker/daemon.json
sulla maggior parte degli host). Ad esempio:
1
2
3
4
5
6
{
"default-address-pools":
[
{"scope":"local","base":"172.18.0.0/16","size":24}
]
}
Nell’esempio sopra stiamo configurando Docker ad utilizzare blocchi di rete /24
all’interno dell’intervallo 172.18.0.0/16
, il che ci dà 256 reti con 254 indirizzi utilizzabili ciascuna, probabilmente sufficienti per la maggior parte delle persone. Ovviamente, questo non influirà su nessuna rete esistente che hai già creato e puoi sempre sovrascrivere questa impostazione quando crei una nuova rete. Puoi creare più di un blocco di allocazione, se lo desideri, e devi riavviare il servizio Docker dopo aver modificato il file di configurazione affinché le modifiche abbiano effetto.
Di default, Docker assegnerà 172.17.0.0/16 alla rete Bridge predefinita e inizierà ad allocare blocchi di indirizzi /16 a partire da 172.18.0.0 fino a quando lo spazio nell’intervallo 172.16.0.0/12 non sarà esaurito. A quel punto, Docker passerà all’allocazione di blocchi /24 nell’intervallo 192.168.0.0/16. Inoltre, riserva l’intervallo 10.0.0.0/8 per le reti Overlay con Docker Swarm.
Verifiche
A questo punto possiamo verificare con ping
se riusciamo a raggiungere i container nelle reti:
1
2
3
4
5
6
7
8
9
10
11
12
$ docker exec -it container-app bash
root@2b11345af874:/> ping container-db
PING container-db (172.18.1.2): 56 data bytes
64 bytes from 172.18.1.2: seq=0 ttl=64 time=0.080 ms
root@2b11345af874:/> ping proxy-app
PING proxy-app (172.18.0.4): 56 data bytes
64 bytes from 172.18.0.4: seq=0 ttl=64 time=0.261 ms
root@2b11345af874:/> ping google.com
PING google.com (142.250.180.14): 56 data bytes
64 bytes from 142.250.180.14: seq=0 ttl=117 time=7.417 ms
e dal container-db
:
1
2
3
4
5
6
7
8
9
10
$ docker exec -it container-db bash
57c7d2873c43:/> ping container-app
PING container-app (172.18.1.3): 56 data bytes
64 bytes from 172.20.1.3: seq=0 ttl=64 time=0.054 ms
57c7d2873c43:/> ping proxy-app
ping: bad address 'proxy-app'
57c7d2873c43:/> ping google.com
ping: bad address 'google.com'
dal test qui sopra, il container container-db, sulla sua rete backend, non può raggiungere proxy-app (perché non è collegato alla rete) e non può raggiungere Google (perché la rete è interna). Al contrario, il container container-app può raggiungere tutti e tre, e in particolare può comunicare con il suo database e con proxy-app senza dover uscire dalla rete Docker.
Se fai qualche prova, scoprirai che ogni rete ha il proprio suffisso DNS, che corrisponde al nome della rete stessa. Ad esempio, proxy-app può essere raggiunto da container-app come proxy-app.proxy, e container-db come container-db.backend. Questo dettaglio poco importante, ma a volte può tornare utile.
In Conclusione
L’obiettivo finale è minimizzare la “distanza” che i tuoi container devono percorrere per comunicare tra loro.
Se adotti l’approccio classico di esporre il container del database all’host e poi connetterti a esso da un altro container utilizzando l’indirizzo IP e la porta dell’host, il traffico partirà dal container sorgente, verrà NAT-ato verso l’interfaccia dell’host, poi di nuovo NAT-ato nella rete Docker per raggiungere il container del database, e infine il traffico di ritorno seguirà lo stesso percorso al contrario.
Ovviamente, è inefficiente, considerando che i container si trovano praticamente uno accanto all’altro.
Potresti decidere che non ti interessa isolare i container su reti interne separate e vuoi semplicemente mantenere tutto il più semplice possibile. In tal caso, puoi mettere tutto su un’unica grande rete ‘proxy’ e risolvere il problema in questo modo.
In alternativa, potresti decidere di voler mettere ogni servizio sulla propria piccola rete e usare un reverse proxy per connetterli tra loro
Se hai più progetti Docker-compose, probabilmente dovrai condividere più reti tra di essi. Questo può essere fatto definendole come esterne in qualsiasi progetto aggiuntivo, il che dice a Compose che non deve crearle o gestirle, perché ciò è già gestito altrove:
1
2
3
networks:
proxy:
external: true
Vale anche se hai creato una rete Docker utilizzando la CLI (con il comando docker network create
) invece che con Docker-Compose.
Se utilizzi una piattaforma che configura Docker per te, come un NAS Synology/QNAP/Asustor o Unraid, è importante essere molto più attenti nella pianificazione della rete, poiché spesso queste piattaforme effettuano scelte insolite che differiscono dai valori predefiniti normali.
Inoltre, se usi Ubuntu, assicurati di non aver installato Docker per errore come Snap, può causare problemi strani a causa del suo sandboxing.
Se dedichi qualche minuto a pianificare la rete, puoi risparmiarti molti problemi (e tempi di inattività) in futuro, quando dovrai smantellare tutto per portarlo allo stato desiderato. Ricorda che non puoi modificare la configurazione di una rete Docker senza distruggerla e ricrearla, e per farlo devi fermare ed eliminare ogni container collegato a quella rete.