Block Image

I container sono effimeri

Come visto nel primo articolo su Docker (Primi passi con Docker), i container sono dei processi leggeri e isolati. Essi inoltre sono effimeri: vengono creati, cancellati, riavviati, cambiano indirizzo IP, etc.

Quando cancelliamo un container e poi lo ricreiamo, perdiamo tutte le modifiche fatte precedentemente.
Questo perché di default, Docker non mantiene la persistenza dei container.
L'ideale sarebbe che le nostre applicazioni containerizzate fossero stateless. Le stesse però dovrebbero sfruttare uno strumento per la persistenza dei dati, come un database, un sistema di messaggistica come Kafka.
Bene, e se volessimo containerizzare anche questi tool di persistenza? Sarebbe inutile creare un container postgres se poi non si potesse mantenere la persistenza del suo filesystem vero?

Bind Mount & Volumi

Docker offre due opzioni per permettere la persistenza dei filesystem dei container: bind mount e volumi.

Il bind mount permette di montare una directory dell'host di Docker all'interno di un container.

I volumi invece sono creati e gestiti da Docker. Possono essere creati esplicitamente eseguendo comando
docker volume create <volume_name>
oppure possono essere creati da Docker al momento della creazione di un container. I dati all'interno del volume sono archiviati dentro una directory dell'host di Docker.
Un volume può essere condiviso da più container.

Quando possibile, è da preferite la tecnica dei volumi piuttosto del bind mount, essendo quest'ultima poco portabile (dipende dal sistema operativo dell'host di Docker, ad esempio il filesystem di Windows è diverso da quello di Linux, pertanto non puoi utilizzare esattamente lo stesso path, e di conseguenza lo stesso comando, sui due sistemi operativi differenti).

Vediamo nel dettaglio queste due tecniche.

Bind Mount

Come detto poc'anzi, con la tecnica del bind mount, possiamo montare una cartella del filesystem della macchina host sul filesystem del container.
Ad esempio creiamo una pagina html chiamata index.html sulla nostra macchina:

<!DOCTYPE html>
<html>
    <head>
        <title>Welcome to nginx!</title>
        <style>
            html { color-scheme: light dark; }
            body { width: 35em; margin: 0 auto;
                font-family: Tahoma, Verdana, Arial, sans-serif; }
        </style>
    </head>
    <body>
        <h1>Welcome to my custom page!</h1>
        <p>If you see this page, the nginx web server is successfully installed and
            working. Further configuration is required.</p>
    </body>
</html>

Ora creiamo un container NGINX che utilizza la pagina html appena creata, con la tecnica del bind mount:
docker run --name=nginx -d -p 8080:80 -v /Users/vracca/html:/usr/share/nginx/html nginx

Analizziamo il comando appena eseguito:

  1. Con --name indichiamo a Docker qual è il nome che assegniamo al container.
  2. Con -d avviamo il container in detached mode, quindi in background.
  3. Con -p 8080:80 creiamo un tunnel di rete mappando la porta 80 del container con la porta 8080 del nostro host.
  4. Con -v /Users/vracca/html:/usr/share/nginx/html indichiamo a Docker che vogliamo creare uno strato di persistenza, montando il contenuto della directory del nostro host (path prima dei due punti), sul filesystem del container (path dopo i due punti). In pratica stiamo dicendo: "nella cartella html del container, metti il contenuto della cartella html dell'host".
  5. Infine, nginx è il nome dell'immagine su cui vogliamo creare il container. Non indicando un tag, verrà presa di default l'immagine con tag latest.

Eseguendo una cURL, possiamo vedere che la pagina di default di NGINX è quella effettivamente creata sul nostro host:

Block Image

Volumi

Supponiamo adesso che vogliamo un database postgres containerizzato. Eseguiamo il seguente comando che scarica la versione 13.5 dell'immagine ufficiale di postgres e crea un container di quest'ultima:
docker run --name postgres -e POSTGRES_USER=user -e POSTGRES_PASSWORD=password -e POSTGRES_DB=mydb -p 5432:5432 -d postgres:13.5
Creiamo anche un container del tool pgAdmin, che è un frontend di postgres:
docker run --name=pgadmin -e PGADMIN_DEFAULT_PASSWORD=user -e PGADMIN_DEFAULT_EMAIL=admin@admin.com -p 5050:80 -d dpage/pgadmin4

Dal browser, andiamo su localhost:5050 per accedere a pgAdmin, ed eseguiamo l'accesso con mail e password settati durante la creazione del container pgAdmin:

Block Image

Ora colleghiamo pgAdmin col database postgres del container. Andiamo su Servers, clicchiamo col tasto destro e andiamo su Create. Da qui inseriamo il nome del db, l'host, user e password del database postgres settati al momento della creazione del container postgres:

Block ImageBlock Image

Clicchiamo infine su Save.

Nota: Nel campo Host name/address inserite l'IP della vostra macchina locale e non localhost, poiché per il container, localhost equivale al suo indirizzo IP, non a quello macchina host.

Ora andiamo sul nostro database e creiamo una tabella (tasto destro su public e clicchiamo su CREATE Script):

Block Image

create table EMPLOYEES (
	id serial primary key,
	name varchar( 50)
);

insert into EMPLOYEES(name) values ('Mario');
commit;

Abbiamo creato la tabella EMPLOYEES con una riga. Eseguendo una select possiamo verificare che la riga è stata correttamente inserita:

select * from employees;

Bene, ora applichiamo un caso d'uso comune: supponiamo di voler aggiornare la nostra versione di postgres alla 14.1.
Stoppiamo e cancelliamo il container di postgres:
docker stop postgres && docker rm postgres
Adesso creiamo nuovamente il container di postgres ma con la versione dell'immagine 14.1:
docker run --name postgres -e POSTGRES_USER=user -e POSTGRES_PASSWORD=password -e POSTGRES_DB=mydb -p 5432:5432 -d postgres:14.1

Eseguiamo di nuovo l'accesso al database da pgAdmin; andando nella sezione public, non troviamo più il nostro database!

Questo è il comportamento atteso: abbiamo cancellato un container, quindi anche il suo filesystem.
Utilizziamo allora i volumi di Docker! Cancelliamo il container di postgres come fatto precedentemente.
Ora creiamo un volume:
docker volume create pgdata

Creiamo quindi nuovamente il container postgres, indicando questa volta che deve essere utilizzato il volume pgdata:
docker run --name postgres -e POSTGRES_USER=user -e POSTGRES_PASSWORD=password -e POSTGRES_DB=mydb -p 5432:5432 -v pgdata:/var/lib/postgresql/data -d postgres:14.1

Da notare che il comando per utilizzare un volume, durante la crezione del container, è uguale a quello del bind mount. La differenza sta nell'indicare, come primo parametro prima dei due punti, il nome del volume invece che un path.

Accediamo nuovamente a pgadmin e creiamo la tabella con una riga, come fatto in precedenza.

Per verificare la persistenza, cancelliamo il container e lo ricreiamo. Potete vedere, accedendo a pgAdmin, che il database, con i suoi dati, è ancora presente.

Come faccio a sapere qual è la directory dell'host dove è montato il volume

Facile, con il comando docker inspect sul volume appena creato!

Block Image

Come possiamo vedere dalla figura, l'inspect sul volume permette di visualizzare informazioni su quest'ultimo, tra cui il path dove è stato montato sull'host.
Questo è molto utile quando ad esempio vogliamo trasferire il container e il suo volume su un altro host: ci basta copiare la cartella pgdata sul nuovo host Docker per avere tutti i dati del volume.

Conclusioni

In questo articolo abbiamo visto come rendere persistente un container Docker con la tecnica dei volumi e del bind mount. Come già detto, è preferibile utilizzare il primo metodo, essendo più portabile. Rendendo i container persistenti, possiamo containerizzare anche applicazioni stateful e tool come database, broker di messaggistica.

Articoli su Docker: Docker