Mehrfacher RUN vs. einfach verketteter RUN in Dockerfile, was ist besser?

131

Dockerfile.1führt mehrere aus 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 schließt sich ihnen an:

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

Jeder RUNerstellt eine Ebene, daher habe ich immer angenommen, dass weniger Ebenen besser und damit Dockerfile.2besser sind.

Dies ist offensichtlich der Fall, wenn a RUNetwas entfernt, das von einem vorherigen hinzugefügt wurde RUN(dh yum install nano && yum clean all), aber in Fällen, in denen jeder RUNetwas hinzufügt, müssen wir einige Punkte berücksichtigen:

  1. Ebenen sollten nur einen Unterschied über dem vorherigen hinzufügen. Wenn also die spätere Ebene nicht etwas entfernt, das in einer vorherigen Ebene hinzugefügt wurde, sollte zwischen beiden Methoden nicht viel Speicherplatz gespart werden ...

  2. Ebenen werden parallel von Docker Hub gezogen, sodass sie Dockerfile.1, obwohl sie wahrscheinlich etwas größer sind, theoretisch schneller heruntergeladen werden.

  3. Wenn ein vierter Satz (dh echo This is the D > d) hinzugefügt und lokal neu erstellt wird Dockerfile.1, wird er dank des Caches schneller erstellt, muss jedoch Dockerfile.2alle 4 Befehle erneut ausführen.

Also die Frage: Was ist ein besserer Weg, um eine Docker-Datei zu erstellen?

Yajo
quelle
1
Kann im Allgemeinen nicht beantwortet werden, da es von der Situation und der Verwendung des Bildes abhängt (für Größe, Download-Geschwindigkeit oder Gebäudegeschwindigkeit optimieren)
Henry

Antworten:

99

Wenn möglich, füge ich Befehle, die Dateien erstellen, immer mit Befehlen zusammen, die dieselben Dateien in einer einzigen RUNZeile löschen . Dies liegt daran, dass jede RUNZeile dem Bild eine Ebene hinzufügt. Bei der Ausgabe handelt es sich buchstäblich um die Änderungen des Dateisystems, mit denen Sie docker diffauf dem von ihm erstellten temporären Container anzeigen können . Wenn Sie eine Datei löschen, die in einer anderen Ebene erstellt wurde, registriert das Union-Dateisystem lediglich die Dateisystemänderung in einer neuen Ebene. Die Datei ist noch in der vorherigen Ebene vorhanden und wird über das Netzwerk versendet und auf der Festplatte gespeichert. Wenn Sie also Quellcode herunterladen, extrahieren, in eine Binärdatei kompilieren und am Ende die TGZ- und Quelldateien löschen, möchten Sie wirklich, dass dies alles in einer einzigen Ebene erfolgt, um die Bildgröße zu verringern.

Als Nächstes habe ich Ebenen persönlich aufgeteilt, basierend auf ihrem Potenzial zur Wiederverwendung in anderen Bildern und der erwarteten Caching-Nutzung. Wenn ich 4 Bilder habe, die alle dasselbe Basis-Image haben (z. B. Debian), kann ich eine Sammlung allgemeiner Dienstprogramme für die meisten dieser Bilder in den ersten Ausführungsbefehl ziehen, damit die anderen Bilder vom Caching profitieren.

Die Reihenfolge in der Docker-Datei ist wichtig, wenn Sie die Wiederverwendung des Bildcaches betrachten. Ich sehe mir alle Komponenten an, die sehr selten aktualisiert werden, möglicherweise nur, wenn das Basis-Image aktualisiert wird, und stelle diese hoch oben in die Docker-Datei. Gegen Ende der Docker-Datei füge ich alle Befehle hinzu, die schnell ausgeführt werden und sich häufig ändern können, z. B. das Hinzufügen eines Benutzers mit einer hostspezifischen UID oder das Erstellen von Ordnern und das Ändern von Berechtigungen. Wenn der Container interpretierten Code (z. B. JavaScript) enthält, der aktiv entwickelt wird, wird dieser so spät wie möglich hinzugefügt, sodass bei einer Neuerstellung nur diese einzelne Änderung ausgeführt wird.

In jeder dieser Änderungsgruppen konsolidiere ich so gut ich kann, um Ebenen zu minimieren. Wenn es also 4 verschiedene Quellcode-Ordner gibt, werden diese in einem einzelnen Ordner abgelegt, sodass er mit einem einzigen Befehl hinzugefügt werden kann. Alle Paketinstallationen von apt-get werden nach Möglichkeit zu einem einzigen RUN zusammengeführt, um den Aufwand für den Paketmanager (Aktualisierung und Bereinigung) zu minimieren.


Update für mehrstufige Builds:

Ich mache mir viel weniger Sorgen um die Reduzierung der Bildgröße in den nicht endgültigen Phasen eines mehrstufigen Builds. Wenn diese Phasen nicht markiert und an andere Knoten gesendet werden, können Sie die Wahrscheinlichkeit einer Wiederverwendung des Caches maximieren, indem Sie jeden Befehl in eine separate RUNZeile aufteilen.

Dies ist jedoch keine perfekte Lösung, um Ebenen zu quetschen, da Sie zwischen den Phasen nur die Dateien kopieren und nicht die restlichen Bild-Metadaten wie Einstellungen der Umgebungsvariablen, Einstiegspunkt und Befehl. Wenn Sie Pakete in einer Linux-Distribution installieren, sind die Bibliotheken und andere Abhängigkeiten möglicherweise im gesamten Dateisystem verteilt, was das Kopieren aller Abhängigkeiten schwierig macht.

Aus diesem Grund verwende ich mehrstufige Builds als Ersatz für das Erstellen von Binärdateien auf einem CI / CD-Server, sodass auf meinem CI / CD-Server nur das docker buildTool zum Ausführen erforderlich ist und kein JDK, NodeJS, Go und Alle anderen installierten Kompilierungswerkzeuge.

BMitch
quelle
30

Offizielle Antwort in ihren Best Practices aufgeführt (offizielle Bilder müssen diese einhalten)

Minimieren Sie die Anzahl der Ebenen

Sie müssen das Gleichgewicht zwischen der Lesbarkeit (und damit der langfristigen Wartbarkeit) der Docker-Datei und der Minimierung der Anzahl der verwendeten Ebenen finden. Seien Sie strategisch und vorsichtig in Bezug auf die Anzahl der verwendeten Ebenen.

Da Docker 1.10 die COPY, ADDund RUNAussagen fügen Sie eine neue Ebene zu Ihrem Bild. Seien Sie vorsichtig, wenn Sie diese Aussagen verwenden. Versuchen Sie, Befehle in einer einzigen RUNAnweisung zu kombinieren . Trennen Sie dies nur, wenn es für die Lesbarkeit erforderlich ist.

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

Update: Mehrstufig im Docker> 17.05

Bei mehrstufigen Builds können Sie mehrere FROMAnweisungen in Ihrer Docker-Datei verwenden. Jede FROMAussage ist eine Bühne und kann ein eigenes Basisbild haben. In der letzten Phase verwenden Sie ein minimales Basis-Image wie alpine, kopieren die Build-Artefakte aus früheren Phasen und installieren die Laufzeitanforderungen. Das Endergebnis dieser Phase ist Ihr Image. Hier sorgen Sie sich also um die zuvor beschriebenen Ebenen.

Wie üblich verfügt Docker über hervorragende Dokumente zu mehrstufigen Builds. Hier ist ein kurzer Auszug:

Bei mehrstufigen Builds verwenden Sie mehrere FROM-Anweisungen in Ihrer Docker-Datei. Jeder FROM-Befehl kann eine andere Basis verwenden, und jeder von ihnen beginnt eine neue Phase des Builds. Sie können Artefakte selektiv von einer Stufe in eine andere kopieren und alles, was Sie nicht wollen, im endgültigen Bild zurücklassen.

Einen großartigen Blog-Beitrag dazu finden Sie hier: https://blog.alexellis.io/mutli-stage-docker-builds/

Um Ihre Punkte zu beantworten:

  1. Ja, Ebenen sind wie Unterschiede. Ich glaube nicht, dass Ebenen hinzugefügt werden, wenn es absolut keine Änderungen gibt. Das Problem ist, dass Sie etwas, das Sie in Schicht 2 installiert / heruntergeladen haben, in Schicht 3 nicht mehr entfernen können. Sobald also etwas in eine Ebene geschrieben ist, kann die Bildgröße nicht mehr verringert werden, indem diese entfernt wird.

  2. Obwohl Ebenen parallel gezogen werden können, wodurch sie möglicherweise schneller werden, erhöht jede Ebene zweifellos die Bildgröße, selbst wenn sie Dateien entfernen.

  3. Ja, Caching ist nützlich, wenn Sie Ihre Docker-Datei aktualisieren. Aber es funktioniert in eine Richtung. Wenn Sie 10 Ebenen haben und Ebene 6 ändern, müssen Sie immer noch alles von Ebene 6 bis 10 neu erstellen. Es kommt also nicht allzu oft vor, dass der Erstellungsprozess beschleunigt wird, aber die Größe Ihres Bildes wird garantiert unnötig vergrößert.


Vielen Dank an @Mohan , der mich daran erinnert hat, diese Antwort zu aktualisieren.

Menzo Wijmenga
quelle
1
Dies ist jetzt veraltet - siehe Antwort unten.
Mohan
1
@ Mohan danke für die Erinnerung! Ich habe den Beitrag aktualisiert, um den Benutzern zu helfen.
Menzo Wijmenga
18

Es scheint, dass die obigen Antworten veraltet sind. Anmerkung der Dokumentation:

Vor Docker 17.05 und noch mehr vor Docker 1.10 war es wichtig, die Anzahl der Ebenen in Ihrem Bild zu minimieren. Die folgenden Verbesserungen haben diesen Bedarf gemindert:

[...]

Docker 17.05 und höher bietet Unterstützung für mehrstufige Builds, mit denen Sie nur die benötigten Artefakte in das endgültige Image kopieren können. Auf diese Weise können Sie Tools und Debug-Informationen in Ihre Zwischenerstellungsphasen einbeziehen, ohne das endgültige Image zu vergrößern.

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

und

Beachten Sie, dass in diesem Beispiel auch zwei RUN-Befehle mithilfe des Operators Bash && künstlich komprimiert werden, um zu vermeiden, dass eine zusätzliche Ebene im Bild erstellt wird. Dies ist fehleranfällig und schwer zu warten.

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

Best Practice scheint sich dahingehend geändert zu haben, mehrstufige Builds zu verwenden und das Dockerfiles lesbar zu halten.

Mohan
quelle
Während mehrstufige Builds eine gute Option zu sein scheinen, um das Gleichgewicht zu halten, wird die eigentliche Lösung für diese Frage kommen, wenn die docker image build --squashOption außerhalb des experimentellen Bereichs liegt.
Yajo
2
@Yajo - Ich bin skeptisch squash, ob ich nicht experimentell bin . Es hat viele Gimmicks und machte erst vor mehrstufigen Builds Sinn. Bei mehrstufigen Builds müssen Sie nur die Endstufe optimieren, was sehr einfach ist.
Menzo Wijmenga
1
@Yajo Um das zu erweitern, beeinflussen nur Ebenen in der letzten Phase die Größe des endgültigen Bildes. Wenn Sie also alle Builder-Gubbins in früheren Phasen einsetzen und in der letzten Phase nur Pakete installieren und Dateien aus früheren Phasen kopieren, funktioniert alles wunderbar und Squash wird nicht benötigt.
Mohan
3

Dies hängt davon ab, was Sie in Ihre Bildebenen aufnehmen.

Der entscheidende Punkt ist, so viele Ebenen wie möglich zu teilen:

Schlechtes Beispiel:

Dockerfile.1

RUN yum install big-package && yum install package1

Dockerfile.2

RUN yum install big-package && yum install package2

Gutes Beispiel:

Dockerfile.1

RUN yum install big-package
RUN yum install package1

Dockerfile.2

RUN yum install big-package
RUN yum install package2

Ein weiterer Vorschlag ist, dass das Löschen nur dann nicht so nützlich ist, wenn es auf derselben Ebene wie die Aktion zum Hinzufügen / Installieren erfolgt.

xdays
quelle
Würden diese 2 wirklich den RUN yum install big-packageFrom-Cache teilen ?
Yajo
Ja, sie würden dieselbe Ebene teilen, vorausgesetzt, sie beginnen auf derselben Basis.
Ondra Žižka