Sebbene l'esempio appena visto sia banale, dovrebbe comunque risultare sorprendente la velocità con cui sono state svolte tutte le operazioni. A tal proposito, cerchiamo di capire meglio cosa succede quando proviamo ad istanziare un container:
- l’immagine del container viene (se non già presente) scaricata in locale;
- viene creato un ambiente isolato in cui avviare il container;
- il messaggio viene stampato a video;
- l’ambiente precedentemente creato viene dismesso.
Tutto ciò avviene in pochi secondi e con un semplice comando intuitivo e leggibile.
Immagini e container
Nella lezione precedente abbiamo posto l’attenzione su come un’immagine rappresenti, di fatto, una serie di layer immutabili, e di come questa caratteristica permetta a due immagini di condividere un layer comune. Una pila di layer accessibili in sola lettura, però, non ha molto senso di esistere, ragion per cui quello che avviene nel momento in cui si chiede a Docker di istanziare un container è la creazione, in cima a tutti gli altri layer, di un singolo layer scrivibile. D’ora in avanti, tutte le modifiche apportate al container verranno memorizzate all’interno di questo layer, detto anche layer container per differenziarlo da quelli in sola lettura, detti invece layer immagine.
Arrivati fin qui dovrebbe essere più chiara la relazione tra immagini e container: un’immagine è una pila di layer accessibili in sola lettura; il container relativo è la stessa pila di layer con sopra un layer scrivibile. Il contenuto finale del container è ottenuto per sovrapposizione di tutti questi layer.
Così come da un negativo posso sviluppare più copie della stessa foto, a partire da un’immagine posso avviare più istanze dello stesso container. Nel momento in cui arriverà la richiesta di avviare una seconda istanza di un container, Docker non farà altro che creare un nuovo layer container. Ad esempio, se eseguissimo due istanze dell’immagine nginx:1.7
, si verrebbe a creare una situazione simile alla seguente:
Come possiamo osservare nella figura, i layer immagine sono condivisi ed hanno tutti lo stesso identificativo, essendo comuni e generati in base al loro contenuto. Gli identificativi dei due layer container, invece, sono diversi tra loro poichè, a differenza dei layer immagine sottostanti, questi vengono generati in maniera casuale (per ovvi motivi).
Gestione dei container
Per mostrare a video l’elenco dei container, possiamo utilizzare il comando docker ps
. Se avessimo eseguito il solo container hello-world
dovremmo visualizzare un elenco vuoto:
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
Ciò accade perché i container che abbiamo creato non sono più attivi: infatti, un container è attivo fintantoché è attivo il processo al suo interno. Nel caso del container hello-world
, il processo al suo interno è la stampa a video del messaggio di saluto; non appena questo si conclude, anche il container cessa la sua esecuzione. Per vedere tutti i container, anche quelli non più attivi, possiamo usare il flag -a
o --all
:
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c3849603f184 hello-world "/hello" 3 weeks ago Exited trusting_bell
0017d2a0b9d1 hello-world "/hello" 6 seconds ago Exited boring_turing
Il comando mostrerà a video l’id breve del container layer, il nome dell’immagine che l’ha generato, il processo al suo interno, la data di creazione e lo stato. Oltre all’id esadecimale, ogni container ha anche un nome più "user friendly", che viene assegnato da Docker ed ha sempre la forma di un aggettivo, seguito dal nome di uno scienziato famoso. Se volessimo scegliere noi un nome più significativo, potremmo farlo specificandolo all’interno del comando docker run
, utilizzando il flag --name
:
$ docker run --name hello_html_it hello-world
Modalità detached e modalità interattiva
Oltre al flag --name
appena visto, il comando docker run
supporta altre opzioni che condizionano fortemente il lifestyle del container. Tra i più interessanti troviamo sicuramente -d
, ad indicare un container detached. In questa modalità, il container appena avviato viene eseguito in background. Ad esempio:
$ docker run -d hello-world
b4ec94bf57023778c0ad191e4626f563cd86edfbb893cb701172e676462fe063
In questo caso, piuttosto che il solito saluto ci viene mostrata una stringa, rappresentante l’id in forma estesa del layer container. Per visualizzare l’output prodotto in background, possiamo utilizzare il comando docker logs
seguito dall’identificativo del container (bastano anche i primi caratteri):
docker logs b4ec9
L’utilizzo di tale modalità è particolarmente indicato per l’avvio di container che fungeranno poi da servizi, quali web server, DBMS e così via.
I flag -i
e -t
(quasi sempre usati come singolo flag -it
), invece, fanno sì che i canali standard di un container vengano rediretti, dando così l’impressione di essere fisicamente all'interno dello stesso. Per questo esempio, utilizzeremo l’immagine di Ubuntu, ne istanzieremo un container e proveremo ad utilizzare la shell al suo interno in maniera interattiva. Oltre ai vari flag appena visti, il comando docker run
accetta in ingresso un eseguibile da lanciare una volta che il container è pronto (visualizzato nella colonna COMMAND
dell'output di docker ps
). In questo esempio, vorremo visualizzare l'output di un semplice comando ls
, e perciò avremo bisogno di una shell; pertanto, l’eseguibile che andremo ad indicare sarà appunto /bin/bash
e il comando finale avrà questa forma:
$ docker run -it ubuntu /bin/bash
root@996fc96427ff:/# ls
bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
Per evidenziare il cambio di ambiente, viene mostrato un prompt diverso dal precedente, nel nostro caso root@996fc96427ff:/#
dove root
è il nome utente e 996fc96427ff
l’id breve del container. A questo punto abbiamo pieno accesso al container e possiamo utilizzare ls
per listare file e directory.
Poichè, come detto precedentemente, un container resta in vita finché è in vita il processo ospitato al suo interno, per terminare la modalità interattiva sarà sufficiente eseguire il comando exit
.
Per terminare, invece, la sola modalità interattiva (senza chiudere pertanto il container), utilizzeremo la combinazione di tasti CTRL + P + Q
. Se vogliamo ristabilire un collegamento interattivo col container, possiamo utilizzare il comando docker attach
, seguito dall’identificativo del container:
$ docker run -it ubuntu /bin/bash
root@113e7c31aae9:/# // CTRL + P + Q
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
113e7c31aae9 ubuntu "/bin/bash" 12 seconds ago Up 11 seconds awesome_bartik
$ docker attach 113e7c
root@113e7c31aae9:/#
Come risulta anche dall’output di docker ps
, la combinazione di tasti CTRL + P + Q
non ha arrestato il container. Per riattaccarci al container useremo il comando docker attach
seguito dal container id (anche in questo caso basta la porzione iniziale).
Eliminazione di container
Una volta che iniziamo a prendere confidenza con Docker, è facile che i container inizino a proliferare sempre più. Per fare ordine, esiste il comando docker rm
(remove) seguito dal container id o parte di esso. Ad esempio:
docker rm 113e7c
oppure, se il container che si vuole eliminare è ancora in esecuzione:
docker rm --force 113e7c
Cancellare un container vuol dire eliminare il container layer creato al momento dell’esecuzione del comando docker run
: tutte le modifiche verranno perse irrimediabilmente, mentre i layer immagine sottostanti resteranno sul nostro hard disk.
Ciclo di vita di un container
Se abbiamo l’esigenza di avviare un container attualmente spento, il semplice docker run
non servirà più, e al suo posto va utilizzato il comando docker start
seguito dall’id del container. Supponiamo di aver terminato il container precedente (id 113e7c31aae9) e abbiamo ora bisogno di rientrarci; procederemo come segue:
$ docker start 113e7c31aae9
113e7c31aae9
$ docker attach 113e7c31aae9
root@113e7c31aae9:/#
Analogamente, esiste il comando docker stop
per terminare un container attualmente in esecuzione, i comandi pause
e unpause
per sospendere e riattivare un container, nonché il comando restart
per riavviarlo in caso di necessità.