Block Image

Di default i container Docker possono comunicare col mondo esterno, ma non viceversa.
Per esporre la porta di un container col mondo esterno, possiamo effettuare un mapping tramite la flag -p quando si crea un container. Ad esempio col comando:
docker run --name nginx -d -p 8080:80 nginx
stiamo creando un container nginx e stiamo mappando la porta 8080 del Docker host con la porta 80 del container.

Ma come funziona la comunicazione tra container?

Il comando network e le reti di default di Docker

Il comando docker network permette di gestire le reti in Docker. In particolare, eseguendo il comando docker network --help possiamo vedere quali comandi sono messi a disposizione:

Block Image

I comandi a disposizione sono pochi e chiari.
Proviamo a visualizzare la lista di reti con il comando
docker network ls

Block Image

Di default, la lista contiene tre reti:

  1. la rete bridge, chiamata docker0, è la rete che i container utilizzano di default. I container sono collegati quindi alla stessa rete e pertanto possono comunicare tra loro. Inoltre questo offre anche un isolamento verso il mondo esterno e verso tutti i container che non fanno parte di questa rete
  2. la rete host, non è una vera e propria rete; un container che fa parte di questa rete, si collega in pratica alla rete del Docker Host (funzionalità non disponibile su Docker Desktop)
  3. la rete none anch'essa non è una vera e propria rete; non collega il container a nessuna rete.

Se creiamo quindi due container NGINX, ad esempio eseguendo i comandi:
docker run --name nginx1 -d nginx
docker run --name nginx2 -d nginx
possiamo notare, col comando docker inspect nginx1 e docker inspect nginx2 che fanno parte della stessa rete.
Infatti entrando nel container nginx2, col comando docker exec -it nginx2 bash, possiamo eseguire con successo una cURL sull'Ip del container nginx1:

Block Image

Anche se i due container fanno parte della stessa rete e quindi possono "pingarsi" a vicenda, questo non risolve un caso d'uso comune: ad esempio un container di un'applicazione web che deve comunicare con un container che fa da database. Essendo i container effimeri, ed essendo che quando riavviamo un container questo può cambiare IP, non possiamo far comunicare l'applicazione web col database tramite l'indirizzo IP di quest'ultimo, non essendo fisso.
Nel paragrafo seguente, vedremo come risolvere questo problema.

Nel paragrafo Persistenza nei container Docker: volumi e bind mount, abbiamo creato due container per vedere la funzionalità dei volumi: un container postgres e un container pgAdmin.
Il container postgres è stato creato col seguente comando:
docker run --name postgres -e POSTGRES_USER=user -e POSTGRES_PASSWORD=password -e POSTGRES_DB=mydb -p 5432:5432 -d postgres:13.5
La flag -p permette di mappare una porta del Docker Host con una porta del container. Perché l'abbiamo utilizzata? Per far sì che il container pgAdmin possa comunicare col database.
Il container pgAdmin è stato creato col seguente comando:
docker run --name=pgadmin -e PGADMIN_DEFAULT_PASSWORD=user -e PGADMIN_DEFAULT_EMAIL=admin@admin.com -p 5050:80 -d dpage/pgadmin4
Anche qui creiamo una mappatura di porte: mappiamo la porta 5050 del nostro host con la porta 80 del container.
Così facendo, possiamo accedere a pgAdmin da localhost:5050.

In realtà noi accediamo al database di postgres utilizzando esclusivamente pgAdmin: l'ideale sarebbe evitare la mappatura -p 5432:5432 ed inserire direttamente l'hostname del container di postgres. Inoltre, se eliminassimo quella mappatura, eviteremmo di esporre il database di postgres verso l'esterno, aggiungendo un grado di sicurezza in più. Vediamo come fare!

Una feature legacy di Docker è quella di collegare i container tramite la flag --link:
--link <name or id>:alias
dove name è il nome di un container e alias un alias per il nome del collegamento, oppure:
--link <name or id>
in questo caso l'alias sarà uguale al nome del container.
Questa flag permette, durante la creazione di un container col comando docker run, di collegare quest'ultimo ad un determinato container. Ma cosa si intende per "collegare"? Vediamolo con un esempio!
Distruggiamo e ricreiamo il container nginx2:
docker stop nginx2 && docker rm nginx2 && docker run --name nginx2 -d --link nginx1 nginx.
Ora proviamo nuovamente ad entrare nel container nginx2 e questa volta eseguiamo una cURL in questo modo:

Block Image

Stavolta il container può effettuare un cURL utilizzando direttamente il nome del container linkato, cioè nginx1.
Ed è proprio questo quello che fa la flag --link. Se eseguiamo il comando cat sul file hosts del container nginx2, possiamo vedere che è stato inserito da Docker la mappatura ip_nginx1->nginx1:

Block Image

Inoltre, sono state aggiunte anche diverse variabili di ambiente che indicano varie informazioni sul container nginx1:

Block Image

Notiamo che l'IP del container nginx1 è 172.17.0.2. Ma cosa succederebbe se spegnessimo il container nginx1, creassimo un container nginx3 e poi avviassimo nuovamente il container nginx1? Vediamo!

Block Image

Il container nginx1 ha cambiato IP! Sarà un problema ora per il container nginx2?

Block Image

Ovviamente nessun problema! Docker ha aggiornato anche l'IP di nginx1 su nginx2. Altrimenti non ci sarebbe alcun vantaggio a utilizzare questa flag.

Allo stesso modo quindi, possiamo collegare il container di postgres con quello di pgAdmin, senza avere la necessità di esporre il database verso l'esterno.

Tuttavia, con questa flag vengono condivise anche variabili di ambiente tra i container, che non è una buona cosa.
Comunque, questa flag è stata deprecata a favore delle network custom, quindi il suo uso è da evitare.

Creazione di network custom

È possibile create network custom col comando docker network create <network_name>.
Due container facenti parte della stessa rete custom, saranno in grado di comunicare tra loro e saranno isolati dalle altre reti Docker, compresa quella di default, la docker0.

Block Image

La figura sopra, presa dalla documentazione ufficiale di Docker, mostra come la rete docker0 abbia un indirizzo di rete diverso dalla rete custom my_bridge.

Alcune differenze tra la rete bridge di default, docker0 e una rete bridge custom:

  1. I container che fanno parte della rete docker0 possono accedere a ogni altro container della stessa rete esclusivamente tramite indirizzo IP (a meno che non si utilizzi --link).
    Un container in una rete custom pùo comunicare con altri container della stessa rete custom tramite i loro nomi o alias.
  2. Se non specificata la flag --network durante la creazione dei container, questi faranno parte della rete docker0. Di default quindi i container non sono isolati tra loro.
    Specificando una rete custom, solo i container della stessa rete custom possono comunicare tra loro.

Creiamo una rete chiamata nginx_net: docker network create nginx_net.
Eseguiamo un inspect su di essa:

docker network inspect nginx_net
[
    {
        "Name": "nginx_net",
        "Id": "0165d657ff0af159642158c5adf0c4f171eeae00376dd69fcae2b22e4ec4ffb4",
        "Created": "2022-01-15T14:52:11.414646094Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "172.18.0.0/16",
                    "Gateway": "172.18.0.1"
                }
            ]
        },
...

Nel campo subnet vediamo subito che la rete è 172.18.0.0 che è diversa da quella di docker0, che nel mio caso è 172.17.0.0.
Volendo, possiamo anche impostare la subnet durante creazione della rete:

docker network rm nginx_net
docker network create --subnet 10.100.0.0/16 nginx_net

Creiamo ora nuovamente i container nginx1 e nginx2, questa volta utilizzando per entrambi la rete nginx_net, con la flag --network <network_name>

Block Image

In questo modo, i container creati sono sulla stessa rete ed inoltre sono isolati dagli altri container che non fanno parte di quest'ultima.
Se proviamo nuovamente a eseguire il comando curl nginx1 dal container nginx2, possiamo verificare che funziona senza problemi.

Quindi il modo giusto per far comunicare il container di postgres con quello di pgAdmin, è:

  1. creare una rete custom
  2. creare il container postgres utilizzando la rete custom
  3. create il container pgADmin utilizzando la rete custom.

A quel punto, nella schermata di pgAdmin dove si setta la connessione al db, possiamo inserire come hostname il nome del container di postgres.

Accenno sul networking avanzato di Docker

Quando abbiamo visualizzato la lista di reti disponibili, oltre alla colonna NAME, c'era anche una colonna chiamata DRIVER.
Il networking di Docker viene creato utilizzando i driver. Come abbiamo già visto, Docker crea di default tre reti che utilizzano i driver bridge, host e none.

Il driver bridge permette di creare reti virtuali private all'interno dello stesso demone Docker.
Le reti custom create col comando docker network create utilizzano di default il driver bridge.

È possibile però far comunicare container anche di differenti demoni Docker, utilizzando il driver overlay.

Block Image

È possibile anche creare reti gestendo direttamente indirizzi MAC dei container tramite il driver macvlan.

Block Image

Se vuoi approfindire questi due driver, questi sono i link della docs ufficiale:
https://docs.docker.com/network/overlay/ e https://docs.docker.com/network/macvlan/

Conclusioni

In questo articolo abbiamo visto come funziona il networking in Docker e come far comunicare due (o più) container.
Abbiamo visto l'uso della flag --link, che è però deprecata ed è da evitare. Abbiamo visto come creare una rete custom e come collegare i container a quest'ultima con la flag --network. Questo è l'approccio consigliato da utilizzare.

Docs ufficiale su --link: https://docs.docker.com/network/links/.
Docs ufficiale sul networking in Docker: https://docs.docker.com/network/.
Miei articoli su Docker: Docker

Libri consigliati su Docker e Kubernetes: