RUN multiplo vs. RUN a catena singola in Dockerfile, cosa è meglio?


132

Dockerfile.1esegue più RUN:

FROM busybox
RUN echo This is the A > a
RUN echo This is the B > b
RUN echo This is the C > c

Dockerfile.2 si unisce a loro:

FROM busybox
RUN echo This is the A > a &&\
    echo This is the B > b &&\
    echo This is the C > c

Ciascuno RUNcrea un livello, quindi ho sempre pensato che meno livelli fosse migliore e quindi Dockerfile.2migliore.

Questo è ovviamente vero quando un RUNrimuove qualcosa aggiunto da un precedente RUN(cioè yum install nano && yum clean all), ma nei casi in cui ognuno RUNaggiunge qualcosa, ci sono alcuni punti che dobbiamo considerare:

  1. Si suppone che i livelli aggiungano semplicemente un diff sopra quello precedente, quindi se il layer successivo non rimuove qualcosa aggiunto in uno precedente, non ci dovrebbe essere molto spazio su disco per risparmiare vantaggio tra i due metodi ...

  2. I livelli vengono estratti in parallelo dall'hub Docker, quindi Dockerfile.1, sebbene probabilmente leggermente più grandi, teoricamente verrebbero scaricati più velocemente.

  3. Se l'aggiunta di una quarta frase (ovvero echo This is the D > d) e la ricostruzione locale, Dockerfile.1comporterebbero più velocemente grazie alla cache, ma Dockerfile.2dovrebbero eseguire nuovamente tutti e 4 i comandi.

Quindi, la domanda: qual è il modo migliore per fare un Dockerfile?


1
Non è possibile rispondere in generale poiché dipende dalla situazione e dall'uso dell'immagine (ottimizzazione per dimensioni, velocità di download o velocità di costruzione)
Henry,

Risposte:


99

Quando possibile, unisco sempre i comandi che creano file con i comandi che eliminano gli stessi file in un'unica RUNriga. Questo perché ogni RUNriga aggiunge un livello all'immagine, l'output è letteralmente le modifiche al filesystem che potresti vedere docker diffsul contenitore temporaneo che crea. Se si elimina un file creato in un livello diverso, tutto il file system dell'unione fa è registrare la modifica del file system in un nuovo livello, il file esiste ancora nel livello precedente e viene spedito in rete e archiviato su disco. Quindi, se scarichi il codice sorgente, lo estrai, lo compili in un file binario e quindi elimini i file tgz e sorgente alla fine, vuoi davvero che tutto ciò avvenga in un singolo livello per ridurre le dimensioni dell'immagine.

Successivamente, ho diviso personalmente i livelli in base al loro potenziale di riutilizzo in altre immagini e al previsto utilizzo della cache. Se ho 4 immagini, tutte con la stessa immagine di base (es. Debian), posso inserire una raccolta di utilità comuni nella maggior parte di quelle immagini nel comando di prima esecuzione in modo che le altre immagini traggano vantaggio dalla memorizzazione nella cache.

L'ordine nel Dockerfile è importante quando si guarda al riutilizzo della cache delle immagini. Guardo tutti i componenti che si aggiorneranno molto raramente, possibilmente solo quando l'immagine di base si aggiorna e li metto in alto nel Dockerfile. Verso la fine del Dockerfile, includo tutti i comandi che verranno eseguiti rapidamente e potrebbero cambiare frequentemente, ad esempio aggiungendo un utente con un UID specifico dell'host o creando cartelle e modificando le autorizzazioni. Se il contenitore include codice interpretato (ad es. JavaScript) che viene sviluppato attivamente, viene aggiunto il più tardi possibile in modo che una ricostruzione esegua solo quella singola modifica.

In ciascuno di questi gruppi di modifiche, mi consolido nel miglior modo possibile per ridurre al minimo i livelli. Quindi, se ci sono 4 diverse cartelle di codice sorgente, queste vengono collocate all'interno di una singola cartella in modo che possa essere aggiunta con un singolo comando. Qualsiasi installazione di pacchetto da qualcosa come apt-get viene unita in un singolo RUN quando possibile per ridurre al minimo la quantità di overhead del gestore pacchetti (aggiornamento e pulizia).


Aggiornamento per build multi-stage:

Mi preoccupo molto meno di ridurre le dimensioni dell'immagine negli stadi non finali di una build a più stadi. Quando queste fasi non sono taggate e spedite ad altri nodi, è possibile massimizzare la probabilità di un riutilizzo della cache suddividendo ciascun comando in una RUNriga separata .

Tuttavia, questa non è una soluzione perfetta per schiacciare i livelli poiché tutto ciò che copi tra le fasi sono i file e non il resto dei metadati dell'immagine come impostazioni delle variabili di ambiente, punto di accesso e comando. E quando installi pacchetti in una distribuzione Linux, le librerie e le altre dipendenze potrebbero essere sparse in tutto il filesystem, rendendo difficile una copia di tutte le dipendenze.

Per questo docker buildmotivo, utilizzo build multi-stage in sostituzione della creazione di file binari su un server CI / CD, in modo che il mio server CI / CD debba solo avere gli strumenti per funzionare , e non avere un jdk, nodejs, go e qualsiasi altro strumento di compilazione installato.


30

Risposta ufficiale elencata nelle loro migliori pratiche (le immagini ufficiali DEVONO aderire a queste)

Ridurre al minimo il numero di layer

È necessario trovare l'equilibrio tra leggibilità (e quindi manutenibilità a lungo termine) del Dockerfile e ridurre al minimo il numero di livelli utilizzati. Sii strategico e cauto riguardo al numero di livelli che usi.

Dal momento che le finestra mobile 1.10 COPY, ADDe RUNle dichiarazioni aggiungere un nuovo livello per la vostra immagine. Sii cauto quando usi queste affermazioni. Prova a combinare i comandi in una singola RUNistruzione. Separare questo solo se è necessario per la leggibilità.

Maggiori informazioni: https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/#/minimize-the-number-of-layers

Aggiornamento: multi stage nella finestra mobile> 17.05

Con build multi-stage è possibile utilizzare più FROMistruzioni nel file Docker. Ogni FROMistruzione è una fase e può avere una propria immagine di base. Nella fase finale usi un'immagine di base minima come Alpine, copia i manufatti di build dalle fasi precedenti e installa i requisiti di runtime. Il risultato finale di questa fase è la tua immagine. Quindi è qui che ti preoccupi dei livelli come descritto in precedenza.

Come al solito, la finestra mobile ha ottimi documenti su build multi-stage. Ecco un breve estratto:

Con build multi-stage, usi più istruzioni FROM nel tuo Dockerfile. Ciascuna istruzione FROM può utilizzare una base diversa e ognuna di esse inizia una nuova fase della build. Puoi copiare selettivamente artefatti da uno stadio all'altro, lasciando dietro di te tutto ciò che non vuoi nell'immagine finale.

Un ottimo post sul blog su questo può essere trovato qui: https://blog.alexellis.io/mutli-stage-docker-builds/

Per rispondere ai tuoi punti:

  1. Sì, i livelli sono simili alle differenze. Non penso che ci siano livelli aggiunti se ci sono assolutamente zero cambiamenti. Il problema è che una volta installato / scaricato qualcosa nel layer # 2, non è possibile rimuoverlo nel layer # 3. Quindi, una volta che qualcosa è scritto in un livello, la dimensione dell'immagine non può più essere ridotta rimuovendola.

  2. Sebbene i livelli possano essere tirati in parallelo, rendendolo potenzialmente più veloce, ogni livello aumenta senza dubbio le dimensioni dell'immagine, anche se stanno rimuovendo i file.

  3. Sì, la memorizzazione nella cache è utile se stai aggiornando il file docker. Ma funziona in una direzione. Se hai 10 livelli e cambi il livello # 6, dovrai comunque ricostruire tutto dal livello # 6- # 10. Quindi non è troppo spesso che accelererà il processo di compilazione, ma è garantito che aumenti inutilmente le dimensioni della tua immagine.


Grazie a @Mohan per avermi ricordato di aggiornare questa risposta.


1
Questo è ora obsoleto - vedi risposta sotto.
Mohan,

1
@Mohan grazie per il promemoria! Ho aggiornato il post per aiutare gli utenti.
Menzo Wijmenga il

19

Sembra che le risposte sopra siano obsolete. La nota dei documenti:

Prima di Docker 17.05, e ancora di più, prima di Docker 1.10, era importante ridurre al minimo il numero di livelli nell'immagine. I seguenti miglioramenti hanno mitigato questa necessità:

[...]

Docker 17.05 e versioni successive aggiungono supporto per build multi-stage, che consentono di copiare solo i manufatti necessari nell'immagine finale. Ciò consente di includere strumenti e informazioni di debug nelle fasi di compilazione intermedie senza aumentare le dimensioni dell'immagine finale.

https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/#minimize-the-number-of-layers

e

Si noti che questo esempio comprime anche artificialmente due comandi RUN insieme usando l'operatore Bash &&, per evitare di creare un livello aggiuntivo nell'immagine. Questo è soggetto a guasti e difficile da mantenere.

https://docs.docker.com/engine/userguide/eng-image/multistage-build/

Le migliori pratiche sembrano essere cambiate nell'uso di build multistadio e nel mantenere la Dockerfileleggibilità.


Mentre le build multistadio sembrano una buona opzione per mantenere l'equilibrio, la soluzione effettiva a questa domanda arriverà quando l' docker image build --squashopzione andrà al di fuori di quella sperimentale.
Yajo,

2
@Yajo - Sono scettico sul superamento squashdell'esperimento. Ha molti espedienti e ha senso solo prima delle build multistadio. Con le build multistadio devi solo ottimizzare la fase finale, il che è molto semplice.
Menzo Wijmenga il

1
@Yajo Per espanderlo, solo i livelli nell'ultima fase fanno la differenza nella dimensione dell'immagine finale. Quindi, se metti tutte le gubbin del tuo costruttore nelle fasi precedenti e hai la fase finale, installa i pacchetti e copia i file delle fasi precedenti, tutto funziona magnificamente e non è necessario lo squash.
Mohan,

3

Dipende da ciò che includi nei livelli dell'immagine.

Il punto chiave è condividere il maggior numero possibile di livelli:

Cattivo esempio:

Dockerfile.1

RUN yum install big-package && yum install package1

Dockerfile.2

RUN yum install big-package && yum install package2

Buon esempio:

Dockerfile.1

RUN yum install big-package
RUN yum install package1

Dockerfile.2

RUN yum install big-package
RUN yum install package2

Un altro suggerimento è che l'eliminazione non è così utile solo se si verifica sullo stesso livello dell'azione di aggiunta / installazione.


Questi 2 condivideranno davvero il contenuto RUN yum install big-packagedella cache?
Yajo

Sì, avrebbero condiviso lo stesso livello, a condizione che iniziassero dalla stessa base.
Ondra Žižka,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.