Il software open source Docker si è affermato come la tec­no­lo­gia standard per la vir­tua­liz­za­zio­ne basata su container, che rap­pre­sen­ta il prossimo passo nell’evo­lu­zio­ne delle macchine virtuali dalle quali, tuttavia, si distingue in modo si­gni­fi­ca­ti­vo. Infatti, invece di simulare un sistema operativo completo, una singola ap­pli­ca­zio­ne è vir­tua­liz­za­ta in un container. Oggi, i container Docker sono uti­liz­za­ti in tutte le fasi del ciclo di vita del software, come lo sviluppo, la fase di testing e l’ese­cu­zio­ne vera e propria.

Esistono vari concetti nell’eco­si­ste­ma Docker. Conoscere e capire questi com­po­nen­ti è es­sen­zia­le per lavorare con Docker in modo efficace. In par­ti­co­la­re, questi includono le immagini Docker, i container Docker e i Doc­ker­fi­le. In questo articolo vi forniamo alcune in­for­ma­zio­ni di base e vi offriamo dei consigli pratici per l’uso.

Cos’è un Doc­ker­fi­le?

Un Doc­ker­fi­le è l’elemento co­sti­tu­ti­vo dell’eco­si­ste­ma Docker, che descrive i passaggi per creare un’immagine Docker. Il flusso di in­for­ma­zio­ni segue un modello centrale: Doc­ker­fi­le > immagine Docker > container Docker.

Un container Docker ha una vita limitata e in­te­ra­gi­sce con il suo ambiente. Pensate a un container come a un organismo vivente, ad esempio un organismo uni­cel­lu­la­re come le cellule di lievito. Seguendo questa analogia, un’immagine Docker è più o meno equi­va­len­te all’in­for­ma­zio­ne genetica. Tutti i container creati da una singola immagine sono gli stessi, proprio come tutti gli organismi uni­cel­lu­la­ri sono clonati dalla stessa in­for­ma­zio­ne genetica. Quindi, come si in­se­ri­sco­no i file Docker in questo modello?

Un Doc­ker­fi­le definisce i passaggi per creare una nuova immagine. È fon­da­men­ta­le 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 cam­bia­men­ti specifici che, tornando al nostro esempio delle cellule di lievito, cor­ri­spon­do­no alle mutazioni genetiche. Un Doc­ker­fi­le specifica due cose per una nuova immagine Docker:

  1. L’immagine di base da cui deriva la nuova immagine. In questo modo la nuova immagine viene ancorata nell’albero ge­nea­lo­gi­co dell’eco­si­ste­ma Docker.
  2. Una serie di modifiche spe­ci­fi­che che di­stin­guo­no la nuova immagine da quella di base.

Come funziona un Doc­ker­fi­le e come si crea un’immagine a partire da questo?

Fon­da­men­tal­men­te, un Doc­ker­fi­le è un normale file di testo. Il Doc­ker­fi­le contiene una serie di istru­zio­ni, ciascuna su una riga separata. Le istru­zio­ni vengono eseguite una dopo l’altra per creare un’immagine Docker. Potreste già avere una certa co­no­scen­za di questa idea seguendo l’ese­cu­zio­ne di uno script di ela­bo­ra­zio­ne batch. Durante l’ese­cu­zio­ne, vengono aggiunti altri livelli all’immagine un po’ alla volta. Vi forniamo una spie­ga­zio­ne più det­ta­glia­ta in merito nel nostro articolo sulle immagini Docker.

Un’immagine Docker viene creata eseguendo le istru­zio­ni contenute in un Doc­ker­fi­le. Questo passaggio è chiamato processo di co­stru­zio­ne 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 co­stru­zio­ne. 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 istru­zio­ni nel Doc­ker­fi­le ottengono quindi l’accesso ai file e alle directory nel contesto di co­stru­zio­ne.

A volte non si vogliono includere tutti i file presenti nella directory di origine locale nel contesto di co­stru­zio­ne. A questo scopo potete usare il file .doc­ke­ri­gno­re, usato per escludere file e directory dal contesto di co­stru­zio­ne. 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’è strut­tu­ra­to un Doc­ker­fi­le?

Un Doc­ker­fi­le è un semplice file di testo chiamato “Doc­ker­fi­le”. Si prega di notare che la prima lettera deve essere maiuscola. Il file contiene una voce per riga. Di seguito ri­por­tia­mo la struttura generale di un Doc­ker­fi­le:

# Commento
ISTRUZIONE argomenti

Oltre ai commenti, i Doc­ker­fi­le con­ten­go­no istru­zio­ni e argomenti che de­scri­vo­no la struttura dell’immagine.

Commenti e direttive parser

I commenti con­ten­go­no in­for­ma­zio­ni destinate prin­ci­pal­men­te agli esseri umani. Ad esempio, su Python, Perl e Ruby, i commenti di un Doc­ker­fi­le iniziano con il segno can­cel­let­to (#). Le righe di commento vengono rimosse durante il processo di co­stru­zio­ne prima di ulteriori ela­bo­ra­zio­ni. Fate at­ten­zio­ne al fatto che solo le righe che iniziano con il segno can­cel­let­to sono ri­co­no­sciu­te come righe di commento.

Qui vi pre­sen­tia­mo un valido esempio:

# La nostra immagine base
FROM busybox

Al contrario, il codice riportato di seguito presenta un errore poiché il segno can­cel­let­to 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 Doc­ker­fi­le, al­tri­men­ti saranno trattate come commenti e rimosse durante la co­stru­zio­ne. È anche im­por­tan­te notare che una data direttiva parser può essere usata solo una volta in un Doc­ker­fi­le.

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 istru­zio­ni su più righe, così come per esprimere caratteri speciali. La direttiva parser “syntax”, invece, specifica le regole che il parser deve uti­liz­za­re per elaborare le istru­zio­ni di Doc­ker­fi­le. Vi forniamo un esempio:

# syntax=docker/dockerfile:1
# escape=\

Istru­zio­ni, argomenti e variabili

Le istru­zio­ni co­sti­tui­sco­no la maggior parte del contenuto del Doc­ker­fi­le, de­scri­vo­no la struttura specifica di un’immagine Docker e vengono eseguite una dopo l’altra. Così come i comandi sulla riga di comando, le istru­zio­ni accettano argomenti. Alcune istru­zio­ni sono di­ret­ta­men­te pa­ra­go­na­bi­li a specifici comandi della riga di comando. Con­se­guen­te­men­te, l’istru­zio­ne COPY che copia file e directory è ap­pros­si­ma­ti­va­men­te equi­va­len­te al comando cp sulla riga di comando. Tuttavia, una dif­fe­ren­za dalla riga di comando è che alcune istru­zio­ni di Doc­ker­fi­le ri­spon­do­no a regole spe­ci­fi­che per la loro sequenza. Inoltre, alcune istru­zio­ni possono apparire solo una volta in un Doc­ker­fi­le.

N.B.

Le istru­zio­ni non devono essere scritte in maiuscolo. Dovreste comunque seguire le regole con­ven­zio­na­li quando si crea un Doc­ker­fi­le.

Per quanto riguarda gli argomenti, è ne­ces­sa­rio fare una di­stin­zio­ne tra parti a codifica fissa e parti variabili. Docker segue la me­to­do­lo­gia della “twelve-factor app” e usa variabili d’ambiente per con­fi­gu­ra­re i container. L’istru­zio­ne ENV è usata per definire le variabili d’ambiente in un Doc­ker­fi­le. Ora, diamo un’occhiata a come assegnare un valore alla variabile d’ambiente.

I valori me­mo­riz­za­ti 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 al­ter­na­ti­va per de­li­mi­ta­re espli­ci­ta­men­te il nome della variabile in cui questo è in­cor­po­ra­to 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 istru­zio­ni più im­por­tan­ti del Doc­ker­fi­le

In questa sezione pre­sen­te­re­mo le istru­zio­ni più im­por­tan­ti del Doc­ker­fi­le. Tra­di­zio­nal­men­te, alcune istru­zio­ni, spe­cial­men­te FROM, potevano apparire solo una volta su ogni Doc­ker­fi­le. Tuttavia, l’in­tro­du­zio­ne delle co­stru­zio­ni a più stadi ha permesso di de­scri­ve­re più immagini in un Doc­ker­fi­le. La re­stri­zio­ne si applica quindi a ogni singolo stadio di co­stru­zio­ne.

Istru­zio­ne De­scri­zio­ne Notazione
FROM Imposta l’immagine di base Deve apparire come prima istru­zio­ne; permette solo un’entrata per ogni fase di co­stru­zio­ne
ENV Imposta le variabili d’ambiente per il processo di co­stru­zio­ne e il runtime del container
ARG Dichiara i parametri della riga di comando per il processo di co­stru­zio­ne Può apparire prima dell’istru­zio­ne FROM
WORKDIR Cambia la directory corrente
USER Cambia l’ap­par­te­nen­za 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 scon­si­glia l’uso
RUN Esegue i comandi nell’immagine durante il processo di co­stru­zio­ne Crea un nuovo livello
CMD Imposta l’argomento di default per l’avvio del container Solo un record per ogni fase di co­stru­zio­ne
EN­TRY­POINT Imposta il comando di default per l’avvio del container Solo un record per ogni fase di co­stru­zio­ne
EXPOSE Definisce le as­se­gna­zio­ni delle porte per il container in ese­cu­zio­ne 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

Istru­zio­ne FROM

L’istru­zio­ne FROM imposta l’immagine di base su cui operano le istru­zio­ni suc­ces­si­ve. Questa istru­zio­ne può esistere solo una volta in ogni fase di co­stru­zio­ne e deve apparire come prima istru­zio­ne. Bisogna tenere a mente un’av­ver­ten­za: l’istru­zio­ne ARG può apparire prima dell’istru­zio­ne FROM. Potete quindi spe­ci­fi­ca­re esat­ta­men­te quale immagine viene usata come immagine di base tramite un argomento della riga di comando quando si inizia il processo di co­stru­zio­ne.

Ogni immagine Docker deve essere basata su un’immagine base. In altre parole, ogni immagine Docker ha esat­ta­men­te 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.

Istru­zio­ni ENV e ARG

Queste due istru­zio­ni assegnano un valore a una variabile. La di­stin­zio­ne tra le due è prin­ci­pal­men­te data dalla pro­ve­nien­za dei valori e il contesto in cui le variabili sono di­spo­ni­bi­li. Esa­mi­nia­mo prima l’istru­zio­ne ARG.

L’istru­zio­ne ARG dichiara una variabile nel Doc­ker­fi­le di­spo­ni­bi­le solo durante il processo di co­stru­zio­ne. Il valore di una variabile di­chia­ra­ta con ARG viene passato come argomento della riga di comando quando il processo di co­stru­zio­ne viene avviato. Di seguito vi mostriamo un esempio in cui si dichiara la variabile di co­stru­zio­ne “user”:

ARG user

Quando si inizia il processo di co­stru­zio­ne, si tra­sfe­ri­sce il valore della variabile:

docker build --build-arg user=admin

Quando si dichiara la variabile, si può scegliere di spe­ci­fi­ca­re un valore pre­de­fi­ni­to. Se all’inizio del processo di co­stru­zio­ne non viene tra­sfe­ri­to un argomento adatto, alla variabile viene dato il valore pre­de­fi­ni­to:

ARG user=tester

Se non si utilizza “--build-arg”, la variabile “user” conterrà il valore pre­de­fi­ni­to “tester”:

docker build

Ora vediamo come definire una variabile d’ambiente usando l’istru­zio­ne ENV. A dif­fe­ren­za dell’istru­zio­ne ARG, una variabile definita con ENV esiste sia durante il processo di co­stru­zio­ne che durante il runtime del container. L’istru­zio­ne ENV può essere scritta in due modi.

  1. Notazione rac­co­man­da­ta:
ENV version="1.0"

2. Notazione al­ter­na­ti­va per re­tro­com­pa­ti­bi­li­tà:

ENV version 1.0
Consiglio

L’istru­zio­ne ENV funziona più o meno come il comando “export” sulla riga di comando.

Istru­zio­ni WORKDIR e USER

L’istru­zio­ne WORKDIR è usata per cambiare le directory durante il processo di co­stru­zio­ne, così come all’avvio del container. Quando viene chiamata, WORKDIR si applica a tutte le istru­zio­ni suc­ces­si­ve. Durante il processo di co­stru­zio­ne vengono in­fluen­za­te le istru­zio­ni RUN, COPY e ADD. Durante l’ese­cu­zio­ne del container, invece, le istru­zio­ni CMD e EN­TRY­POINT.

Consiglio

L’istru­zio­ne WORKDIR è ap­pros­si­ma­ti­va­men­te equi­va­len­te al comando “cd” sulla riga di comando.

L’istru­zio­ne USER è usata per cambiare l’utente corrente (Linux), così come l’istru­zio­ne WORKDIR è usata per cambiare la directory. Potete anche scegliere di definire l’ap­par­te­nen­za al gruppo dell’utente. Quando viene chiamata USER, questa si applica a tutte le istru­zio­ni suc­ces­si­ve. Durante il processo di co­stru­zio­ne, le istru­zio­ni RUN sono in­fluen­za­te dall’ap­par­te­nen­za all’utente e al gruppo. Durante il runtime del container, le istru­zio­ni in­fluen­za­te sono CMD ed EN­TRY­POINT.

Consiglio

L’istru­zio­ne USER è ap­pros­si­ma­ti­va­men­te equi­va­len­te al comando “su” sulla riga di comando.

Istru­zio­ni COPY e ADD

Le istru­zio­ni COPY e ADD sono entrambe uti­liz­za­te per ag­giun­ge­re file e directory all’immagine Docker. Entrambe le istru­zio­ni creano un nuovo livello che viene aggiunto all’immagine esistente. La fonte per l’istru­zio­ne COPY è sempre il contesto di co­stru­zio­ne. Nell’esempio seguente, copiamo un file readme dalla sot­to­di­rec­to­ry “doc” nel contesto di co­stru­zio­ne alla directory “app” di primo livello dell’immagine:

COPY ./doc/readme.md /app/
Consiglio

L’istru­zio­ne COPY è ap­pros­si­ma­ti­va­men­te equi­va­len­te al comando “cp” sulla riga di comando.

L’istru­zio­ne ADD si comporta in modo quasi identico ma può anche re­cu­pe­ra­re risorse URL al di fuori del contesto di co­stru­zio­ne e de­com­pri­me­re i file compressi. Nella pratica, questo può portare a effetti col­la­te­ra­li ina­spet­ta­ti. Pertanto, l’uso dell’istru­zio­ne ADD è espres­sa­men­te scon­si­glia­to. Nella maggior parte dei casi si dovrebbe usare solo l’istru­zio­ne COPY.

Istru­zio­ne RUN

L’istru­zio­ne RUN è una delle istru­zio­ni più comuni di Doc­ker­fi­le. Quando si usa l’istru­zio­ne RUN, Docker viene istruito a eseguire un comando della riga di comando durante il processo di co­stru­zio­ne. Le modifiche ri­sul­tan­ti sono aggiunte all’immagine esistente come un nuovo livello. L’istru­zio­ne RUN può essere scritta in due modi:

  1. Notazione “Shell”: gli argomenti tra­sfe­ri­ti a RUN vengono eseguiti nella shell pre­de­fi­ni­ta dell’immagine. I simboli speciali e le variabili d’ambiente vengono so­sti­tui­ti seguendo le regole della shell. Di seguito ri­por­tia­mo l’esempio di una chiamata che dà il benvenuto all’utente corrente usando una subshell “$()”:
RUN echo "Hello $(whoami)"

2. Notazione “Exec”: invece di tra­sfe­ri­re un comando alla shell, viene ri­chia­ma­to di­ret­ta­men­te un file ese­gui­bi­le. Ulteriori argomenti possono essere tra­sfe­ri­ti 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"]
N.B.

In linea di principio, l’istru­zio­ne RUN può essere usata per so­sti­tui­re alcune altre istru­zio­ni Docker. Ad esempio, la chiamata “RUN cd src” è fon­da­men­tal­men­te equi­va­len­te a “WORKDIR src”. Tuttavia, questo approccio crea dei Doc­ker­fi­le che diventano più difficili da leggere e gestire man mano che le di­men­sio­ni crescono. Dovreste quindi, ove possibile, usare delle istru­zio­ni spe­cia­liz­za­te.

Istru­zio­ne CMD ed EN­TRY­POINT

L’istru­zio­ne RUN esegue un comando durante il processo di co­stru­zio­ne, creando un nuovo livello nell’immagine Docker. Al contrario, le istru­zio­ni CMD ed EN­TRY­POINT eseguono un comando quando il container viene avviato. Vi è tuttavia una sottile dif­fe­ren­za tra le due istru­zio­ni.

  • EN­TRY­POINT è 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 ese­gui­bi­le.
  • CMD si usa invece per creare un container che esegue un’azione definita all’avvio senza ulteriori parametri. L’azione pre­im­po­sta­ta può essere fa­cil­men­te so­vra­scrit­ta da parametri adeguati.

Ciò che entrambe le istru­zio­ni hanno in comune è che possono apparire solo una volta nel Doc­ker­fi­le. Tuttavia, è possibile com­bi­nar­le. In questo caso, EN­TRY­POINT definirà l’azione pre­de­fi­ni­ta da eseguire quando il container viene avviato, mentre CMD definirà i parametri fa­cil­men­te so­vra­scri­vi­bi­li per l’azione.

Il nostro record sul Doc­ker­fi­le:

ENTRYPOINT ["echo", "Hello"]
CMD ["World"]

Di seguito ri­por­tia­mo il comando cor­ri­spon­den­te sulla riga di comando:

# Output "Hello World"
docker run my_image
# Output "Hello Moon"
docker run my_image Moon

Istru­zio­ne EXPOSE

I container Docker co­mu­ni­ca­no at­tra­ver­so la rete. I servizi in ese­cu­zio­ne nel container sono in­di­riz­za­ti tramite porte spe­ci­fi­ca­te. L’istru­zio­ne EXPOSE documenta l’as­se­gna­zio­ne delle porte e supporta i pro­to­col­li TCP e UDP. Quando un container viene avviato con “docker run -P”, il container scansiona le porte definite da EXPOSE. In al­ter­na­ti­va, le porte assegnate possono essere so­vra­scrit­te con “docker run -p”.

Vi pro­po­nia­mo un esempio. Sup­po­nia­mo che il nostro Doc­ker­fi­le contenga le seguenti istru­zio­ni EXPOSE:

EXPOSE 80/tcp
EXPOSE 80/udp

Sono quindi di­spo­ni­bi­li 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

Istru­zio­ne VOLUME

Un Doc­ker­fi­le 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 ne­ces­sa­rio un mec­ca­ni­smo per scambiare dati tra il container in ese­cu­zio­ne e il sistema host. L’istru­zio­ne VOLUME definisce un “punto di montaggio” all’interno del container.

Con­si­de­ria­mo il seguente estratto di Doc­ker­fi­le. Creiamo una directory “shared” nella directory di primo livello dell’immagine e poi spe­ci­fi­chia­mo che questa directory deve essere montata nel sistema host quando il container viene avviato:

RUN mkdir /shared
VOLUME /shared

Notate che non è possibile spe­ci­fi­ca­re il percorso effettivo sul sistema host all’interno del Doc­ker­fi­le. Di default, le directory definite dall’istru­zio­ne VOLUME sono montate sul sistema host sotto “/var/lib/docker/volumes/”.

Come si modifica un Doc­ker­fi­le?

Ricordate che un Doc­ker­fi­le è un file di testo (semplice) e quindi può essere mo­di­fi­ca­to usando i soliti metodi. Un semplice editor di testo è pro­ba­bil­men­te l’opzione più frequente, ad esempio un editor con un’in­ter­fac­cia grafica. Le opzioni sono tante: tra gli editor più popolari si an­no­ve­ra­no VSCode, Sublime Text, Atom e Notepad++. In al­ter­na­ti­va, è di­spo­ni­bi­le un certo numero di editor sulla riga di comando. Oltre agli editor originali Vim e Vi, sono am­pia­men­te uti­liz­za­ti gli editor sem­pli­fi­ca­ti Pico e Nano.

N.B.

Per mo­di­fi­ca­re un file di testo semplice si do­vreb­be­ro uti­liz­za­re esclu­si­va­men­te degli editor adatti a questo scopo. In nessun caso dovreste usare un ela­bo­ra­to­re di testi, come Microsoft Word, Apple Pages, Li­breOf­fi­ce o Ope­nOf­fi­ce, per mo­di­fi­ca­re un file Docker.

Vai al menu prin­ci­pa­le