Il progetto Docker si è affermato come uno standard per la vir­tua­liz­za­zio­ne basata su container tramite il software omonimo. Un concetto chiave quando si usano delle piat­ta­for­me Docker è quello dell’immagine Docker. In questo articolo, spie­ghe­re­mo come le immagini Docker sono costruite e come fun­zio­na­no.

Cos’è un’immagine Docker?

Potreste già conoscere il termine “immagine” nel contesto della vir­tua­liz­za­zio­ne delle macchine virtuali (VM). Di solito, l’immagine di una VM è una copia di un sistema operativo. Un’immagine VM può contenere altri com­po­nen­ti in­stal­la­ti come database e server web. Il termine deriva dal tempo in cui il software veniva di­stri­bui­to su supporti ottici di dati come CD-ROM e DVD. Al tempo, se si voleva creare una copia locale del supporto dati, si doveva creare un’“immagine” con un software speciale.

La vir­tua­liz­za­zio­ne basata su container è il passo suc­ces­si­vo più logico nello sviluppo della vir­tua­liz­za­zio­ne delle VM. Invece di vir­tua­liz­za­re un computer virtuale (macchina) con il proprio sistema operativo, un’immagine Docker di solito consiste in una sola ap­pli­ca­zio­ne. Questa potrebbe essere rap­pre­sen­ta­ta da un singolo file binario o da una com­bi­na­zio­ne di diversi com­po­nen­ti software.

Per eseguire l’ap­pli­ca­zio­ne viene prima creato un container dall’immagine. Tutti i container in ese­cu­zio­ne su un host Docker usano lo stesso kernel del sistema operativo. Di con­se­guen­za, i container Docker e le immagini Docker sono di solito si­gni­fi­ca­ti­va­men­te più leggeri delle macchine virtuali a essi com­pa­ra­bi­li e delle loro immagini.

I concetti di container Docker e immagini Docker sono stret­ta­men­te collegati. Infatti, non solo un container Docker può essere creato da un’immagine Docker, ma una nuova immagine può anche essere creata da un container in ese­cu­zio­ne. Questo è il motivo per cui diciamo che le immagini Docker e i container Docker rimandano al paradosso dell’uovo e della gallina:

Comando Docker De­scri­zio­ne Analogia con il paradosso dell’uovo e la gallina
docker run <image-id> Crea un container Docker da un’immagine Il pulcino esce dall’uovo
docker commit <container-id> Crea un’immagine Docker da un container La gallina depone un uovo nuovo

Nel sistema biologico dell’uovo e della gallina, da un uovo viene prodotto esat­ta­men­te un pulcino e l’uovo viene perso nel processo. Al contrario, un’immagine Docker può essere usata per creare un numero il­li­mi­ta­to di container simili. Questa ri­pro­du­ci­bi­li­tà rende Docker una piat­ta­for­ma ideale per ap­pli­ca­zio­ni e servizi scalabili.

Un’immagine Docker è un modello im­mu­ta­bi­le che può essere usato ri­pe­tu­ta­men­te per creare container Docker. L’immagine contiene tutte le in­for­ma­zio­ni e le di­pen­den­ze ne­ces­sa­rie per eseguire un container, comprese tutte le librerie di programma di base e le in­ter­fac­ce utente. Di solito ciò comprende un ambiente a riga di comando (“shell”) e un’im­ple­men­ta­zio­ne della libreria standard di C. Di seguito ri­por­tia­mo una pa­no­ra­mi­ca dell’immagine ufficiale “Alpine Linux”:

Kernel Linux Libreria standard C Comando Unix
Dall’host musl libc BusyBox

Oltre a questi com­po­nen­ti di base che integrano il kernel Linux, un’immagine Docker di solito contiene anche un software ag­giun­ti­vo. Di seguito troverete alcuni esempi di com­po­nen­ti software per diverse aree di ap­pli­ca­zio­ne. Si prega di notare che una singola immagine Docker so­li­ta­men­te contiene una selezione ridotta dei com­po­nen­ti mostrati:

Area d’ap­pli­ca­zio­ne Com­po­nen­ti Software
Linguaggi di pro­gram­ma­zio­ne PHP, Python, Ruby, Java, Ja­va­Script
Strumenti di sviluppo node/npm, React, Laravel
Sistemi database MySQL, Postgres, MongoDB, Redis
Server web Apache, nginx, lighttpd
Cache e proxy Varnish, Squid
Sistemi di gestione del contenuto WordPress, Magento, Ruby on Rails

In cosa si dif­fe­ren­zia un’immagine Docker da un container Docker?

Come abbiamo visto, le immagini Docker e i container Docker sono stret­ta­men­te correlati ma in cosa dif­fe­ri­sco­no i due concetti?

Prima di tutto, un’immagine Docker è inerte, occupa uno spazio di ar­chi­via­zio­ne minimo e non utilizza alcuna risorsa di sistema. Inoltre, un’immagine Docker non può essere mo­di­fi­ca­ta dopo la creazione e come tale è un supporto di “sola lettura”. È anche utile ricordare che è possibile ag­giun­ge­re modifiche a un’immagine Docker esistente, ma questo creerà nuove immagini. No­no­stan­te ciò, rimarrà comunque una versione originale e non mo­di­fi­ca­ta dell’immagine.

Come già an­ti­ci­pa­to, un’immagine Docker può essere usata per creare un numero il­li­mi­ta­to di container simili ma in cosa esat­ta­men­te un container Docker è diverso da un’immagine Docker? Di­ver­sa­men­te da un’immagine Docker, un container Docker è un’istanza in ese­cu­zio­ne (cioè un’istanza in fase di ese­cu­zio­ne) di un’immagine Docker. Come qualsiasi software eseguito su un computer, un container Docker in ese­cu­zio­ne utilizza le risorse di sistema, la memoria di lavoro e i cicli della CPU. Inoltre, lo stato di un container cambia durante il suo ciclo di vita.

Se questa de­scri­zio­ne sembra troppo astratta, vi pro­po­nia­mo di usare un esempio che prende spunto dalla vita di tutti i giorni: pensate a un’immagine Docker come a un DVD. Il DVD stesso è inerte, sta nella sua custodia e non fa nulla. Occupa per­ma­nen­te­men­te lo stesso spazio limitato nella stanza. Il contenuto diventa “vivo” solo quando il DVD viene ri­pro­dot­to in un ambiente speciale (il lettore DVD).

Così come il film generato quando un DVD viene ri­pro­dot­to, un container Docker in ese­cu­zio­ne ha uno stato. Nel caso di un film, questo include il tempo di ri­pro­du­zio­ne corrente, la lingua se­le­zio­na­ta, i sot­to­ti­to­li, ecc. Questo stato cambia nel tempo, e un film in ri­pro­du­zio­ne consuma co­stan­te­men­te elet­tri­ci­tà. Proprio come un numero il­li­mi­ta­to di container simili può essere creato da un’immagine Docker, il film su un DVD può essere ri­pro­dot­to più e più volte. Inoltre, il film in ese­cu­zio­ne può essere fermato e avviato, proprio come un container Docker.

Concetto Docker Analogia Modo Stato Consumo di risorse
Immagine Docker DVD Inerte “Sola lettura” /im­mu­ta­bi­le Fisso
Container Docker “vivo” Riproduce film Cambia nel tempo Varia in base all’uso

Come e dove vengono uti­liz­za­te le immagini Docker?

Al giorno d’oggi, Docker è usato in tutte le fasi del ciclo di vita del software, comprese le fasi di sviluppo, test e fun­zio­na­men­to. Il concetto centrale nell’eco­si­ste­ma Docker è il container, il quale viene sempre creato da un’immagine. Come tale, le immagini Docker sono usate ovunque si usi Docker. Vi mostriamo alcuni esempi.

Immagini Docker in ambienti di sviluppo locali

Se svi­lup­pa­te un software sul vostro di­spo­si­ti­vo, vorrete mantenere l’ambiente di sviluppo locale il più coerente possibile. Il più delle volte avrete bisogno di versioni per­fet­ta­men­te cor­ri­spon­den­ti sia del lin­guag­gio di pro­gram­ma­zio­ne, che delle librerie e di altri com­po­nen­ti software. Se anche solo uno dei molti livelli che in­te­ra­gi­sco­no viene cambiato, può ra­pi­da­men­te scon­vol­ge­re gli altri livelli. Questo può causare la mancata com­pi­la­zio­ne del codice sorgente o il mancato avvio del server web. In questo caso, l’im­mu­ta­bi­li­tà di un’immagine Docker è in­cre­di­bil­men­te utile. In quanto svi­lup­pa­to­ri, potete essere sicuri che l’ambiente contenuto nell’immagine rimarrà coerente.

I grandi progetti di sviluppo possono essere portati avanti da vari team. In questo caso, usare un ambiente che rimane stabile nel tempo è cruciale per la com­pa­ra­bi­li­tà e la ri­pro­du­ci­bi­li­tà. Tutti gli svi­lup­pa­to­ri di un team possono infatti usare la stessa immagine e quando un nuovo svi­lup­pa­to­re si unisce al team, può trovare l’immagine Docker giusta e iniziare subito a lavorare. Quando vengono apportate modifiche all’ambiente di sviluppo, viene creata una nuova immagine Docker. Gli svi­lup­pa­to­ri possono quindi ottenere la nuova immagine e sono così im­me­dia­ta­men­te ag­gior­na­ti.

Immagini Docker in una Service-oriented ar­chi­tec­tu­re (SOA)

Le immagini Docker co­sti­tui­sco­no la base della moderna ar­chi­tet­tu­ra orientata ai servizi. Invece di una singola ap­pli­ca­zio­ne mo­no­li­ti­ca, vengono svi­lup­pa­ti servizi in­di­vi­dua­li con in­ter­fac­ce ben definite. Ogni servizio è im­pac­chet­ta­to nella propria immagine. I container lanciati da questa immagine co­mu­ni­ca­no tra loro at­tra­ver­so la rete e sta­bi­li­sco­no la fun­zio­na­li­tà com­ples­si­va dell’ap­pli­ca­zio­ne. Rac­chiu­den­do i servizi nelle proprie immagini Docker in­di­vi­dua­li, è possibile svi­lup­par­li e man­te­ner­li in modo in­di­pen­den­te. I singoli servizi possono anche essere scritti in diversi linguaggi di pro­gram­ma­zio­ne.

Immagini Docker per fornitori di servizi di hosting (PaaS)

Le immagini Docker possono essere uti­liz­za­te anche nei data center. Ogni servizio (ad esempio, load balancer, server web, server di database, ecc.) può essere definito come un’immagine Docker. I container ri­sul­tan­ti possono sup­por­ta­re ciascuno un certo carico. Il software di or­che­stra­zio­ne monitora il container, il suo carico e il suo stato. Quando il carico aumenta, l’or­che­stra­to­re avvia ulteriori container dall’immagine cor­ri­spon­den­te. Questo approccio permette di scalare ra­pi­da­men­te i servizi per ri­spon­de­re a con­di­zio­ni mutevoli.

Come si co­strui­sco­no le immagini Docker?

In contrasto con le immagini delle macchine virtuali, un’immagine Docker nor­mal­men­te non è un file singolo; è invece co­sti­tui­ta da una com­bi­na­zio­ne di diversi com­po­nen­ti. Di seguito vi mostriamo una rapida pa­no­ra­mi­ca dei diversi com­po­nen­ti che co­sti­tui­sco­no un’immagine Docker (più dettagli se­gui­ran­no in seguito):

  • I livelli dell’immagine con­ten­go­no dati aggiunti da ope­ra­zio­ni ef­fet­tua­te sul file system. I livelli sono so­vrap­po­sti e poi ridotti a un livello coerente da un union file system.
  • Un’immagine madre prepara le funzioni di base dell’immagine e la ancora nella directory prin­ci­pa­le dei file dell’eco­si­ste­ma Docker.
  • Un’immagine manifest descrive la com­po­si­zio­ne dell’immagine e iden­ti­fi­ca i livelli dell’immagine.

Cosa si deve fare se si vuole con­ver­ti­re un’immagine Docker in un singolo file? Potete usare il comando “docker save” sulla riga di comando. Questo crea un file di sal­va­tag­gio .tar che può essere fa­cil­men­te spostato tra i sistemi. Con il seguente comando, ad esempio, un’immagine Docker con il nome “busybox” viene scritta in un file “busybox.tar”:

docker save busybox > busybox.tar

Spesso, l’output del comando “docker save” è trasmesso in Gzip sulla riga di comando. In questo modo, i dati vengono compressi dopo essere stati vi­sua­liz­za­ti nel file .tar:

docker save myimage:latest | gzip > myimage_latest.tar.gz

Un file immagine creato tramite “docker save” può essere inserito nell’host Docker locale come immagine Docker tramite il comando “docker load”:

docker load busybox.tar

Livelli d’immagine

Un’immagine Docker è composta da livelli di sola lettura. Ogni livello descrive le suc­ces­si­ve modifiche al file system dell’immagine. Per ogni ope­ra­zio­ne che porta a una modifica del file system, viene creato un nuovo livello. L’approccio usato qui è so­li­ta­men­te indicato come “copy-on-write”: un accesso in scrittura crea una copia mo­di­fi­ca­ta dell’immagine in un nuovo livello mentre i dati originali rimangono invariati. Se questo principio vi suona familiare è perché il software di controllo della versione Git funziona allo stesso modo.

È possibile vi­sua­liz­za­re i livelli di un’immagine Docker uti­liz­zan­do il comando “Docker image inspect” sulla riga di comando. Questo comando re­sti­tui­sce un documento JSON che può essere elaborato con lo strumento standard jq:

docker image inspect <image-id> | jq -r '.[].RootFS.Layers[]'

Per unire nuo­va­men­te i cam­bia­men­ti nei livelli viene usato un file system speciale: l’union file system. Questo so­vrap­po­ne tutti i livelli per produrre una struttura coerente di cartelle e file sull’in­ter­fac­cia. Sto­ri­ca­men­te, sono state usate varie tec­no­lo­gie co­no­sciu­te come “storage driver” per im­ple­men­ta­re l’union file system. Oggi, lo storage driver “overlay2” è rac­co­man­da­to nella maggior parte dei casi:

Storage driver Commento
overlay2 Rac­co­man­da­to per l’uso al giorno d’oggi
aufs, overlay Usato in versioni pre­ce­den­ti

È anche possibile vi­sua­liz­za­re il driver di ar­chi­via­zio­ne uti­liz­za­to per un’immagine Docker. Per far ciò bisognerà usare il comando “docker image inspect” sulla riga di comando. Questo re­sti­tui­sce un documento JSON che possiamo poi elaborare con lo strumento standard jq:

docker image inspect <image-id> | jq -r '.[].GraphDriver.Name'

Ogni livello dell’immagine è iden­ti­fi­ca­to con un hash chiaro calcolato dalle modifiche contenute su di esso. Se due immagini usano lo stesso livello, questo sarà me­mo­riz­za­to in locale solo una volta. Entrambe le immagini useranno quindi lo stesso livello. Questo assicura una me­mo­riz­za­zio­ne locale ef­fi­cien­te e riduce i volumi di tra­sfe­ri­men­to quando si ottengono le immagini.

Immagine genitore

Un’immagine Docker di solito ha un’“immagine madre” sot­to­stan­te. Nella maggior parte dei casi, l’immagine madre è definita da una direttiva FROM nel Doc­ker­fi­le. Quest’immagine definisce una base su cui si basano le immagini derivate. I livelli dell’immagine esistente sono so­vrap­po­sti a livelli ag­giun­ti­vi.

Quando un’immagine Docker “eredita” dall’immagine madre, è posta in una directory di file che contiene tutte le immagini esistenti. Vi state chiedendo dove inizia la directory prin­ci­pa­le dei file? Le sue radici sono de­ter­mi­na­te da alcune “immagini base” speciali. Nella maggior parte dei casi, un’immagine base è definita con la direttiva “FROM scratch” nel Doc­ker­fi­le. Vi sono comunque altri modi per creare un’immagine base. Potrete saperne di più nella sezione “Da dove vengono le immagini Docker?”.

Immagine manifest

Come spiegato nelle sezioni pre­ce­den­ti, un’immagine Docker è composta da diversi livelli. È quindi possibile uti­liz­za­re il comando “Docker image pull” per estrarre un’immagine Docker da un registro online. In questo caso, non viene scaricato un singolo file. Invece, il demone Docker locale scarica i singoli livelli e li salva. Quindi, da dove vengono le in­for­ma­zio­ni sui singoli livelli?

Le in­for­ma­zio­ni riguardo il tipo di livelli d’immagine di cui è composta un’immagine Docker si possono trovare nell’immagine manifest. Un’immagine manifest è un file JSON che descrive com­ple­ta­men­te un’immagine Docker e contiene quanto segue:

  • In­for­ma­zio­ni sulla versione, lo schema e la di­men­sio­ne
  • Gli hash crit­to­gra­fi­ci dei livelli d’immagine uti­liz­za­ti
  • In­for­ma­zio­ni sulle ar­chi­tet­tu­re dei pro­ces­so­ri di­spo­ni­bi­li

Per iden­ti­fi­ca­re chia­ra­men­te un’immagine Docker, viene creato un hash crit­to­gra­fi­co dell’immagine manifest. Quando viene usato il comando “Docker image pull”, il file manifest viene scaricato. Il demone Docker locale ottiene quindi i singoli livelli dell’immagine.

Da dove vengono le immagini Docker?

Come spe­ci­fi­ca­to nel corso dell’articolo, le immagini Docker sono una parte im­por­tan­te dell’eco­si­ste­ma Docker. Esistono molti modi diversi per ottenere un’immagine Docker. Vi sono però due metodi di base fon­da­men­ta­li che vedremo più in dettaglio di seguito:

  1. Estrarre le immagini Docker esistenti da un registro
  2. Creare nuove immagini Docker

Estrarre le immagini Docker esistenti da un registro

Spesso, un progetto Docker inizia quando un’immagine Docker esistente viene estratta da un registro. Si tratta di una piat­ta­for­ma a cui si può accedere tramite la rete che fornisce le immagini Docker. L’host Docker locale comunica con il registro per scaricare un’immagine Docker dopo l’ese­cu­zio­ne di un comando “docker image pull”.

Vi sono dei registri online ac­ces­si­bi­li pub­bli­ca­men­te che offrono una vasta selezione di immagini Docker esistenti da uti­liz­za­re. In data corrente, vi sono più di otto milioni di immagini Docker li­be­ra­men­te di­spo­ni­bi­li sul registro ufficiale Docker “Docker Hub”. Oltre alle immagini Docker, Microsoft “Azure Container Registry” include altre immagini container in una varietà di formati diversi. È inoltre possibile uti­liz­za­re la piat­ta­for­ma per creare i propri registri di container privati.

Oltre ai registri online di cui sopra, è anche possibile ospitare un registro locale au­to­no­ma­men­te. Ad esempio, le grandi or­ga­niz­za­zio­ni spesso usano questa opzione per per­met­te­re ai propri team un accesso protetto alle immagini Docker create au­to­no­ma­men­te. Docker ha creato il Docker Trusted Registry (DTR) esat­ta­men­te per questo scopo. Si tratta di una soluzione in sede per la fornitura di un registro interno nel proprio data center.

Creare una nuova immagine Docker

A volte potreste voler creare un’immagine Docker ap­po­si­ta­men­te adattata a un progetto specifico. So­li­ta­men­te, è possibile usare un’immagine Docker esistente e adattarla alle proprie esigenze. Ricordate che le immagini Docker sono im­mu­ta­bi­li e che quando viene fatta una modifica, viene creata una nuova immagine Docker. Esistono diversi modi per creare una nuova immagine Docker:

  1. Costruire sull’immagine madre con il Doc­ker­fi­le
  2. Generarne un’immagine dal container in ese­cu­zio­ne
  3. Creare una nuova immagine base

L’approccio più comune per creare una nuova immagine Docker è quello di scrivere un Doc­ker­fi­le. Un Doc­ker­fi­le contiene comandi speciali che de­fi­ni­sco­no l’immagine madre e qualsiasi modifica richiesta. Ri­chia­man­do il comando “docker image build” verrà creata una nuova immagine Docker dal Doc­ker­fi­le. Ve ne mostriamo un esempio rapido:

# Crea il Dockerfile sulla riga di comando
cat <<EOF > ./Dockerfile
FROM busybox
RUN echo "hello world"
EOF
# Crea un’immagine Docker da un Dockerfile
docker image build

Sto­ri­ca­men­te, il termine “immagine” deriva dal co­sid­det­to “imaging” di un supporto dati. Nel contesto delle macchine virtuali (VM), è possibile creare un’istan­ta­nea dell’immagine di una VM in ese­cu­zio­ne. Un processo simile può essere eseguito con Docker. Con il comando “docker commit”, è possibile creare un’immagine di un container in ese­cu­zio­ne come una nuova immagine Docker. Tutte le modifiche apportate al container saranno salvate:

docker commit <container-id>

Inoltre, è possibile passare le istru­zio­ni del Doc­ker­fi­le uti­liz­zan­do il comando “docker commit”. In questo caso le modifiche co­di­fi­ca­te con le istru­zio­ni diventano parte della nuova immagine Docker:

docker commit --change <dockerfile instructions> <container-id>

È inoltre possibile usare il comando “docker image history” per rin­trac­cia­re quali modifiche sono state fatte a un’immagine Docker in seguito:

docker image history <image-id>

Come an­ti­ci­pa­to, è possibile basare una nuova immagine Docker su un’immagine madre o sullo stato di un container in ese­cu­zio­ne, ma come si crea una nuova immagine Docker da zero? Esistono due modi diversi per farlo. Una prima opzione prevede l’utilizzo di un Doc­ker­fi­le con la direttiva speciale “FROM scratch” come descritto sopra. Questo crea una nuova immagine base minima.

Se si pre­fe­ri­sce invece non usare l’immagine Docker data dalla direttiva “FROM scratch”, bisognerà ricorrere a uno strumento speciale come de­boo­tstrap e preparare una di­stri­bu­zio­ne Linux. Questa sarà poi im­pac­chet­ta­ta in un file tarball con il comando tar e importata nell’host Docker locale tramite “docker image import”.

I comandi più im­por­tan­ti per le immagini Docker

Comando immagine Docker Spie­ga­zio­ne
docker image build Crea un’immagine Docker da un Doc­ker­fi­le
docker image history Mostra i passaggi ef­fet­tua­ti per la creazione di un’immagine Docker
docker image import Crea un’immagine Docker da un file tarball
docker image inspect Mostra in­for­ma­zio­ni det­ta­glia­te su un’immagine Docker
docker image load Crea un file d’immagine creato con “Docker image save”
docker image ls / Docker images Elenca le immagini di­spo­ni­bi­li sull’host Docker
docker image prune Rimuove le immagini Docker inu­ti­liz­za­te dall’host Docker
docker image pull Estrae un’immagine Docker dal registro
docker image push Invia un’immagine Docker al registro
docker image rm Rimuove un’immagine Docker dall’host Docker locale
docker image save Crea un file d’immagine
docker image tag Inserisce tag a un’immagine Docker
Vai al menu prin­ci­pa­le