Un'immagine docker è in realtà un elenco collegato di livelli di filesystem. Ogni istruzione in un Dockerfile crea un livello di file system che descrive le differenze nel file system prima e dopo l'esecuzione dell'istruzione corrispondente. Il docker inspect
sottocomando può essere utilizzato su un'immagine docker per rivelare la sua natura di essere un elenco collegato di livelli di filesystem.
Il numero di livelli utilizzati in un'immagine è importante
- quando si spinge o si estraggono immagini, poiché influisce sul numero di upload o download simultanei che si verificano.
- quando si avvia un contenitore, poiché i livelli vengono combinati insieme per produrre il filesystem utilizzato nel contenitore; più livelli sono coinvolti, peggiore è la prestazione, ma i diversi backend del filesystem ne sono influenzati in modo diverso.
Ciò ha diverse conseguenze su come dovrebbero essere costruite le immagini. Il primo e più importante consiglio che posso dare è:
Consiglio n. 1 Assicurati che i passaggi di compilazione in cui è coinvolto il tuo codice sorgente arrivino il più tardi possibile nel Dockerfile e non siano collegati a comandi precedenti usando a &&
o a ;
.
La ragione di ciò è che tutti i passaggi precedenti verranno memorizzati nella cache e i livelli corrispondenti non dovranno essere scaricati più e più volte. Ciò significa build più veloci e versioni più veloci, che è probabilmente quello che vuoi. È interessante notare che è sorprendentemente difficile fare un uso ottimale della cache docker.
Il mio secondo consiglio è meno importante ma lo trovo molto utile dal punto di vista della manutenzione:
Consiglio n. 2 Non scrivere comandi complessi nel Dockerfile ma piuttosto usare script che devono essere copiati ed eseguiti.
Un file Docker che segue questo consiglio sarebbe simile
COPY apt_setup.sh /root/
RUN sh -x /root/apt_setup.sh
COPY install_pacakges.sh /root/
RUN sh -x /root/install_packages.sh
e così via. Il consiglio di associare diversi comandi con &&
ha solo un ambito limitato. È molto più facile scrivere con gli script, dove è possibile utilizzare le funzioni, ecc. Per evitare ridondanza o per scopi di documentazione.
Persone interessate ai pre-processori e disposte a evitare il piccolo sovraccarico causato dai COPY
passaggi e che in realtà stanno generando al volo un Dockerfile in cui il
COPY apt_setup.sh /root/
RUN sh -x /root/apt_setup.sh
le sequenze sono sostituite da
RUN base64 --decode … | sh -x
dove …
è la versione codificata in base64 di apt_setup.sh
.
Il mio terzo consiglio è per le persone che vogliono limitare le dimensioni e il numero di livelli al possibile costo di build più lunghe.
Consiglio n. 3 Usare with
-idiom per evitare i file presenti nei livelli intermedi ma non nel file system risultante.
Un file aggiunto da alcune istruzioni della finestra mobile e rimosso da alcune istruzioni successive non è presente nel file system risultante ma viene menzionato due volte negli strati della finestra mobile che costituisce l'immagine della finestra mobile nella costruzione. Una volta, con il nome e il contenuto completo nel livello risultante dall'istruzione aggiungendolo, e una volta come avviso di cancellazione nel livello risultante dall'istruzione rimuovendolo.
Ad esempio, supponiamo che abbiamo temporaneamente bisogno di un compilatore C e di qualche immagine e consideriamo il file
# !!! THIS DISPLAYS SOME PROBLEM --- DO NOT USE !!!
RUN apt-get install -y gcc
RUN gcc --version
RUN apt-get --purge autoremove -y gcc
(Un esempio più realistico costruirebbe un software con il compilatore invece di affermare semplicemente la sua presenza con la --version
bandiera.)
Lo snippet Dockerfile crea tre livelli, il primo contiene la suite gcc completa in modo che anche se non è presente nel filesystem finale i dati corrispondenti fanno ancora parte dell'immagine allo stesso modo e devono essere scaricati, caricati e decompressi ogni volta che l'immagine finale è.
Il with
-idiom è una forma comune di programmazione funzionale per isolare la proprietà delle risorse e delle risorse liberando dalla logica utilizzarlo. È facile trasporre questo linguaggio allo shell-scripting e possiamo riformulare i comandi precedenti come il seguente script, da usare con COPY & RUN
come nel Consiglio n. 2.
# with_c_compiler SIMPLE-COMMAND
# Execute SIMPLE-COMMAND in a sub-shell with gcc being available.
with_c_compiler()
(
set -e
apt-get install -y gcc
"$@"
trap 'apt-get --purge autoremove -y gcc' EXIT
)
with_c_compiler\
gcc --version
Comandi complessi possono essere trasformati in funzioni in modo che possano essere inviati al with_c_compiler
. È anche possibile concatenare chiamate di più with_whatever
funzioni, ma forse non è molto desiderabile. (Usando più funzioni esoteriche della shell, è certamente possibile fare with_c_compiler
accettare comandi complessi, ma è sotto tutti gli aspetti preferibile avvolgere questi comandi complessi in funzioni.)
Se vogliamo ignorare il Consiglio n. 2, lo snippet del file Docker risultante sarebbe
RUN apt-get install -y gcc\
&& gcc --version\
&& apt-get --purge autoremove -y gcc
che non è così facile da leggere e mantenere a causa dell'offuscamento. Guarda come la variante shell-script enfatizza la parte importante gcc --version
mentre la &&
variante concatenata seppellisce quella parte nel mezzo del rumore.