Immagine Docker
Il progetto Docker si è affermato come uno standard per la virtualizzazione basata su container tramite il software omonimo. Un concetto chiave quando si usano delle piattaforme Docker è quello dell’immagine Docker. In questo articolo, spiegheremo come le immagini Docker sono costruite e come funzionano.
Cos’è un’immagine Docker?
Potreste già conoscere il termine “immagine” nel contesto della virtualizzazione delle macchine virtuali (VM). Di solito, l’immagine di una VM è una copia di un sistema operativo. Un’immagine VM può contenere altri componenti installati come database e server web. Il termine deriva dal tempo in cui il software veniva distribuito 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 virtualizzazione basata su container è il passo successivo più logico nello sviluppo della virtualizzazione delle VM. Invece di virtualizzare un computer virtuale (macchina) con il proprio sistema operativo, un’immagine Docker di solito consiste in una sola applicazione. Questa potrebbe essere rappresentata da un singolo file binario o da una combinazione di diversi componenti software.
Per eseguire l’applicazione viene prima creato un container dall’immagine. Tutti i container in esecuzione su un host Docker usano lo stesso kernel del sistema operativo. Di conseguenza, i container Docker e le immagini Docker sono di solito significativamente più leggeri delle macchine virtuali a essi comparabili e delle loro immagini.
I concetti di container Docker e immagini Docker sono strettamente 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 esecuzione. Questo è il motivo per cui diciamo che le immagini Docker e i container Docker rimandano al paradosso dell’uovo e della gallina:
Comando Docker | Descrizione | 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 esattamente un pulcino e l’uovo viene perso nel processo. Al contrario, un’immagine Docker può essere usata per creare un numero illimitato di container simili. Questa riproducibilità rende Docker una piattaforma ideale per applicazioni e servizi scalabili.
Un’immagine Docker è un modello immutabile che può essere usato ripetutamente per creare container Docker. L’immagine contiene tutte le informazioni e le dipendenze necessarie per eseguire un container, comprese tutte le librerie di programma di base e le interfacce utente. Di solito ciò comprende un ambiente a riga di comando (“shell”) e un’implementazione della libreria standard di C. Di seguito riportiamo una panoramica dell’immagine ufficiale “Alpine Linux”:
Kernel Linux | Libreria standard C | Comando Unix |
Dall’host | musl libc | BusyBox |
Oltre a questi componenti di base che integrano il kernel Linux, un’immagine Docker di solito contiene anche un software aggiuntivo. Di seguito troverete alcuni esempi di componenti software per diverse aree di applicazione. Si prega di notare che una singola immagine Docker solitamente contiene una selezione ridotta dei componenti mostrati:
Area d’applicazione | Componenti Software |
Linguaggi di programmazione | PHP, Python, Ruby, Java, JavaScript |
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 differenzia un’immagine Docker da un container Docker?
Come abbiamo visto, le immagini Docker e i container Docker sono strettamente correlati ma in cosa differiscono i due concetti?
Prima di tutto, un’immagine Docker è inerte, occupa uno spazio di archiviazione minimo e non utilizza alcuna risorsa di sistema. Inoltre, un’immagine Docker non può essere modificata dopo la creazione e come tale è un supporto di “sola lettura”. È anche utile ricordare che è possibile aggiungere modifiche a un’immagine Docker esistente, ma questo creerà nuove immagini. Nonostante ciò, rimarrà comunque una versione originale e non modificata dell’immagine.
Come già anticipato, un’immagine Docker può essere usata per creare un numero illimitato di container simili ma in cosa esattamente un container Docker è diverso da un’immagine Docker? Diversamente da un’immagine Docker, un container Docker è un’istanza in esecuzione (cioè un’istanza in fase di esecuzione) di un’immagine Docker. Come qualsiasi software eseguito su un computer, un container Docker in esecuzione 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 descrizione sembra troppo astratta, vi proponiamo 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 permanentemente lo stesso spazio limitato nella stanza. Il contenuto diventa “vivo” solo quando il DVD viene riprodotto in un ambiente speciale (il lettore DVD).
Così come il film generato quando un DVD viene riprodotto, un container Docker in esecuzione ha uno stato. Nel caso di un film, questo include il tempo di riproduzione corrente, la lingua selezionata, i sottotitoli, ecc. Questo stato cambia nel tempo, e un film in riproduzione consuma costantemente elettricità. Proprio come un numero illimitato di container simili può essere creato da un’immagine Docker, il film su un DVD può essere riprodotto più e più volte. Inoltre, il film in esecuzione può essere fermato e avviato, proprio come un container Docker.
Concetto Docker | Analogia | Modo | Stato | Consumo di risorse |
Immagine Docker | DVD | Inerte | “Sola lettura” /immutabile | Fisso |
Container Docker | “vivo” | Riproduce film | Cambia nel tempo | Varia in base all’uso |
Come e dove vengono utilizzate 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 funzionamento. Il concetto centrale nell’ecosistema 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 sviluppate un software sul vostro dispositivo, vorrete mantenere l’ambiente di sviluppo locale il più coerente possibile. Il più delle volte avrete bisogno di versioni perfettamente corrispondenti sia del linguaggio di programmazione, che delle librerie e di altri componenti software. Se anche solo uno dei molti livelli che interagiscono viene cambiato, può rapidamente sconvolgere gli altri livelli. Questo può causare la mancata compilazione del codice sorgente o il mancato avvio del server web. In questo caso, l’immutabilità di un’immagine Docker è incredibilmente utile. In quanto sviluppatori, 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 comparabilità e la riproducibilità. Tutti gli sviluppatori di un team possono infatti usare la stessa immagine e quando un nuovo sviluppatore 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 sviluppatori possono quindi ottenere la nuova immagine e sono così immediatamente aggiornati.
Immagini Docker in una Service-oriented architecture (SOA)
Le immagini Docker costituiscono la base della moderna architettura orientata ai servizi. Invece di una singola applicazione monolitica, vengono sviluppati servizi individuali con interfacce ben definite. Ogni servizio è impacchettato nella propria immagine. I container lanciati da questa immagine comunicano tra loro attraverso la rete e stabiliscono la funzionalità complessiva dell’applicazione. Racchiudendo i servizi nelle proprie immagini Docker individuali, è possibile svilupparli e mantenerli in modo indipendente. I singoli servizi possono anche essere scritti in diversi linguaggi di programmazione.
Immagini Docker per fornitori di servizi di hosting (PaaS)
Le immagini Docker possono essere utilizzate 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 risultanti possono supportare ciascuno un certo carico. Il software di orchestrazione monitora il container, il suo carico e il suo stato. Quando il carico aumenta, l’orchestratore avvia ulteriori container dall’immagine corrispondente. Questo approccio permette di scalare rapidamente i servizi per rispondere a condizioni mutevoli.
Come si costruiscono le immagini Docker?
In contrasto con le immagini delle macchine virtuali, un’immagine Docker normalmente non è un file singolo; è invece costituita da una combinazione di diversi componenti. Di seguito vi mostriamo una rapida panoramica dei diversi componenti che costituiscono un’immagine Docker (più dettagli seguiranno in seguito):
- I livelli dell’immagine contengono dati aggiunti da operazioni effettuate sul file system. I livelli sono sovrapposti 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 principale dei file dell’ecosistema Docker.
- Un’immagine manifest descrive la composizione dell’immagine e identifica i livelli dell’immagine.
Cosa si deve fare se si vuole convertire un’immagine Docker in un singolo file? Potete usare il comando “docker save” sulla riga di comando. Questo crea un file di salvataggio .tar che può essere facilmente 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 visualizzati 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 successive modifiche al file system dell’immagine. Per ogni operazione che porta a una modifica del file system, viene creato un nuovo livello. L’approccio usato qui è solitamente indicato come “copy-on-write”: un accesso in scrittura crea una copia modificata 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 visualizzare i livelli di un’immagine Docker utilizzando il comando “Docker image inspect” sulla riga di comando. Questo comando restituisce un documento JSON che può essere elaborato con lo strumento standard jq:
docker image inspect <image-id> | jq -r '.[].RootFS.Layers[]'
Per unire nuovamente i cambiamenti nei livelli viene usato un file system speciale: l’union file system. Questo sovrappone tutti i livelli per produrre una struttura coerente di cartelle e file sull’interfaccia. Storicamente, sono state usate varie tecnologie conosciute come “storage driver” per implementare l’union file system. Oggi, lo storage driver “overlay2” è raccomandato nella maggior parte dei casi:
Storage driver | Commento |
overlay2 | Raccomandato per l’uso al giorno d’oggi |
aufs, overlay | Usato in versioni precedenti |
È anche possibile visualizzare il driver di archiviazione utilizzato per un’immagine Docker. Per far ciò bisognerà usare il comando “docker image inspect” sulla riga di comando. Questo restituisce 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 è identificato con un hash chiaro calcolato dalle modifiche contenute su di esso. Se due immagini usano lo stesso livello, questo sarà memorizzato in locale solo una volta. Entrambe le immagini useranno quindi lo stesso livello. Questo assicura una memorizzazione locale efficiente e riduce i volumi di trasferimento quando si ottengono le immagini.
Immagine genitore
Un’immagine Docker di solito ha un’“immagine madre” sottostante. Nella maggior parte dei casi, l’immagine madre è definita da una direttiva FROM nel Dockerfile. Quest’immagine definisce una base su cui si basano le immagini derivate. I livelli dell’immagine esistente sono sovrapposti a livelli aggiuntivi.
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 principale dei file? Le sue radici sono determinate da alcune “immagini base” speciali. Nella maggior parte dei casi, un’immagine base è definita con la direttiva “FROM scratch” nel Dockerfile. 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 precedenti, un’immagine Docker è composta da diversi livelli. È quindi possibile utilizzare 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 informazioni sui singoli livelli?
Le informazioni 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 completamente un’immagine Docker e contiene quanto segue:
- Informazioni sulla versione, lo schema e la dimensione
- Gli hash crittografici dei livelli d’immagine utilizzati
- Informazioni sulle architetture dei processori disponibili
Per identificare chiaramente un’immagine Docker, viene creato un hash crittografico 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 specificato nel corso dell’articolo, le immagini Docker sono una parte importante dell’ecosistema Docker. Esistono molti modi diversi per ottenere un’immagine Docker. Vi sono però due metodi di base fondamentali che vedremo più in dettaglio di seguito:
- Estrarre le immagini Docker esistenti da un registro
- 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 piattaforma 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’esecuzione di un comando “docker image pull”.
Vi sono dei registri online accessibili pubblicamente che offrono una vasta selezione di immagini Docker esistenti da utilizzare. In data corrente, vi sono più di otto milioni di immagini Docker liberamente disponibili 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 utilizzare la piattaforma per creare i propri registri di container privati.
Oltre ai registri online di cui sopra, è anche possibile ospitare un registro locale autonomamente. Ad esempio, le grandi organizzazioni spesso usano questa opzione per permettere ai propri team un accesso protetto alle immagini Docker create autonomamente. Docker ha creato il Docker Trusted Registry (DTR) esattamente 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 appositamente adattata a un progetto specifico. Solitamente, è possibile usare un’immagine Docker esistente e adattarla alle proprie esigenze. Ricordate che le immagini Docker sono immutabili e che quando viene fatta una modifica, viene creata una nuova immagine Docker. Esistono diversi modi per creare una nuova immagine Docker:
- Costruire sull’immagine madre con il Dockerfile
- Generarne un’immagine dal container in esecuzione
- Creare una nuova immagine base
L’approccio più comune per creare una nuova immagine Docker è quello di scrivere un Dockerfile. Un Dockerfile contiene comandi speciali che definiscono l’immagine madre e qualsiasi modifica richiesta. Richiamando il comando “docker image build” verrà creata una nuova immagine Docker dal Dockerfile. 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
Storicamente, il termine “immagine” deriva dal cosiddetto “imaging” di un supporto dati. Nel contesto delle macchine virtuali (VM), è possibile creare un’istantanea dell’immagine di una VM in esecuzione. Un processo simile può essere eseguito con Docker. Con il comando “docker commit”, è possibile creare un’immagine di un container in esecuzione come una nuova immagine Docker. Tutte le modifiche apportate al container saranno salvate:
docker commit <container-id>
Inoltre, è possibile passare le istruzioni del Dockerfile utilizzando il comando “docker commit”. In questo caso le modifiche codificate con le istruzioni diventano parte della nuova immagine Docker:
docker commit --change <dockerfile instructions> <container-id>
È inoltre possibile usare il comando “docker image history” per rintracciare quali modifiche sono state fatte a un’immagine Docker in seguito:
docker image history <image-id>
Come anticipato, è possibile basare una nuova immagine Docker su un’immagine madre o sullo stato di un container in esecuzione, ma come si crea una nuova immagine Docker da zero? Esistono due modi diversi per farlo. Una prima opzione prevede l’utilizzo di un Dockerfile con la direttiva speciale “FROM scratch” come descritto sopra. Questo crea una nuova immagine base minima.
Se si preferisce invece non usare l’immagine Docker data dalla direttiva “FROM scratch”, bisognerà ricorrere a uno strumento speciale come debootstrap e preparare una distribuzione Linux. Questa sarà poi impacchettata in un file tarball con il comando tar e importata nell’host Docker locale tramite “docker image import”.
I comandi più importanti per le immagini Docker
Comando immagine Docker | Spiegazione |
docker image build | Crea un’immagine Docker da un Dockerfile |
docker image history | Mostra i passaggi effettuati per la creazione di un’immagine Docker |
docker image import | Crea un’immagine Docker da un file tarball |
docker image inspect | Mostra informazioni dettagliate 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 disponibili sull’host Docker |
docker image prune | Rimuove le immagini Docker inutilizzate 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 |