Guida pratica alle reti Docker personalizzate e alla comunicazione tra container

Questa guida offre un'esplorazione pratica delle reti bridge Docker personalizzate e del loro ruolo nella comunicazione tra container. Impara a creare, gestire e connettere container utilizzando la CLI di Docker e Docker Compose. Scopri come le reti personalizzate abilitano la risoluzione DNS automatica, migliorano l'isolamento e semplificano la comunicazione tra servizi, portando ad applicazioni containerizzate più robuste e scalabili.

Guida pratica alle reti Docker personalizzate e alla comunicazione tra container

Le reti Docker personalizzate sono una di quelle funzionalità che sembrano opzionali finché non esegui più di un container. Il bridge predefinito può andare bene per un test rapido, ma un bridge definito dall'utente ti offre nomi di servizio prevedibili, un isolamento più pulito e un debug più semplice. Per una piccola applicazione con un container web, un container API e un database, la differenza è immediata: l'API può connettersi a db:5432 invece di inseguire l'IP che Docker ha assegnato oggi.

Questa guida si concentra sulle reti bridge definite dall'utente su un singolo host Docker. Le reti overlay, il networking di Kubernetes e il service discovery di Swarm risolvono problemi simili in configurazioni multi-host, ma la rete bridge rimane lo strumento quotidiano per lo sviluppo locale, le piccole distribuzioni e i progetti Docker Compose.

Perché il bridge predefinito diventa scomodo

Docker crea automaticamente una rete chiamata bridge. Se esegui container senza specificare una rete, di solito finiscono lì. Funziona per casi semplici, ma non è piacevole per applicazioni multi-container.

Su una rete bridge definita dall'utente, Docker fornisce DNS integrato per i nomi dei container e i nomi dei servizi Compose. Sul bridge predefinito, la scoperta basata sui nomi è limitata e il legacy linking non è un pattern su cui dovresti costruire. Il risultato pratico è che le reti personalizzate ti permettono di configurare le applicazioni con hostname stabili:

DATABASE_HOST=db
REDIS_HOST=redis
API_BASE_URL=http://api:3000

Questo è più facile da leggere, più facile da spostare tra macchine e meno fragile degli indirizzi IP dei container.

Le reti personalizzate creano anche un confine più chiaro. I container collegati alla stessa rete possono comunicare tra loro. I container su reti diverse non possono, a meno che non colleghi un container a entrambe o pubblichi le porte attraverso l'host. Questo non è un modello di sicurezza completo, ma è un utile livello di separazione.

Creare una rete con la CLI di Docker

Crea un bridge definito dall'utente:

docker network create app-net

Avvia due container su di essa:

docker run -d --name db --network app-net -e POSTGRES_PASSWORD=devpass postgres:16
docker run -d --name adminer --network app-net -p 8080:8080 adminer

Dal container adminer, il nome host del database è db. Non è necessario conoscere il suo IP.

Ispeziona la rete:

docker network inspect app-net

Vedrai il driver, la subnet, il gateway e i container collegati. Durante il debug, questo comando risponde a una domanda fondamentale: i due container sono effettivamente sulla stessa rete?

Puoi collegare un container esistente:

docker network connect app-net some-container

E scollegarlo:

docker network disconnect app-net some-container

Docker non rimuoverà una rete mentre ci sono ancora container collegati. Scollega o rimuovi prima i container:

docker network rm app-net

Le porte pubblicate sono diverse dalle porte da container a container

Una confusione comune: i container sulla stessa rete Docker non hanno bisogno di porte host pubblicate per comunicare tra loro. Le porte pubblicate sono per il traffico in entrata dall'host o dall'esterno dell'host.

Se un container API ascolta sulla porta 3000 e un container web è sulla stessa rete, il container web può chiamare:

http://api:3000

Hai bisogno di -p 3000:3000 solo se vuoi raggiungere l'API dal browser del tuo laptop o da un altro host attraverso l'host Docker.

Questo significa che il tuo database di solito non dovrebbe pubblicare una porta host in una configurazione simile alla produzione, a meno che qualcosa al di fuori di Docker non abbia bisogno di accesso diretto. Lascia che l'API raggiunga db:5432 attraverso la rete Docker privata.

Usa Compose per normali app multi-servizio

Docker Compose crea una rete predefinita per il progetto anche se non ne definisci una. I servizi possono raggiungersi a vicenda tramite il nome del servizio:

services:
  web:
    image: nginx:latest
    ports:
      - "8080:80"
    depends_on:
      - api

  api:
    image: my-api:latest
    environment:
      DATABASE_HOST: db

  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: devpass

In quel file, api può raggiungere db usando l'hostname db. web può raggiungere api usando l'hostname api, assumendo che la configurazione a livello di applicazione punti lì.

Puoi anche definire reti con nome quando vuoi un'intenzione o una separazione più chiara:

services:
  web:
    image: nginx:latest
    ports:
      - "8080:80"
    networks:
      - frontend

  api:
    image: my-api:latest
    environment:
      DATABASE_HOST: db
    networks:
      - frontend
      - backend

  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: devpass
    networks:
      - backend

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge

Qui, web non può parlare direttamente con db perché non condividono una rete. api è il ponte tra i due livelli applicativi. Questa è una forma utile per servizi reali: esponi solo il servizio di bordo all'host, mantieni il database privato e collega ogni servizio solo dove deve comunicare.

depends_on non è prontezza

depends_on di Compose controlla l'ordine di avvio nell'uso comune di Compose, ma non garantisce che il database sia pronto ad accettare connessioni. La tua API potrebbe avviarsi dopo che il processo del container db è partito e fallire comunque perché PostgreSQL si sta inizializzando.

Gestisci la prontezza nell'applicazione con tentativi, o usa un health check e una configurazione Compose che rispetti lo stato di salute del servizio per la tua versione di Compose e il tuo flusso di lavoro. Anche in questo caso, la logica di retry a livello di applicazione è ancora l'abitudine più affidabile perché i database possono riavviarsi dopo l'avvio iniziale.

Una configurazione API pratica usa DATABASE_HOST=db e ritenta la connessione per un breve periodo prima di uscire con un errore chiaro.

Le subnet personalizzate sono utili, ma non abusarne

Puoi scegliere una subnet:

docker network create --subnet 172.28.0.0/16 app-net

In Compose:

networks:
  backend:
    driver: bridge
    ipam:
      config:
        - subnet: 172.28.0.0/16

Questo aiuta quando la subnet automatica di Docker si sovrappone a una VPN, una rete aziendale o un'altra route sull'host. Non è necessario per la maggior parte dei progetti. Hardcodare gli IP dei container dovrebbe essere raro; i nomi dei servizi sono di solito il contratto migliore.

Risolvere i problemi di comunicazione di rete

Quando un container non riesce a raggiungerne un altro, controlla in questo ordine:

  1. Entrambi i container sono in esecuzione?
  2. Sono collegati alla stessa rete?
  3. Il client sta usando il nome del servizio/container, non localhost?
  4. Il server sta ascoltando sulla porta e interfaccia previste?
  5. La porta è pubblicata solo quando è necessario l'accesso dall'host?

L'errore localhost è particolarmente comune. All'interno di un container, localhost significa quello stesso container, non l'host Docker e non un altro servizio. Se l'API prova a connettersi a localhost:5432, sta cercando PostgreSQL all'interno del container API. Usa db:5432 quando il servizio database si chiama db.

Ispeziona le reti:

docker network inspect app-net

Esegui un container diagnostico temporaneo sulla stessa rete:

docker run --rm -it --network app-net alpine sh

All'interno, installa o usa gli strumenti disponibili secondo necessità:

getent hosts db
nc -vz db 5432

Le immagini minime potrebbero non avere nc, curl o strumenti DNS installati. Un container di debug di breve durata è spesso più pulito che aggiungere pacchetti di risoluzione dei problemi all'immagine della tua applicazione.

Un pattern predefinito sensato

Per la maggior parte delle app su singolo host, usa Compose e lascia che crei la rete del progetto. Aggiungi reti esplicite quando hai bisogno di separazione, come frontend e backend. Usa i nomi dei servizi per il traffico interno. Pubblica solo le porte che gli umani, i proxy inversi o i sistemi esterni devono raggiungere.

Questo ti dà una configurazione facile da spiegare:

  • Il browser raggiunge localhost:8080 perché web pubblica una porta.
  • web raggiunge api attraverso la rete Docker.
  • api raggiunge db attraverso la rete backend.
  • db non ha una porta host a meno che non ci sia una reale ragione operativa.

Le reti Docker personalizzate non sono solo una bella funzionalità. Sono la differenza tra container che casualmente girano sulla stessa macchina e servizi che hanno un modello di comunicazione chiaro.

Gli alias di rete possono facilitare le migrazioni

A volte un'applicazione si aspetta un hostname che non vuoi usare come nome del servizio Compose. Puoi aggiungere un alias su una rete:

services:
  postgres:
    image: postgres:16
    networks:
      backend:
        aliases:
          - database

networks:
  backend:
    driver: bridge

I container su backend possono ora raggiungere il servizio come postgres o database. Questo è utile quando si migra un'app più vecchia che usa già DATABASE_HOST=database, ma non userei alias ovunque. I nomi dei servizi sono più semplici quando controlli la configurazione dell'applicazione.

L'accesso all'host è un problema separato

Un container che parla con un altro container è diverso da un container che parla con l'host Docker. Su Docker Desktop, host.docker.internal è comunemente disponibile. Su Linux, il supporto dipende dalla versione di Docker e dalla configurazione; molti team lo aggiungono esplicitamente quando necessario:

docker run --add-host=host.docker.internal:host-gateway ...

Usalo con parsimonia. Se un container dipende fortemente da servizi in esecuzione direttamente sull'host, la tua configurazione potrebbe diventare più difficile da riprodurre in CI o sulla macchina di un altro sviluppatore. Per database e cache, eseguire la dipendenza come un altro servizio sulla stessa rete Docker è di solito più pulito.

Le porte interne dovrebbero corrispondere al processo, non al commento del Dockerfile

Il networking Docker non si preoccupa di ciò che dice una riga EXPOSE del Dockerfile a meno che gli strumenti non la usino come metadati. L'applicazione deve effettivamente ascoltare sulla porta che chiami. Se un'app Node ascolta sulla 3000, gli altri container dovrebbero usare api:3000 anche se qualcuno ha scritto EXPOSE 8080 per errore.

Controlla anche l'indirizzo di bind. Un servizio in ascolto su 127.0.0.1 all'interno del suo container potrebbe non essere raggiungibile da altri container. Per il traffico da container a container, il processo di solito deve ascoltare su 0.0.0.0 o sull'interfaccia di rete del container.

Mantieni il design della rete noioso

È tentante creare molte reti perché la funzionalità esiste. Inizia con i percorsi di comunicazione di cui hai effettivamente bisogno. Una piccola app potrebbe aver bisogno solo della rete Compose predefinita. Un'app web più realistica potrebbe aver bisogno di frontend e backend. Oltre a ciò, ogni nuova rete dovrebbe avere una ragione che qualcuno possa spiegare durante un incidente.

Un buon design di rete rende la risoluzione dei problemi più facile. Quando web non riesce a raggiungere db, e sai che intenzionalmente non condividono una rete, la risposta è architetturale piuttosto che misteriosa. Quando ogni servizio è collegato a ogni rete, la rete non documenta più nulla.

Una revisione realistica prima di rilasciare

Prima di considerare finito uno script o una configurazione di container, leggilo una volta come se fossi la prossima persona che dovrà fare debug alle 2 di notte. Questo cambia ciò che noti. Un prompt che aveva senso mentre scrivevi lo script potrebbe essere ambiguo quando appare in un log CI. Un nome di servizio Docker che sembrava ovvio potrebbe non corrispondere al nome della variabile nell'applicazione. Un valore predefinito di Bash potrebbe essere sicuro per lo sviluppo e pericoloso per la produzione.

Mi piace fare una breve prova a secco con valori deliberatamente scomodi. Usa un percorso con spazi. Usa un valore opzionale vuoto. Prova un nome file che inizia con un trattino. Esegui lo script da una directory di lavoro diversa. Avvia il container senza una variabile d'ambiente prevista. Questi test non sono fantasiosi, ma catturano le supposizioni che di solito si rompono per prime.

Controlla anche il messaggio di errore. Se l'unico output è fallito, il consiglio dell'articolo non è stato implementato. Un errore utile dice quale valore è stato usato, quale controllo è fallito e cosa l'operatore può cambiare. Questo non significa scaricare ogni variabile d'ambiente o stampare segreti. Significa essere specifici dove la specificità aiuta: il percorso di configurazione, il nome del comando mancante, il nome della rete, l'hostname del servizio o la porta che il processo ha tentato di associare.

L'abitudine finale è mantenere gli esempi vicini al modo in cui il sistema viene effettivamente eseguito. Se la produzione usa Compose, testa con Compose. Se uno script è avviato da systemd, testalo con systemd o con un ambiente altrettanto minimo. Se un comando dovrebbe essere sicuro per copia e incolla, includi le virgolette, i separatori -- e la validazione nell'esempio stesso. I lettori copiano pattern funzionanti più spesso di quanto copino avvertimenti.

Quella revisione non è burocrazia. È come la piccola automazione rimane noiosa. Noioso è ciò che vuoi da prompt di shell, caricatori di configurazione, espansione di variabili, diagnostica dei container e networking Docker. Meno sorprendente è il comportamento, più facile è per il prossimo operatore fidarsi.

Per il networking Docker, documenta il percorso del traffico previsto accanto al file Compose o nel README del servizio. Una breve nota come web -> api:3000 -> db:5432 previene molta confusione. Rende anche le revisioni più facili: se qualcuno pubblica la porta del database o collega web alla rete backend, la modifica deve giustificarsi rispetto al percorso previsto.

Quando l'app cresce, rivisita la mappa di rete. Vecchi alias, porte pubblicate inutilizzate e servizi collegati a reti di cui non hanno più bisogno sono fonti silenziose di rischio operativo.