Reti Docker su un VPS: bridge, host e macvlan spiegati
Come funzionano le reti bridge, host e macvlan di Docker su un singolo VPS. Risoluzione DNS su bridge personalizzato, pubblicazione porte, configurazione IPv6 e isolamento di rete con Docker Compose.
Hai un VPS. Più container devono comunicare tra loro e con il mondo esterno. Docker offre tre driver di rete (network driver) rilevanti: bridge, host e macvlan. Ciascuno fa compromessi diversi tra isolamento, prestazioni e praticità.
Questo articolo spiega quando scegliere ciascun driver, come collegare i container usando reti bridge personalizzate e Docker Compose, e come evitare gli errori comuni che rendono i servizi irraggiungibili o accidentalmente esposti.
Prerequisiti: conoscenza base della CLI Docker e familiarità con Docker Compose.
Qual è la differenza tra le reti bridge e host di Docker?
La rete bridge assegna a ogni container il proprio network namespace con una coppia ethernet virtuale (veth) che lo collega a un bridge software sull'host. I container ricevono IP privati (tipicamente nel range 172.17.0.0/16) e raggiungono l'esterno tramite NAT gestito da iptables. La rete host rimuove completamente il network namespace: il container condivide lo stack di rete dell'host, collegandosi direttamente alle porte dell'host senza traduzione di indirizzi.
Confronto rapido:
| Criterio | Bridge personalizzato | Host | Macvlan |
|---|---|---|---|
| Isolamento di rete | Completo (namespace proprio) | Nessuno (condivide l'host) | Completo (indirizzo MAC proprio) |
| Risoluzione DNS | Automatica per nome container | Usa il DNS dell'host | Nessun DNS integrato |
| Mapping porte | Richiesto (-p) |
Non necessario (binding diretto) | Non necessario (IP reale) |
| Overhead prestazionale | Basso (NAT + veth) | Nessuno | Nessuno |
| Rischio di sicurezza | Basso (isolato di default) | Alto (nessun confine di rete) | Medio (richiede modalità promiscua) |
| Rilevanza VPS | Scelta principale | Monitoring, alto throughput | Raramente utilizzabile |
Perché usare una rete bridge personalizzata invece della bridge di default?
La rete bridge di default (chiamata docker0) non fornisce risoluzione DNS tra container. I container possono raggiungersi solo tramite indirizzo IP, che cambia a ogni riavvio. Le reti bridge personalizzate offrono DNS automatico, migliore isolamento e connessione/disconnessione a caldo. Usa una bridge personalizzata per tutto. Tratta la bridge di default come un retaggio.
Il problema DNS
Quando Docker si avvia, crea una rete bridge di default. Ogni container senza flag --network esplicito finisce su di essa. Ma i container sulla bridge di default non possono risolversi per nome:
# Start two containers on the default bridge
docker run -d --name web nginx:alpine
docker run -d --name client alpine sleep 3600
# Try name resolution - this fails
docker exec client ping -c1 web
# ping: bad address 'web'
Prova lo stesso con una bridge personalizzata:
# Create a custom bridge network
docker network create app-net
# Start containers on it
docker run -d --name web2 --network app-net nginx:alpine
docker run -d --name client2 --network app-net alpine sleep 3600
# Name resolution works
docker exec client2 ping -c1 web2
# PING web2 (172.18.0.2): 56 data bytes
# 64 bytes from 172.18.0.2: seq=0 ttl=64 time=0.089 ms
Come funziona il DNS integrato di Docker
Ogni container su una rete definita dall'utente riceve 127.0.0.11 come server DNS. È il server DNS integrato di Docker. Risolve i nomi dei container e gli alias di servizio nei rispettivi indirizzi IP attuali. Se il nome non corrisponde a un container, inoltra la query ai server DNS configurati sull'host.
docker exec client2 cat /etc/resolv.conf
# nameserver 127.0.0.11
Il server DNS gira nel network namespace del container, ma la risoluzione effettiva avviene nel daemon Docker sull'host. Per evitare conflitti con servizi che potrebbero usare la porta 53 dentro il container, il listener DNS di Docker usa internamente una porta alta casuale e reindirizza le query tramite regole iptables.
Non esiste un indirizzo IPv6 equivalente. L'indirizzo 127.0.0.11 funziona anche nei container solo IPv6.
Il server DNS integrato restituisce un TTL di 600 secondi (10 minuti) per i record dei nomi container. Questo conta per i deployment blue-green: se sostituisci un container, gli altri possono ancora risolvere il vecchio IP per 10 minuti. Le applicazioni con cache DNS aggressivo (Java lo cache indefinitamente di default) manterranno indirizzi obsoleti ancora più a lungo.
I container sulla stessa rete bridge personalizzata espongono tutte le porte tra loro senza alcun flag -p. La pubblicazione delle porte controlla solo l'accesso dall'esterno della rete (l'host o internet). Due container su app-net comunicano liberamente su qualsiasi porta.
Bridge di default vs bridge personalizzata
| Funzionalità | Bridge di default (docker0) |
Bridge personalizzata |
|---|---|---|
| DNS per nome container | No | Sì |
| Connessione/disconnessione dinamica | No (richiede riavvio container) | Sì |
| Configurabile per rete | No (richiede modifica daemon.json + riavvio) |
Sì |
| Isolamento dagli altri stack | No (tutti i container non assegnati la condividono) | Sì |
| Raccomandato per la produzione | No | Sì |
Quando usare la rete host su un VPS?
Usa la rete host quando un container necessita di prestazioni di rete grezze o deve collegarsi dinamicamente a molte porte. Il container condivide direttamente lo stack di rete dell'host, bypassando NAT e bridge ethernet virtuale. Questo elimina l'overhead misurabile per i carichi ad alto throughput.
Casi d'uso tipici su un VPS:
- Agenti di monitoring come Prometheus node-exporter o Netdata che devono vedere tutte le interfacce e il traffico dell'host
- Server DNS che devono ascoltare sull'IP reale dell'host
- Servizi sensibili alle prestazioni dove l'overhead NAT conta (alti tassi di pacchetti, carichi UDP intensivi)
# Run a container with host networking
docker run -d --name node-exporter --network host \
prom/node-exporter:latest
Il flag --publish viene ignorato con la rete host. Il container si collega direttamente alle porte dell'host. Se la porta 9100 è già in uso sull'host, il container non si avvia.
Compromesso di sicurezza
La rete host offre lo stesso isolamento di rete dell'esecuzione diretta del processo sull'host: nessuno. Il container può vedere tutte le interfacce di rete, collegarsi a qualsiasi porta e catturare il traffico. Usalo solo quando hai un motivo specifico.
# Verify which ports a host-networked container opened
ss -tlnp | grep node_exporter
# LISTEN 0 4096 *:9100 *:* users:(("node_exporter",pid=12345,fd=3))
Fai attenzione: questo output mostra che il processo ascolta su *:9100, ovvero tutte le interfacce. Con la rete bridge, controlli questo tramite il flag -p. Con la rete host, è l'applicazione stessa a decidere a cosa collegarsi.
Cos'è la rete macvlan e ti serve su un VPS?
Macvlan assegna a ogni container il proprio indirizzo MAC, facendolo apparire come un dispositivo fisico separato sulla rete. Il container ottiene un IP reale dalla sottorete della tua LAN ed è raggiungibile direttamente senza mapping di porte.
Su un VPS, quasi certamente non hai bisogno di macvlan. Ecco perché:
- La maggior parte dei provider cloud lo blocca. Macvlan richiede che la NIC fisica operi in modalità promiscua. Gli hypervisor VPS tipicamente bloccano questo a livello di switch virtuale.
- Nessuna LAN a cui unirsi. Il tuo VPS ha un'unica interfaccia pubblica. Macvlan è progettato per ambienti dove vuoi che i container abbiano indirizzi propri su una LAN esistente, come un laboratorio domestico con dispositivi IoT o applicazioni legacy che si aspettano un IP dedicato.
- La comunicazione host-container si interrompe. Per design, il kernel Linux impedisce a un container macvlan di comunicare con l'host su cui gira. Servono workaround come collegare una seconda rete bridge.
Se il tuo provider VPS ti fornisce un server dedicato con accesso NIC completo e più IP, macvlan diventa praticabile. Per i deployment VPS standard, resta sulle reti bridge personalizzate.
Per riferimento, creare una rete macvlan appare così (probabilmente non ne avrai bisogno su un VPS):
docker network create -d macvlan \
--subnet=192.168.1.0/24 \
--gateway=192.168.1.1 \
-o parent=eth0 \
macnet
L'opzione parent specifica l'interfaccia host a cui collegarsi. Il container ottiene il proprio IP dalla sottorete e il proprio indirizzo MAC. Gli altri dispositivi sulla LAN possono raggiungerlo direttamente.
Qual è la differenza tra esporre e pubblicare una porta Docker?
EXPOSE in un Dockerfile è documentazione. Dichiara su quale porta ascolta l'applicazione. Non apre quella porta verso l'host o l'esterno. --publish (o -p) a runtime crea un mapping di porta effettivo dall'host al container. Senza -p, nessun traffico esterno raggiunge il container.
# In your Dockerfile - this is documentation only
EXPOSE 8080
# This actually maps host:8080 -> container:8080
docker run -d -p 8080:8080 myapp
# This binds to localhost only - external traffic cannot reach it
docker run -d -p 127.0.0.1:8080:8080 myapp
Il pattern di binding 127.0.0.1
Quando esegui servizi dietro un reverse proxy come Nginx, Traefik o Caddy , pubblica le porte dei container solo su 127.0.0.1. Questo impedisce l'accesso diretto da internet, forzando tutto il traffico attraverso il reverse proxy dove avvengono la terminazione TLS, il rate limiting e il controllo degli accessi.
# docker-compose.yml - binding to localhost only
services:
app:
image: myapp:latest
ports:
- "127.0.0.1:3000:3000" # Only reachable from localhost
Verifica il binding:
ss -tlnp | grep 3000
# LISTEN 0 4096 127.0.0.1:3000 0.0.0.0:* users:(("docker-proxy",...))
Fai attenzione: l'output mostra 127.0.0.1:3000, non 0.0.0.0:3000. il traffico esterno sull'IP pubblico del tuo VPS non può raggiungere la porta 3000 direttamente.
Senza il prefisso 127.0.0.1, Docker pubblica su tutte le interfacce di default. Su un VPS con IP pubblico, il tuo servizio è esposto su internet, bypassando il reverse proxy e potenzialmente il firewall.
Riferimento rapido pubblicazione porte
| Sintassi | Si collega a | Accessibile da |
|---|---|---|
-p 8080:80 |
0.0.0.0:8080 + [::]:8080 |
Ovunque (IPv4 + IPv6) |
-p 127.0.0.1:8080:80 |
127.0.0.1:8080 |
Solo localhost |
-p 0.0.0.0:8080:80 |
0.0.0.0:8080 |
Tutte le interfacce IPv4 |
-p [::1]:8080:80 |
[::1]:8080 |
Solo localhost IPv6 |
Come isolare le reti dei container su un singolo VPS?
Crea reti bridge separate per ogni stack applicativo. I container su reti diverse non possono comunicare a meno che non li connetti esplicitamente a una rete condivisa. Per database e servizi interni, usa il flag internal: true per bloccare qualsiasi accesso internet in uscita.
Architettura multi-rete
Una configurazione di produzione tipica su un VPS appare così:
Internet
|
[Reverse Proxy]
/ \
[frontend] [api-net]
| |
webapp api-server
|
[db-net: internal]
|
postgres
Il reverse proxy è connesso a frontend e api-net. Il server API è connesso a api-net e db-net. PostgreSQL si trova solo su db-net, senza route verso internet.
Ecco il file Docker Compose per questa configurazione:
services:
proxy:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
networks:
- frontend
- api-net
webapp:
image: mywebapp:latest
networks:
- frontend
api:
image: myapi:latest
environment:
- DATABASE_URL=postgresql://appuser:${DB_PASSWORD}@db:5432/appdb
networks:
- api-net
- db-net
db:
image: postgres:16-alpine
environment:
- POSTGRES_PASSWORD_FILE=/run/secrets/db_password
volumes:
- pgdata:/var/lib/postgresql/data
networks:
- db-net
networks:
frontend:
driver: bridge
api-net:
driver: bridge
db-net:
driver: bridge
internal: true # No internet access for the database network
volumes:
pgdata:
L'internal: true su db-net significa che PostgreSQL non può raggiungere internet. Non può scaricare aggiornamenti, contattare server remoti né essere sfruttato come pivot per connessioni in uscita. Il server API può raggiungere il database perché è collegato sia a api-net che a db-net.
Verificare l'isolamento di rete
Dopo aver avviato lo stack, conferma l'isolamento:
# List networks created by Compose
docker network ls --filter "name=myproject"
# Inspect a network to see which containers are connected
docker network inspect myproject_db-net --format '{{range .Containers}}{{.Name}} {{end}}'
# myproject-api-1 myproject-db-1
# Confirm the database cannot reach the internet
docker exec myproject-db-1 ping -c1 -W2 8.8.8.8
# ping: sendto: Network unreachable
Fai attenzione: "Network unreachable" conferma che il flag internal: true funziona. Il container del database non ha un gateway di default.
Come abilitare IPv6 per i container Docker?
Abilita IPv6 globalmente in /etc/docker/daemon.json, poi abilitalo per rete con una sottorete. Docker supporta il dual-stack (IPv4 + IPv6) nativamente su Linux. Il parametro ip6tables, abilitato di default, gestisce le regole firewall IPv6 per i container.
Passo 1: configurare il daemon
Modifica /etc/docker/daemon.json:
{
"ipv6": true,
"fixed-cidr-v6": "fd00:dead:beef::/64",
"ip6tables": true,
"default-address-pools": [
{ "base": "172.17.0.0/16", "size": 24 },
{ "base": "fd00:dead:beef::/48", "size": 64 }
]
}
fixed-cidr-v6 assegna una sottorete IPv6 /64 alla bridge di default. Il blocco default-address-pools indica a Docker come allocare le sottoreti per le nuove reti: blocchi /24 dal range IPv4 e blocchi /64 dal range IPv6.
Usa un prefisso ULA (fd00::/8) per il traffico privato tra container. Se il tuo VPS ha un range IPv6 pubblico assegnato dal provider, puoi usare una sottorete di quel range per i container che necessitano di indirizzi IPv6 pubblici.
Riavvia Docker:
sudo systemctl restart docker
Verifica che IPv6 sia abilitato:
docker network inspect bridge --format '{{.EnableIPv6}}'
# true
Passo 2: creare una rete dual-stack
docker network create --ipv6 --subnet 172.20.0.0/24 --subnet fd00:dead:beef:1::/64 app-v6
O in Docker Compose:
networks:
app-v6:
enable_ipv6: true
ipam:
config:
- subnet: 172.20.0.0/24
- subnet: fd00:dead:beef:1::/64
Passo 3: verificare la connettività dual-stack
docker run --rm --network app-v6 alpine ip -6 addr show eth0
# inet6 fd00:dead:beef:1::2/64 scope global
IPv6 è supportato solo sui daemon Docker che girano su Linux. Su sistemi più vecchi, potrebbe essere necessario caricare il modulo del kernel ip6_tables prima di creare reti IPv6:
sudo modprobe ip6_tables
Come definire le reti in Docker Compose?
Docker Compose crea automaticamente una rete di default per ogni progetto. Ogni servizio nel file Compose si unisce a questa rete e può risolvere gli altri servizi tramite il nome del servizio. Per la maggior parte delle applicazioni a stack singolo, questo comportamento di default è sufficiente.
Quando servono più reti per l'isolamento, definiscile esplicitamente nella sezione networks:
services:
web:
image: nginx:alpine
ports:
- "127.0.0.1:8080:80"
networks:
- public
app:
image: node:20-alpine
networks:
- public
- private
redis:
image: redis:7-alpine
networks:
- private
networks:
public:
driver: bridge
private:
driver: bridge
internal: true
In questo file, web può raggiungere app attraverso la rete public. app può raggiungere sia web che redis. redis può raggiungere solo app attraverso private e non ha accesso a internet.
Usare la rete host in Compose
services:
node-exporter:
image: prom/node-exporter:latest
network_mode: host
pid: host
restart: unless-stopped
network_mode: host sostituisce qualsiasi definizione networks. Non puoi combinare la modalità host con reti personalizzate sullo stesso servizio.
Collegarsi a reti esterne
Se una rete è stata creata fuori da Compose (da un altro stack o manualmente), referenziala come esterna:
networks:
shared-proxy:
external: true
Questo permette a più progetti Compose di condividere una rete. Un pattern comune: uno stack Compose esegue il reverse proxy e crea la rete. Gli altri stack la dichiarano esterna e vi collegano i propri servizi.
Comandi di rete Docker: riferimento rapido
| Comando | Cosa fa |
|---|---|
docker network ls |
Elenca tutte le reti |
docker network create mynet |
Crea una rete bridge |
docker network create --ipv6 --subnet fd00::/64 mynet |
Crea una rete dual-stack |
docker network inspect mynet |
Mostra i dettagli della rete (sottorete, container, opzioni) |
docker network connect mynet container1 |
Collega un container in esecuzione a una rete |
docker network disconnect mynet container1 |
Scollega un container da una rete |
docker network prune |
Rimuove tutte le reti inutilizzate |
docker network rm mynet |
Rimuove una rete specifica |
Limiti di scalabilità
Le reti bridge diventano instabili quando più di 1.000 container si connettono a una singola rete. È una limitazione del kernel Linux sul bridge device. Se esegui molti container, distribuiscili su più reti per funzione anziché mettere tutto su un'unica bridge.
Qualcosa non funziona?
I container non riescono a risolversi per nome.
Probabilmente sei sulla bridge di default. Crea una rete personalizzata e collegaci entrambi i container. Verifica con docker inspect <container> --format '{{json .NetworkSettings.Networks}}'.
La porta è pubblicata ma non raggiungibile dall'esterno.
Controlla se è collegata a 127.0.0.1 invece di 0.0.0.0. Esegui ss -tlnp | grep <port> sull'host. Controlla anche le regole del firewall.
Il container non riesce ad accedere a internet.
La rete potrebbe avere internal: true. Verifica con docker network inspect <network> --format '{{.Internal}}'. Se restituisce true, la rete blocca il traffico in uscita per design.
IPv6 non funziona nei container.
Verifica che ip6tables sia abilitato in daemon.json. Su kernel più vecchi, carica il modulo: sudo modprobe ip6_tables. Controlla che la rete abbia IPv6 abilitato: docker network inspect <network> --format '{{.EnableIPv6}}'.
"Address already in use" con la rete host.
Un altro processo (o container) sta già usando quella porta. Trovalo con ss -tlnp | grep <port>. La rete host non fa mapping di porte, quindi i conflitti sono diretti.
Prossimi passi
La tua rete container è configurata. Ecco cosa affrontare:
- Aggiungere un reverse proxy per gestire TLS e routing
- Correggere il bypass del firewall di Docker affinché le porte pubblicate rispettino le regole UFW/nftables
- Aggiungere limiti di risorse e healthcheck ai servizi Compose
- Tornare alla panoramica Docker su VPS per la checklist di produzione completa
Copyright 2026 Virtua.Cloud. Tutti i diritti riservati. Questo contenuto è un'opera originale del team Virtua.Cloud. La riproduzione, ripubblicazione o redistribuzione senza autorizzazione scritta è vietata.