Dockerfile: cosa si nasconde dietro al formato?
Il software open source Docker si è affermato come la tecnologia standard per la virtualizzazione basata su container, che rappresenta il prossimo passo nell’evoluzione delle macchine virtuali dalle quali, tuttavia, si distingue in modo significativo. Infatti, invece di simulare un sistema operativo completo, una singola applicazione è virtualizzata in un container. Oggi, i container Docker sono utilizzati in tutte le fasi del ciclo di vita del software, come lo sviluppo, la fase di testing e l’esecuzione vera e propria.
Esistono vari concetti nell’ecosistema Docker. Conoscere e capire questi componenti è essenziale per lavorare con Docker in modo efficace. In particolare, questi includono le immagini Docker, i container Docker e i Dockerfile. In questo articolo vi forniamo alcune informazioni di base e vi offriamo dei consigli pratici per l’uso.
Cos’è un Dockerfile?
Un Dockerfile è l’elemento costitutivo dell’ecosistema Docker, che descrive i passaggi per creare un’immagine Docker. Il flusso di informazioni segue un modello centrale: Dockerfile > immagine Docker > container Docker.
Un container Docker ha una vita limitata e interagisce con il suo ambiente. Pensate a un container come a un organismo vivente, ad esempio un organismo unicellulare come le cellule di lievito. Seguendo questa analogia, un’immagine Docker è più o meno equivalente all’informazione genetica. Tutti i container creati da una singola immagine sono gli stessi, proprio come tutti gli organismi unicellulari sono clonati dalla stessa informazione genetica. Quindi, come si inseriscono i file Docker in questo modello?
Un Dockerfile definisce i passaggi per creare una nuova immagine. È fondamentale capire che tutto inizia sempre con un’immagine di base esistente. L’immagine appena creata succede l’immagine di base. Vi sono anche una serie di cambiamenti specifici che, tornando al nostro esempio delle cellule di lievito, corrispondono alle mutazioni genetiche. Un Dockerfile specifica due cose per una nuova immagine Docker:
- L’immagine di base da cui deriva la nuova immagine. In questo modo la nuova immagine viene ancorata nell’albero genealogico dell’ecosistema Docker.
- Una serie di modifiche specifiche che distinguono la nuova immagine da quella di base.
Come funziona un Dockerfile e come si crea un’immagine a partire da questo?
Fondamentalmente, un Dockerfile è un normale file di testo. Il Dockerfile contiene una serie di istruzioni, ciascuna su una riga separata. Le istruzioni vengono eseguite una dopo l’altra per creare un’immagine Docker. Potreste già avere una certa conoscenza di questa idea seguendo l’esecuzione di uno script di elaborazione batch. Durante l’esecuzione, vengono aggiunti altri livelli all’immagine un po’ alla volta. Vi forniamo una spiegazione più dettagliata in merito nel nostro articolo sulle immagini Docker.
Un’immagine Docker viene creata eseguendo le istruzioni contenute in un Dockerfile. Questo passaggio è chiamato processo di costruzione e viene avviato eseguendo il comando “docker build”. Il “build context” è un concetto centrale. Questo definisce a quali file e directory ha accesso il processo di costruzione. Qui, una directory locale serve come sorgente. Quando viene eseguito il comando “docker build” il contenuto della directory di origine viene passato al demone Docker. Le istruzioni nel Dockerfile ottengono quindi l’accesso ai file e alle directory nel contesto di costruzione.
A volte non si vogliono includere tutti i file presenti nella directory di origine locale nel contesto di costruzione. A questo scopo potete usare il file .dockerignore, usato per escludere file e directory dal contesto di costruzione. Il nome prende spunto dal file .gitignore di Git. Il punto posto all’inizio del nome del file indica che si tratta di un file nascosto.
Com’è strutturato un Dockerfile?
Un Dockerfile è un semplice file di testo chiamato “Dockerfile”. Si prega di notare che la prima lettera deve essere maiuscola. Il file contiene una voce per riga. Di seguito riportiamo la struttura generale di un Dockerfile:
# Commento
ISTRUZIONE argomenti
Oltre ai commenti, i Dockerfile contengono istruzioni e argomenti che descrivono la struttura dell’immagine.
Commenti e direttive parser
I commenti contengono informazioni destinate principalmente agli esseri umani. Ad esempio, su Python, Perl e Ruby, i commenti di un Dockerfile iniziano con il segno cancelletto (#). Le righe di commento vengono rimosse durante il processo di costruzione prima di ulteriori elaborazioni. Fate attenzione al fatto che solo le righe che iniziano con il segno cancelletto sono riconosciute come righe di commento.
Qui vi presentiamo un valido esempio:
# La nostra immagine base
FROM busybox
Al contrario, il codice riportato di seguito presenta un errore poiché il segno cancelletto non è all’inizio della riga:
FROM busybox # La nostra immagine base
Le direttive parser sono un tipo speciale di commento, che si trovano in righe di commento e devono essere poste all’inizio del Dockerfile, altrimenti saranno trattate come commenti e rimosse durante la costruzione. È anche importante notare che una data direttiva parser può essere usata solo una volta in un Dockerfile.
In data corrente, esistono solo due tipi di direttive parser: “syntax” ed “escape”. La direttiva parser “escape” definisce il simbolo di escape da usare. Questa è usata per scrivere istruzioni su più righe, così come per esprimere caratteri speciali. La direttiva parser “syntax”, invece, specifica le regole che il parser deve utilizzare per elaborare le istruzioni di Dockerfile. Vi forniamo un esempio:
# syntax=docker/dockerfile:1
# escape=\
Istruzioni, argomenti e variabili
Le istruzioni costituiscono la maggior parte del contenuto del Dockerfile, descrivono la struttura specifica di un’immagine Docker e vengono eseguite una dopo l’altra. Così come i comandi sulla riga di comando, le istruzioni accettano argomenti. Alcune istruzioni sono direttamente paragonabili a specifici comandi della riga di comando. Conseguentemente, l’istruzione COPY che copia file e directory è approssimativamente equivalente al comando cp sulla riga di comando. Tuttavia, una differenza dalla riga di comando è che alcune istruzioni di Dockerfile rispondono a regole specifiche per la loro sequenza. Inoltre, alcune istruzioni possono apparire solo una volta in un Dockerfile.
Le istruzioni non devono essere scritte in maiuscolo. Dovreste comunque seguire le regole convenzionali quando si crea un Dockerfile.
Per quanto riguarda gli argomenti, è necessario fare una distinzione tra parti a codifica fissa e parti variabili. Docker segue la metodologia della “twelve-factor app” e usa variabili d’ambiente per configurare i container. L’istruzione ENV è usata per definire le variabili d’ambiente in un Dockerfile. Ora, diamo un’occhiata a come assegnare un valore alla variabile d’ambiente.
I valori memorizzati nelle variabili d’ambiente possono essere letti e usati come parti variabili degli argomenti. Per questo scopo viene usata una sintassi speciale che ricorda lo script di shell. Il nome della variabile d’ambiente è preceduto dal segno del dollaro: $env_var. Esiste anche una notazione alternativa per delimitare esplicitamente il nome della variabile in cui questo è incorporato tra parentesi graffe: ${env_var}. Vediamo un esempio concreto:
# impostare variabile ‘user’ sul valore ‘admin’
ENV user="admin"
# impostare nome utente ‘admin_user’
USER ${user}_user
Le istruzioni più importanti del Dockerfile
In questa sezione presenteremo le istruzioni più importanti del Dockerfile. Tradizionalmente, alcune istruzioni, specialmente FROM, potevano apparire solo una volta su ogni Dockerfile. Tuttavia, l’introduzione delle costruzioni a più stadi ha permesso di descrivere più immagini in un Dockerfile. La restrizione si applica quindi a ogni singolo stadio di costruzione.
Istruzione | Descrizione | Notazione |
---|---|---|
FROM | Imposta l’immagine di base | Deve apparire come prima istruzione; permette solo un’entrata per ogni fase di costruzione |
ENV | Imposta le variabili d’ambiente per il processo di costruzione e il runtime del container | — |
ARG | Dichiara i parametri della riga di comando per il processo di costruzione | Può apparire prima dell’istruzione FROM |
WORKDIR | Cambia la directory corrente | — |
USER | Cambia l’appartenenza a utenti e gruppi | — |
COPY | Copia file e directory nell’immagine | Crea un nuovo livello |
ADD | Copia file e directory nell’immagine | Crea un nuovo livello; se ne sconsiglia l’uso |
RUN | Esegue i comandi nell’immagine durante il processo di costruzione | Crea un nuovo livello |
CMD | Imposta l’argomento di default per l’avvio del container | Solo un record per ogni fase di costruzione |
ENTRYPOINT | Imposta il comando di default per l’avvio del container | Solo un record per ogni fase di costruzione |
EXPOSE | Definisce le assegnazioni delle porte per il container in esecuzione | Le porte devono essere esposte quando si avvia il container |
VOLUME | Include la directory nell’immagine come volume quando si avvia il container nel sistema host | — |
Istruzione FROM
L’istruzione FROM imposta l’immagine di base su cui operano le istruzioni successive. Questa istruzione può esistere solo una volta in ogni fase di costruzione e deve apparire come prima istruzione. Bisogna tenere a mente un’avvertenza: l’istruzione ARG può apparire prima dell’istruzione FROM. Potete quindi specificare esattamente quale immagine viene usata come immagine di base tramite un argomento della riga di comando quando si inizia il processo di costruzione.
Ogni immagine Docker deve essere basata su un’immagine base. In altre parole, ogni immagine Docker ha esattamente un’immagine madre. Questo porta al classico paradosso dell’uovo e la gallina poiché deve esserci un punto di partenza. Nell’universo Docker, il punto di partenza è l’immagine “scratch”. Questa immagine minima è alla base di qualsiasi immagine Docker.
Istruzioni ENV e ARG
Queste due istruzioni assegnano un valore a una variabile. La distinzione tra le due è principalmente data dalla provenienza dei valori e il contesto in cui le variabili sono disponibili. Esaminiamo prima l’istruzione ARG.
L’istruzione ARG dichiara una variabile nel Dockerfile disponibile solo durante il processo di costruzione. Il valore di una variabile dichiarata con ARG viene passato come argomento della riga di comando quando il processo di costruzione viene avviato. Di seguito vi mostriamo un esempio in cui si dichiara la variabile di costruzione “user”:
ARG user
Quando si inizia il processo di costruzione, si trasferisce il valore della variabile:
docker build --build-arg user=admin
Quando si dichiara la variabile, si può scegliere di specificare un valore predefinito. Se all’inizio del processo di costruzione non viene trasferito un argomento adatto, alla variabile viene dato il valore predefinito:
ARG user=tester
Se non si utilizza “--build-arg”, la variabile “user” conterrà il valore predefinito “tester”:
docker build
Ora vediamo come definire una variabile d’ambiente usando l’istruzione ENV. A differenza dell’istruzione ARG, una variabile definita con ENV esiste sia durante il processo di costruzione che durante il runtime del container. L’istruzione ENV può essere scritta in due modi.
- Notazione raccomandata:
ENV version="1.0"
2. Notazione alternativa per retrocompatibilità:
ENV version 1.0
L’istruzione ENV funziona più o meno come il comando “export” sulla riga di comando.
Istruzioni WORKDIR e USER
L’istruzione WORKDIR è usata per cambiare le directory durante il processo di costruzione, così come all’avvio del container. Quando viene chiamata, WORKDIR si applica a tutte le istruzioni successive. Durante il processo di costruzione vengono influenzate le istruzioni RUN, COPY e ADD. Durante l’esecuzione del container, invece, le istruzioni CMD e ENTRYPOINT.
L’istruzione WORKDIR è approssimativamente equivalente al comando “cd” sulla riga di comando.
L’istruzione USER è usata per cambiare l’utente corrente (Linux), così come l’istruzione WORKDIR è usata per cambiare la directory. Potete anche scegliere di definire l’appartenenza al gruppo dell’utente. Quando viene chiamata USER, questa si applica a tutte le istruzioni successive. Durante il processo di costruzione, le istruzioni RUN sono influenzate dall’appartenenza all’utente e al gruppo. Durante il runtime del container, le istruzioni influenzate sono CMD ed ENTRYPOINT.
L’istruzione USER è approssimativamente equivalente al comando “su” sulla riga di comando.
Istruzioni COPY e ADD
Le istruzioni COPY e ADD sono entrambe utilizzate per aggiungere file e directory all’immagine Docker. Entrambe le istruzioni creano un nuovo livello che viene aggiunto all’immagine esistente. La fonte per l’istruzione COPY è sempre il contesto di costruzione. Nell’esempio seguente, copiamo un file readme dalla sottodirectory “doc” nel contesto di costruzione alla directory “app” di primo livello dell’immagine:
COPY ./doc/readme.md /app/
L’istruzione COPY è approssimativamente equivalente al comando “cp” sulla riga di comando.
L’istruzione ADD si comporta in modo quasi identico ma può anche recuperare risorse URL al di fuori del contesto di costruzione e decomprimere i file compressi. Nella pratica, questo può portare a effetti collaterali inaspettati. Pertanto, l’uso dell’istruzione ADD è espressamente sconsigliato. Nella maggior parte dei casi si dovrebbe usare solo l’istruzione COPY.
Istruzione RUN
L’istruzione RUN è una delle istruzioni più comuni di Dockerfile. Quando si usa l’istruzione RUN, Docker viene istruito a eseguire un comando della riga di comando durante il processo di costruzione. Le modifiche risultanti sono aggiunte all’immagine esistente come un nuovo livello. L’istruzione RUN può essere scritta in due modi:
- Notazione “Shell”: gli argomenti trasferiti a RUN vengono eseguiti nella shell predefinita dell’immagine. I simboli speciali e le variabili d’ambiente vengono sostituiti seguendo le regole della shell. Di seguito riportiamo l’esempio di una chiamata che dà il benvenuto all’utente corrente usando una subshell “$()”:
RUN echo "Hello $(whoami)"
2. Notazione “Exec”: invece di trasferire un comando alla shell, viene richiamato direttamente un file eseguibile. Ulteriori argomenti possono essere trasferiti durante il processo. Vi mostriamo un esempio di chiamata che invoca lo strumento di sviluppo “npm” e ordina di eseguire lo script “build”:
CMD ["npm", "run", " build"]
In linea di principio, l’istruzione RUN può essere usata per sostituire alcune altre istruzioni Docker. Ad esempio, la chiamata “RUN cd src” è fondamentalmente equivalente a “WORKDIR src”. Tuttavia, questo approccio crea dei Dockerfile che diventano più difficili da leggere e gestire man mano che le dimensioni crescono. Dovreste quindi, ove possibile, usare delle istruzioni specializzate.
Istruzione CMD ed ENTRYPOINT
L’istruzione RUN esegue un comando durante il processo di costruzione, creando un nuovo livello nell’immagine Docker. Al contrario, le istruzioni CMD ed ENTRYPOINT eseguono un comando quando il container viene avviato. Vi è tuttavia una sottile differenza tra le due istruzioni.
- ENTRYPOINT è usata per creare un container che esegue sempre la stessa azione quando viene avviato. Quindi, in questo caso, il container si comporta come un file eseguibile.
- CMD si usa invece per creare un container che esegue un’azione definita all’avvio senza ulteriori parametri. L’azione preimpostata può essere facilmente sovrascritta da parametri adeguati.
Ciò che entrambe le istruzioni hanno in comune è che possono apparire solo una volta nel Dockerfile. Tuttavia, è possibile combinarle. In questo caso, ENTRYPOINT definirà l’azione predefinita da eseguire quando il container viene avviato, mentre CMD definirà i parametri facilmente sovrascrivibili per l’azione.
Il nostro record sul Dockerfile:
ENTRYPOINT ["echo", "Hello"]
CMD ["World"]
Di seguito riportiamo il comando corrispondente sulla riga di comando:
# Output "Hello World"
docker run my_image
# Output "Hello Moon"
docker run my_image Moon
Istruzione EXPOSE
I container Docker comunicano attraverso la rete. I servizi in esecuzione nel container sono indirizzati tramite porte specificate. L’istruzione EXPOSE documenta l’assegnazione delle porte e supporta i protocolli TCP e UDP. Quando un container viene avviato con “docker run -P”, il container scansiona le porte definite da EXPOSE. In alternativa, le porte assegnate possono essere sovrascritte con “docker run -p”.
Vi proponiamo un esempio. Supponiamo che il nostro Dockerfile contenga le seguenti istruzioni EXPOSE:
EXPOSE 80/tcp
EXPOSE 80/udp
Sono quindi disponibili i seguenti modi per attivare le porte all’avvio del container:
# Container ascolta il traffico TCP/UDP sulla porta 80
docker run -P
# Container ascolta il traffico TCP/UDP sulla porta 81
docker run -p 81:81/tcp
Istruzione VOLUME
Un Dockerfile definisce un’immagine Docker che consiste in vari livelli compilati l’uno sull’altro. I livelli sono di sola lettura di modo che, quando un container viene avviato, sia sempre garantito lo stesso stato. Sarà dunque necessario un meccanismo per scambiare dati tra il container in esecuzione e il sistema host. L’istruzione VOLUME definisce un “punto di montaggio” all’interno del container.
Consideriamo il seguente estratto di Dockerfile. Creiamo una directory “shared” nella directory di primo livello dell’immagine e poi specifichiamo che questa directory deve essere montata nel sistema host quando il container viene avviato:
RUN mkdir /shared
VOLUME /shared
Notate che non è possibile specificare il percorso effettivo sul sistema host all’interno del Dockerfile. Di default, le directory definite dall’istruzione VOLUME sono montate sul sistema host sotto “/var/lib/docker/volumes/”.
Come si modifica un Dockerfile?
Ricordate che un Dockerfile è un file di testo (semplice) e quindi può essere modificato usando i soliti metodi. Un semplice editor di testo è probabilmente l’opzione più frequente, ad esempio un editor con un’interfaccia grafica. Le opzioni sono tante: tra gli editor più popolari si annoverano VSCode, Sublime Text, Atom e Notepad++. In alternativa, è disponibile un certo numero di editor sulla riga di comando. Oltre agli editor originali Vim e Vi, sono ampiamente utilizzati gli editor semplificati Pico e Nano.
Per modificare un file di testo semplice si dovrebbero utilizzare esclusivamente degli editor adatti a questo scopo. In nessun caso dovreste usare un elaboratore di testi, come Microsoft Word, Apple Pages, LibreOffice o OpenOffice, per modificare un file Docker.