Tutorial > Primi passi con Docker

Primi passi con Docker

Pubblicato il: 11 maggio 2021

container Docker immagine registry repository tag

Docker: concetti fondamentali

Per entrare nel mondo Docker è opportuno introdurre due concetti fondamentali: immagini e container. Come abbiamo affermato nei precedenti capitoli Docker è uno strumento che entra in gioco nel rilascio di software e applicazioni in aiuto a sviluppatori e sistemisti. In sostanza un'immagine rappresenta la fotografia di un'applicazione o di un servizio: uno stampo o modello che può essere replicato in varie istanze. L'immagine include al suo interno il codice dell'applicazione e le necessarie dipendenze.

Di per sé l'immagine non è eseguibile ma deve essere istanziata creando un container: un ambiente di esecuzione isolato al cui interno la nostra applicazione viene lanciata. A partire da un'immagine, possono essere generati vari (potenzialmente infiniti) container identici tra loro.

Le immagini sono rappresentazioni statiche: possono essere costruite, salvate, si può fare pull e push di un'immagine...

I container sono vivi e legati all'esecuzione: vengono creati, interrotti, rilanciati, uccisi.

Il nostro primo container

Iniziamo a fare un po' di pratica.

Gli esempi riportati in questo capitolo fanno riferimento a Docker installato sulla Cloud di Aruba. Per attivare un VPS su cui esercitarti visita la pagina Cloud VPS.

Gli esempi sono comunque riproducibili da terminale su qualsiasi piattaforma in cui sia installato Docker.

Apriamo una connessione SSH al nostro server su Aruba Cloud e lanciamo i seguenti comandi:

root@server-prova:~# docker pull hello-world

Using default tag: latest
latest: Pulling from library/hello-world
b8dfde127a29: Pull complete
Digest: sha256:308866a43596e83578c7dfa15e27a73011bdd402185a84c5cd7f32a88b501a24
Status: Downloaded newer image for hello-world:latest
docker.io/library/hello-world:latest

root@server-prova:~# docker run hello-world

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/

L'effetto finale è semplicemente la stampa a schermo di un messaggio di benvenuto all'utente.
Ma cos'è successo nel dettaglio? Vediamo le operazioni compiute da Docker:

  • con il comando docker pull hello-world, l'immagine hello-world:latest viene scaricata dal registry pubblico Docker Hub;
  • il comando docker run hello-world genera, a partire dall'immagine, un nuovo container isolato;
  • l'eseguibile interno al container produce il messaggio di benvenuto;
  • dopo che il processo principale interno al container si è esaurito, l'esecuzione del container si arresta.

Questo piccolo esempio mostra poco della potenza e dell'utilità di Docker, ma ci aiuta a familiarizzare con le meccaniche di questo strumento, che approfondiremo a breve.

Le immagini: Registry, repository e tag

Nell'esempio precedente, abbiamo scaricato da Docker Hub l'immagine hello-world:latest.
Docker hub, fornito da Docker Inc., è un registry, ossia un archivio che ospita immagini Docker. Nel registry pubblico, sono disponibili immagini ufficiali di sistemi operativi, framework e applicazioni di ogni tipo. Docker hub mette a disposizione anche un registry privato dove ogni utente Docker può stoccare immagini delle proprie applicazioni; esistono anche altre soluzioni per avere un registry privato, locale o in cloud.

Alcune immagini disponibili nel registry pubblico del Docker hub

Nel mondo Docker, ci riferiamo alle immagini usando il formato repository:tag. Un repository raccoglie diverse versioni di una stessa immagine; la versione è indicata dal tag.
Tornando all'esempio precedente, noi non abbiamo esplicitato il tag e Docker ha assunto di default il tag latest. Lavorando con Docker, è buona pratica specificare il nome del tag.

Per convincerci di questo aspetto, vediamo alcuni dei tag disponibili per il repository Docker del diffuso linguaggio di programmazione Python: latest, 3.10.0a4-buster, 3.9.1-alpine3.12, 3.8.7-windowsservercore-1809, 3.6.12-slim-stretch

In questo caso, i tag sono molti e differenti fra loro, l'immagine corrispondente al tag latest può cambiare frequentemente e senza preavviso.

Specificare il tag ci assicura che stiamo lavorando con una versione specifica e replicabile. Viceversa, l'uso indiscriminato del tag latest può portare nel tempo all'insorgere di problemi di incompatibilità.

Un modo ancora più preciso ed univoco per riferirci ad una specifica immagine è l'uso del digest (308866a4... nell'esempio della sezione precedente).

Struttura di un immagine Docker

Struttura di due immagini Docker, composte da vari layer (alcuni possono essere comuni)

Tornando sul concetto di immagine, essa consiste in un template statico, contenente le istruzioni per istanziare container; all'interno di questi, eseguiamo un'applicazione/un servizio. Esaminiamo meglio come sono strutturate le immagini. Ogni immagine è composta da vari strati (layer) che vengono sovrapposti uno dopo l'altro durante la costruzione. I layer sono di sola lettura: questa apparente limitazione permette ad immagini diverse di condividere strati comuni.

Comandi per la gestione di immagini

Elenchiamo alcuni comandi per la gestione di immagini:

  • docker pull REPOSITORY[:TAG] scarica un'immagine dal Docker hub. Il tag di default è latest;
  • docker images elenca le immagini presenti nella macchina;
  • docker rmi IMAGE rimuove un'immagine dalla macchina. L'immagine può essere specificata nel formato REPOSITORY[:TAG], oppure può essere indicata con il suo ID.

Dopo averli presentati è il momento di vederli in azione, come faremo immediatamente a seguire.

Adoperiamo il comando pull scaricando l'immagine del web server Nginx e l'immagine di mysql.

root@server-prova:~# docker pull nginx:1.19

1.19: Pulling from library/nginx
75646c2fb410: Pull complete
6128033c842f: Pull complete
71a81b5270eb: Pull complete
b5fc821c48a1: Pull complete
da3f514a6428: Pull complete
3be359fed358: Pull complete
Digest: sha256:bae781e7f518e0fb02245140c97e6ddc9f5fcf6aecc043dd9d17e33aec81c832
Status: Downloaded newer image for nginx:1.19
docker.io/library/nginx:1.19
root@server-prova:~# docker pull mysql:5.6

5.6: Pulling from library/mysql
23a3602cd30c: Pull complete
fde922a9980f: Pull complete
5d5e58faa9a8: Pull complete
35d7b1499787: Pull complete
67575f8b7b80: Pull complete
b2844490be17: Pull complete
87f6510c6a65: Pull complete
877a22faf2fc: Pull complete
6259bafdad04: Pull complete
290903455872: Pull complete
37ba80fc86c3: Pull complete
Digest: sha256:e6c6bdc8cff8960953db5cb42f8be2d1db2931fafbf5f217d65b99ba0edae5df
Status: Downloaded newer image for mysql:5.6
docker.io/library/mysql:5.6

Verifichiamo le immagini presenti.

root@server-prova:~# docker images

REPOSITORY    TAG       IMAGE ID       CREATED       SIZE
nginx         1.19      7ce4f91ef623   8 hours ago   133MB
mysql         5.6       33d1008b853c   8 hours ago   303MB
hello-world   latest    d1165f221234   3 weeks ago   13.3kB

Eliminiamo, ora, l'immagine "hello-word:latest".

root@server-prova:~# docker rmi hello-world:latest

Untagged: hello-world:latest
Untagged: hello-world@sha256:308866a43596e83578c7dfa15e27a73011bdd402185a84c5cd7f32a88b501a24
Deleted: sha256:d1165f2212346b2bab48cb01c1e39ee8ad1be46b87873d9ca7a4e434980a7726
Deleted: sha256:f22b99068db93900abe17f7f5e09ec775c2826ecfe9db961fea68293744144bd

Questa volta eliminiamo l'immagine "mysql", usando l'ID.

root@server-prova:~# docker rmi 33d1

Untagged: mysql:5.6
Untagged: mysql@sha256:e6c6bdc8cff8960953db5cb42f8be2d1db2931fafbf5f217d65b99ba0edae5df
Deleted: sha256:33d1008b853c9bceb939ed6cd92072b94802a9cbd850b2b7ba4d1f809d62cf0e
Deleted: sha256:55aabec991d711e1a3048a6c75114ab81aca940c3728e09db00e8c8cddcec33c
Deleted: sha256:c401cdde7f523701eafabcefa602a131120c2da793e5733e3249f42684f70ee5
Deleted: sha256:9b668936eeaec439c01e8e92e0c82b07d23ab27a2ed66e08c53d3ae1cb5fa46e
Deleted: sha256:13685f9b4c9e2ac7434f55c00c4248258133695a75db1ba1f6bb1939276ee6db
Deleted: sha256:abe457cea3ed0605ecad4be4a49e535f55b2843d6945df0307fb0670a9386af5
Deleted: sha256:2dea2bcfa37321770a40e32c3c6fb813217cc249e5fbe804eba6551ef5493b7c
Deleted: sha256:2112b2088f51d206e1a7932b1121228666a00bd3b98b3862bf4c7f1ba805c667
Deleted: sha256:f55737a394aeee636347caca94bdb0e6e4332009c7845a492de0762c0ce3db0b
Deleted: sha256:c46389353b4501d28cf841500982e6bcec443066e1fd285ef241181b6b4c239a
Deleted: sha256:57d9c1a38b6a4b0525a7b3b2e8176195703eb05fb6bff4f87be651657b3c3aa5
Deleted: sha256:eed409fc820175ad1aef0ccd0ce74be9debe4bafdf60d7627d3302558a281b1b

I container

Nell'esempio presentato precedentemente il comando docker run hello-world ha istanziato un nuovo container a partire dall'immagine hello-world:latest: in questo ambiente isolato, l'applicazione definita nell'immagine viene eseguita.

Struttura di un container

Struttura di un container Docker

Abbiamo descritto l'immagine come una serie di strati immutabili sovrapposti. Quando generiamo un container, a questi layer di sola lettura, viene sovrapposto un layer scrivibile (layer container). Se avviamo un nuovo container a partire dalla stessa immagine, viene creato solo un nuovo layer container. I livelli immutabili che costituiscono l'immagine sono condivisi tra tutti i container istanziati a partire dalla stessa immagine.

Esecuzione di un container

Finora abbiamo introdotto alcuni concetti fondamentali e comandi utili nell'ecosistema Docker. Proviamo a toccare con mano la potenza che questa tecnologia ci offre.

Magari abbiamo sentito parlare di qualche framework, applicazione o db che ci intriga, vorremmo testarlo, ma non abbiamo tempo o voglia da dedicare alle operazioni di installazione e configurazione...
Docker viene in nostro aiuto!
Immaginiamo di voler sperimentare il database a grafo Neo4j. Lanciamo il seguente comando:

root@server-prova:~# docker run -p7474:7474 -p7687:7687 -e NEO4J_AUTH=neo4j/s3cr3t neo4j

Unable to find image 'neo4j:latest' locally
latest: Pulling from library/neo4j
ac2522cc7269: Pull complete
42f37f3c8df9: Pull complete
a456ec241295: Pull complete
4be3831c3e54: Pull complete
cacb12f0ed49: Pull complete
76f58b628fc6: Pull complete
8ba5f3cab8cc: Pull complete
a31d92ce9178: Pull complete
Digest: sha256:732105228bb4baefe77361def4cd4709c31061abc98387bf9d09730447cd8ece
Status: Downloaded newer image for neo4j:latest
Changed password for user 'neo4j'.
Directories in use:
  home:         /var/lib/neo4j
  config:       /var/lib/neo4j/conf
  logs:         /logs
  plugins:      /var/lib/neo4j/plugins
  import:       /var/lib/neo4j/import
  data:         /var/lib/neo4j/data
  certificates: /var/lib/neo4j/certificates
  run:          /var/lib/neo4j/run
Starting Neo4j.
2021-03-31 13:20:13.178+0000 INFO  Starting...
2021-03-31 13:20:19.033+0000 INFO  ======== Neo4j 4.2.4 ========
2021-03-31 13:20:23.842+0000 INFO  Initializing system graph model for component 'security-users' with version -1 and status UNINITIALIZED
2021-03-31 13:20:23.868+0000 INFO  Setting up initial user from `auth.ini` file: neo4j
2021-03-31 13:20:23.875+0000 INFO  Creating new user 'neo4j' (passwordChangeRequired=false, suspended=false)
2021-03-31 13:20:23.889+0000 INFO  Setting version for 'security-users' to 2
2021-03-31 13:20:23.896+0000 INFO  After initialization of system graph model component 'security-users' have version 2 and status CURRENT
2021-03-31 13:20:23.905+0000 INFO  Performing postInitialization step for component 'security-users' with version 2 and status CURRENT
2021-03-31 13:20:24.400+0000 INFO  Bolt enabled on 0.0.0.0:7687.
2021-03-31 13:20:27.432+0000 INFO  Remote interface available at http://localhost:7474/
2021-03-31 13:20:27.434+0000 INFO  Started.

Neo4j è partito!

Dopo aver preso nota dell'IP del nostro server presso Aruba Cloud, da browser, raggiungiamo http://IP-SERVER:7474/ ed ecco l'interfaccia grafica del DB, a nostra disposizione.

Interfaccia grafica di Neo4j

 

Con un solo comando e tempi molto ridotti, siamo riusciti a creare un container che esegue Neo4j.

Comandi docker run e docker ps

Per istanziare il container abbiamo usato il comando run. Si noti che, in questo caso, l'immagine scelta per il container (neo4j) non era presente in locale: Docker ha provveduto autonomamente a scaricarla . Successivamente è stato creato un layer container scrivibile quindi il container si è effettivamente avviato.

Il comando di base è docker run [OPTIONS] IMAGE [COMMAND]
COMMANDè il flag di comando opzionale che viene eseguito all'avvio del container.

Vediamo alcune delle opzioni più comuni.

  • --name: assegna un nome al container;
  • --detach , -d: avvia il container in background e ne stampa l'ID;
  • -it: è la combinazione di due opzioni che ci consente di avviare il container in modalità interattiva e di avere a disposizione una shell interna al container;
  • --env , -e: imposta variabili d'ambiente (nell'esempio, si adopera per impostare le credenziali);
  • --publish , -p: pubblica sulla macchina una porta container (-p 8080:80 significa che la porta 80 del container viene mappata attraverso la porta 8080 della macchina).

Per ottenere la lista dei container attivi, usiamo il comando docker ps.
Se desideriamo includere nella lista i container non più attivi, è necessario aggiungere l'opzione --all (oppure -a).

A segiure sono riportati alcuni esempi.

Inziamo con l'avvio del container, in questo caso  l'immagine "neo4j" in background.
Viene anche visualizzato l'ID del container.

root@server-prova:~# docker run -d neo4j

8afbdf468084d7bfa53eb1431fc5e30638db417d79505ab1a01437442295e28d

Visualizziamo ora, invece, la lista dei container attivi.

root@server-prova:~# docker ps

CONTAINER ID   IMAGE     COMMAND                  CREATED          STATUS          PORTS                     NAMES
8afbdf468084   neo4j     "/sbin/tini -g -- /d…"   12 minutes ago   Up 12 minutes   7473-7474/tcp, 7687/tcp   charming_hermann

Ora lanciamo il container (immagine "alpine") in modalità interattiva ottenendo anche una shell interna al container. Fatto ciò visualizziamo il contenuto del container tramite ls, quindi usciamo dalla shell.

root@server-prova:~# docker run -it alpine

Unable to find image 'alpine:latest' locally
latest: Pulling from library/alpine
9aae54b2144e: Pull complete
Digest: sha256:826f70e0ac33e99a72cf20fb0571245a8fee52d68cb26d8bc58e53bfa65dcdfa
Status: Downloaded newer image for alpine:latest
/ # ls
bin    dev    etc    home   lib    media  mnt    opt    proc   root   run    sbin   srv    sys    tmp    usr    var
/ # exit

root@server-prova:~#

Per ultimo, ma non come ultimo, giunti a questo punto visualizziamo la lista di tutti container, che essi siano attivi o meno.

root@server-prova:~# docker ps -a

CONTAINER ID   IMAGE     COMMAND                  CREATED          STATUS                     PORTS                     NAMES
cf304b3c72cc   alpine    "/bin/sh"                3 minutes ago    Exited (0) 3 minutes ago                             mystifying_lamport

8afbdf468084   neo4j     "/sbin/tini -g -- /d…"   18 minutes ago   Up 18 minutes              7473-7474/tcp, 7687/tcp   charming_hermann

Perchè il mio container si avvia ed esce immediatamente?

Spesso, muovendo i primi passi su Docker, ci troviamo di fronte alla circostanza inattesa in cui istanziamo dei container che si avviano e, immediatamente, si arrestano.
Questa situazione deriva dal principio fondamentale che il container rimane in vita finché il processo principale è in esecuzione. Come vedremo nei prossimi capitoli, il processo principale è solitamente definito nell'immagine ma può anche essere determinato dall'utente al momento della creazione del container.
Nell'esempio appena presentato, si osserva che il container "neo4j" è rimasto attivo, in quanto dipendente da un processo principale che non si esaurisce (/sbin/tini), al contrario "alpine" si è fermato perché, quando siamo usciti dalla bash, il processo /bin/sh si è arrestato.
Può succedere, per finalità di sperimentazione o debug, di dover tenere in vita un container che tende a disattivarsi subito dopo l'avvio. Un espediente potrebbe essere quello di avviare il container passando un comando che non si esaurisce: nel caso di "alpine", potremmo lanciare docker run -d alpine tail -f /dev/null. In contesti di produzione si sconsiglia comunque l'adozione di questo stratagemma.

I prossimi passi

In questo articolo, abbiamo approfondito i concetti di immagine e container. Abbiamo appreso come scaricare immagini dal Docker hub e a gestirle. Abbiamo visto come lanciare container.
Nel prossimo capitolo ci occuperemo in modo più approfondito del ciclo di vita di un container e vedremo i comandi principali per il controllo e la gestione.

Tutorial successivo: