In questa sezione verranno esposti i comandi che compongono la sintassi del Dockerfile.
I comandi esposti faranno seguito a quelli che non abbiamo già visto nella sezione Dockerfile: FROM RUN ARG E CMD.

ENTRYPOINT

ENTRYPOINT permette di configurare un container nella sua esecuzione. Dichiarare un comando con ENTRYPOINT significa che esso verrà sempre eseguito dal container, anche se viene sovrascritto CMD passando una stringa da eseguire a docker container run. Questo permette di cambiare a piacimento eventuali altri comportamenti sovrascrivendo CMD ma preservando l’integrità del sistema disposto dal container.

A dire il vero, anche il comando entrypoint può essere sovrascritto ma solo forzatamente attraverso il flag –entrypoint, dal comando docker container run.

Perché un Dockerfile sia valido, deve contenere almeno un comando tra CMD e ENTRYPOINT.

In generale:

  • CMD va utilizzato per definire il comportamento di default, che può essere sovrascritto
  • ENTRYPOINT va utilizzato per eseguire comandi specifici per il container che, salvo precise eccezioni, non devono essere modificati per personalizzazioni

Al fine di padroneggiare al meglio queste due istruzioni, è necessario capire molto bene come interagiscono tra loro. Questo punto in particolare dipende dalla sintassi scelta per la definizione di questi due tag che può essere bash oppure exec.

Nella tabella seguente vediamo come vengono interpretate le combinazioni tra i due comandi nelle varie sintassi. Le etichette nella prima riga si riferiscono al modo in cui viene descritto il comando entrypoint mentre quelle nella prima colonna al modo in cui viene descritto il comando CMD. Per dubbi e curiosità, lascio un link alla documentazione.

No ENTRYPOINTENTRYPOINT exec_entry p1_entryENTRYPOINT [“exec_entry”, “p1_entry”]
No CMDerror, not allowed/bin/sh -c exec_entry p1_entryexec_entry p1_entry
CMD [“exec_cmd”, “p1_cmd”]exec_cmd p1_cmd/bin/sh -c exec_entry p1_entryexec_entry p1_entry exec_cmd p1_cmd
CMD [“p1_cmd”, “p2_cmd”]p1_cmd p2_cmd/bin/sh -c exec_entry p1_entryexec_entry p1_entry p1_cmd p2_cmd
CMD exec_cmd p1_cmd/bin/sh -c exec_cmd p1_cmd/bin/sh -c exec_entry p1_entryexec_entry p1_entry /bin/sh -c exec_cmd p1_cmd

A prima vista questa tabella può disorientare. Cerchiamo di capirne un po’ di più.

In generale vale la regola che la sintassi bash prevale su quella exec.
Come possiamo vedere nella tabella, quando usiamo la sintassi bash, docker antepone alla stringa che gli passiamo il comando per eseguire su una bash tale istruzione, nello specifico, in ambiente unix viene usato ‘/bin/sh -c’ seguito dal comando.

Questa precedenza, anche se sembra solo una formalità, in realtà provoca tante situazioni particolari. Caso lampante di ciò è quello che vediamo nel caso in cui l’entrypoint usa la sintassi bash mentre il CMD in exec, dove il CMD viene ignorato. Questo avviene per la regola della precedenza di bash su entrypoint.

Bash prevale a tal punto da mascherare anche i bash successivi, infatti nella cella immediatamente sottostante a quella appena vista, nel caso in cui entrambi i comandi sono stati descritti in formato bash, viene eseguita solo l’istruzione descritta nell’entrypoint.

Ultimo caso particolare è quello in cui l’entrypoint è in exec e il CMD in bash, il comando risultante viene accodato e non funzionerà perché avrà l’istruzione per lanciare una bash nel mezzo di esso. Un esempio di tale caso è visibile nella cella in basso a destra della tabella.

Personalmente consiglio di utilizzare sempre la modalità exec sia per CMD sia per ENTRYPOINT, questa configurazione è quella che si comporta sempre nel modo atteso ovvero eseguendo ENTRYPOINT e poi CMD. 

Torniamo un attimo sul Dockerfile dell’immagine mysql e poniamo l’attenzione nelle ultime righe, ovvero:

COPY docker-entrypoint.sh /usr/local/bin/
RUN ln -s usr/local/bin/docker-entrypoint.sh /entrypoint.sh # backwards compat
ENTRYPOINT ["docker-entrypoint.sh"]

EXPOSE 3306 33060
CMD ["mysqld"]

Quello che succede è che viene copiato il file docker-entrypoint.sh in una cartella specifica all’interno dell’immagine(/usr/local/bin/), e poi viene creato un link simbolico a livello di root.

All’avvio del container, l’entrypoint provocherà l’esecuzione di tale file (ENTRYPOINT ["docker-entrypoint.sh"]) per poi passare la palla a CMD che avrà la responsabilità di avviare il demone mysql, eseguendo il comando “mysqld” nel container (CMD ["mysqld"]).

EXPOSE

Notiamo che appena prima di lanciare avviare il demone, viene eseguito il comando EXPOSE 3306. Per chi è familiare con mysql saprà che la 3306 è la porta di default sulla quale mysql espone il proprio servizio. 

I container, di default, sono completamente chiusi, ovvero non c’è nessuna porta aperta verso l’esterno. E’ proprio qui che interviene EXPOSE ( link alla documentazione ), ma anche se il nome ci farebbe pensare che la sua mansione si quella di aprire le porte verso l’esterno, in realtà non è così, o meglio lo è solo in parte.

EXPOSE indica che la porta è esposta agli altri container dell’host, e non al di fuori della rete virtuale di Docker.

 Per poterla esporre all’esterno, è necessaria la pubblicazione di quella porta relativamente al container che viene istanziato a partire dall’immagine che stiamo descrivendo. 

E’ la prima volta che compare il concetto di rete virtuale, per ora possiamo semplificare immaginando una rete privata in cui tutti i container possono vedersi e comunicare tra loro. I dettagli saranno più chiari quando parleremo di network.

Questo si fa contestualmente all’avvio del container con il comando docker container run.
Le modalità previste sono due: aperture selettive o aperture generiche.

Aperture selettive

Nel caso delle aperture selettive, si specifica quale porta – ed eventualmente su quale protocollo – viene pubblicata una porta del container verso l’esterno ( link alla documentazione ).
Ho accennato al protocollo perché è possibile specificare sia il protocollo TCP sia il protocollo UDP.
Per farlo è necessario specificare il protocollo in coda alla definizione della porta preceduto da slash nell’istruzione EXPOSE nel Dockerfile; qualora non fosse specificato la porta verrà esposta di default su protocollo TCP.
Se si presenta la necessità di esporre sia il protocollo TCP sia UDP, l’unica soluzione possibile è quella di esplicitare entrambe le diciture come nell’esempio che segue.

EXPOSE 80/tcp
EXPOSE 80/udp

Se volessi pubblicare nella porta 8000 dell’host il servizio che nel container risponde sulla porta 80 utilizzando solo TCP dovrei eseguire:

docker container run -p 8000:80 immagine_xy

Se invece volessi fare lo stesso con entrambi i protocolli dovrei eseguire:

docker container run -p 8000:80/tcp -p 8000:80/udp immagine_xy

Aperture di tutte le porte esposte

Con il flag -P ( –publish-all) è possibile pubblicare tutte le porte esposte nel container nella stessa porta dell’host.
Questo è un approccio che non mi piace molto e che non mi sento di consigliare se non in circostanze particolari perché ha moltissimi effetti collaterali. Tuttavia può essere comodo in fase di sviluppo.

docker container run -P immagine_xy

Supponendo che l’immagine esponga un servizio sulla porta 80, esso verrà pubblicato sulla porta 80 dell’host. Tradotto significa che tutto il flusso in transito sulla porta 80 dell’host verrà dirottato sul container (impedendo di fatto di avere altri servizi sulla porta 80 dell’host).

LABEL

Questo è il comando che Docker mette a disposizione per specificare dei metadati nel Dockerfile ( link alla documentazione ).

La sintassi prevede la definizione di chiave e valore racchiuse tra doppi apici e separati dal carattere uguale. É possibile andare accapo utilizzando il carattere backslash (\).
Riporto un frammento di esempio della documentazione.

LABEL version="1.0" 
LABEL description="This text illustrates \
 that label-values can span multiple lines."

Anche se è tecnicamente possibile farlo, non è più necessario compattare la definizione delle LABEL in un unico blocco perché non cambia la dimensione finale dell’immagine.

Piccola nota a margine, per indicare il mantainer di un progetto, fino a poco tempo fa si utilizzava il comando “MANTAINER” seguito dal nome. Tuttavia tale istruzione è stata deprecata ed al suo posto viene utilizzata proprio una LABEL nella forma LABEL maintainer=”nome del mantainer”

ENV

ENV sta per Environment, rappresenta una variabile d’ambiente (link alla documentazione).

Questo comando è fondamentale perché è utilizzato per impostare chiavi e valori di configurazione per i container. Attenzione però, qui non vanno impostate le chiavi, ma dei valori di default che verranno poi sostituiti per quanto riguarda le chiavi o configurazioni che possono essere pubbliche, ad esempio la versione di mysql.
Se guardiamo il nostro solito dockerfile di mysql, notiamo che la versione di mysql è impostata con una variabile d’ambiente (ENV MYSQL_MAJOR 8.0)

ENV MYSQL_MAJOR 8.0
ENV MYSQL_VERSION 8.0.20-1debian10

Si usano queste perché funzionano bene in ogni sistema operativo. L’immagine dichiara le variabili d’ambiente con un con un valore di default, poi utilizzando docker container run con i flag –env (o -e nella sua forma compatta) vengono specificate quelle corrette.

E’ possibile trovare questo comando con due diverse tipologie di sintassi; entrambe iniziano con la stringa ‘ENV’ seguita dal nome della variabile, ma la prima assegna alla variabile tutto ciò che segue il primo spazio, compresi altri spazi; la seconda invece utilizza il carattere uguale e racchiude il valore della stringa tra doppi apici.

ENV MYSQL_VERSION="8.0.20-1debian10"
ENV MYSQL_VERSION 8.0.20-1debian10


Personalmente consiglio di utilizzare la sintassi con il carattere uguale che risulta più pulita e ordinata. Inoltre non ho trovato più menzione della forma con gli spazi nella documentazione, il che mi fa sospettare che potrebbe subire una deprecazione in futuro.

COPY / ADD

Ci sono poi i comandi COPY ed ADD che si occupano di copiare qualcosa dal filesystem dell’host all’interno dell’immagine (link alla documentazione).

Questi due comandi hanno molto in comune. La differenza principale tra i due sta nel fatto che ADD è una sorta di COPY più evoluto, che permette di copiare risorse anche passando un url o decomprimendo direttamente un file qualora venisse riconosciuto come file compresso. 

L’invocazione più comune prevede di dichiarare ADD oppure COPY, seguiti dal percorso della risorsa da copiare (source path) e dal percorso in cui effettuare la copia (target path).

ADD percorso/sorgente percorso/destinazione

Nella documentazione ufficiale sono presentati alcuni esempi particolarmente interessanti che utilizzano caratteri wildcard e forme più evolute. Per ora ci basta vedere due casi molto semplici ma importanti:

Copia su percorso assoluto

In questo caso viene copiato il file test.txt nella location absoluteDir presente nella root. Il carattere “/” all’inizio del percorso sta a indicare la root.

ADD test.txt /absoluteDir/

Copia su percorso relativo

In questa forma, il file test.txt verrà copiato nella cartella “CartellaXY” del container.

ADD test.txt CartellaXY/

Ma il percorso relativo, da dove parte? Qual è la location dove viene cercata la cartella CartellaXY?
La ricerca viene fatta all’interno della cartella referenziata da WORKDIR.

WORKDIR

WORKDIR è fondamentalmente un puntore alla cartella in cui ci troviamo.

Anche se può creare un po’ di confusione, WORKDIR è anche il nome del comando che utilizziamo per spostare questo indice all’interno del Dockerfile.
In questo caso, si comporta come il comando ‘cd’ di una shell Unix. Si occupa di impostare la cartella corrente per le istruzioni seguenti alla sua definizione nel Dockerfile.

WORKDIR /percorso/della/cartella/in/cui/posizionarsi

ONBUILD

L’istruzione ONBUILD è molto particolare perché non viene eseguita nell’immagine che verrà costruita con il Dockerfile che stiamo definendo. Questo comando verrà eseguito nel nel processo di build delle immagini che utilizzano la nostra immagine come immagine di partenza specificandola nel tag FROM del proprio Dockerfile.

Un esempio di ciò è descritto nella documentazione (qui il link) dove si parla di un servizio in python che ha bisogno di copiare l’applicazione in una cartella specifica (/app/src) per poterla servire:

ONBUILD ADD . /app/src 
ONBUILD RUN /usr/local/bin/python-build --dir /app/src

Per far si che queste istruzioni vengano eseguite in un secondo momento, esse vengono persistite come trigger instruction utilizzando dei metadati. Alla fine della build dell’immagine che stiamo costruendo, i comandi preceduti dalla stringa ONBUILD verranno aggiunti nella sezione “Onbuild” del file manifest dell’immagine (visibile con docker inspect nome_immagine).

Questi comandi verranno eseguiti subito dopo il comando FROM dell’immagine che utilizza la nostra come immagine di partenza e nello stesso ordine nel quale sono stati definiti.

Ci sono solo due regole a cui prestare attenzione:

  1. Non si possono definire comandi con più occorrenze di ONBUILD (es ONBUILD ONBUILD …)
  2. ONBUILD non può invocare FROM e MANTAINER

HEALTHCHECK

Questo è il comando che Docker mette a disposizione per verificare se un container stia ancora funzionando o meno (link alla documentazione).

Quando si definisce un healthcheck per un container, esso viene dotato di uno stato aggiuntivo chiamato “health status” inizializzato a “starting”.
Quando un healthcheck ha successo, viene impostato allo stato “healthy“.
Se per un certo numero di tentativi il controllo fallisce, allora lo stato diventa “unhealthy“.

È possibile definire un solo HEALTHCHECK per Dockerfile. Qualora e venissero specificati molteplici, verrà considerato solo l’ultimo di essi.

L’exit status del comando che invocheremo determina lo status in questo modo:

  • 0: success – il container è ok
  • 1: error – il container non si sta comportando come ci aspettiamo
  • 2: riservato – questo codice non può essere utilizzato

Per la sintassi, riporto l’esempio della documentazione, in quanto semplice ed esaustivo.

HEALTHCHECK --interval=5m --timeout=3s \
 CMD curl -f http://localhost/ || exit 1

Questo controllo di salute del container è un semplice curl all’indirizzo locale che restituisce 1 in caso di fallimento. I flag –interval e –timeout definiscono rispettivamente quanto tempo deve intercorrere tra un tentativo e il successivo e il tempo massimo di attesa della risposta.

STOPSIGNAL

STOPSIGNAL specifica quale sia il signal di sistema da inviare al container oer terminare l’esecuzione (link alla documentazione).

È possibile utilizzare i numeri corrispondenti alla syscall table oppure il formato SIGNAME (maggiori informazioni a questo link).

Nel Dockerfile di nginx, per esempio, di decide di inviare un segnale di arresto al container con il comando “SIGTERM”.Riporto la riga del Dockerfile di seguito:

STOPSIGNAL SIGTERM

SHELL

Questo comando permette di sostituire la shell di default con quella specificata come argomento.

Le shell di default sono:

  • ["/bin/sh", "-c"] per Linux
  • ["cmd", "/S", "/C"] per Windows

per cambiare la shell con la quale eseguire i comandi successivi, la sintassi è la seguente:

SHELL ["executable", "parameters"]

Nella documentazione (disponibile a questo link), viene fatto un esempio con un cambio shell di un’immagine Windows da quella di default a powershell.

La nuova shell deve essere obbligatoriamente definita in modalità json.

SHELL ["powershell", "-command"]

VOLUME

I volumi sono l’ultimo tassello del Dockerfile.
Siccome si tratta di argomento piuttosto vasto e dato che in questa sezione la carne al fuoco è già molta, ho deciso di dedicare una sezione appositamente a questo argomento.


Prosegui su: coming soon

Immagine:  Albero foto creata da wirestock – it.freepik.com