Schlanke Docker-Images mit dem Multi-Stage-Build

Ein großes Thema bei der diesjährigen DockerCon in Austin (Texas) waren zu große Docker-Images. Der neue Multi-Stage-Build schafft Abhilfe.

Die Folge von großen Docker-Images sind beispielsweise Entwicklungsumgebungen, die sich nur langsam Aufsetzen lassen, was die Entwicklung bremst oder Produktivumgebungen, die durch das regelmäßige Ausrollen den Speicherplatz und die Bandbreite belasten. Sobald man Images von A nach B bewegen möchte (docker pull/push) kommt zudem HTTP ins Spiel, was unter der Haube genutzt wird. Mit großen Images rennt man also im schlimmsten Fall sogar in Timeouts und es geht gar nichts mehr. Mit allerhand Werkzeugen bespickte Docker-Images sorgen zudem für eine Vielzahl von Angriffsvektoren in Produktion.

Mit Version 17.05 (Edge) hält ein Feature namens Multi-Stage-Build Einzug in Docker, welches im Dockerfile Anwendung findet. Damit ist es möglich, mehrere Stages innerhalb eines Dockerfiles zu definieren und Projektdateien von einer Stage zur nächsten zu kopieren. Zusammen mit der Möglichkeit, in jeder Stage ein anderes Image als Basis zu nutzen, ergibt sich dadurch die Möglichkeit, die Abhängigkeiten, die es zum Kompilieren/Bauen benötigt, von den Abhängigkeiten, die es zur Laufzeit braucht, zu trennen.

Was heißt das nun konkret?

Nehmen wir ein einfaches Web-Projekt, welches aus statischen Dateien, wie CSS-, JS-Dateien und ein paar Bildern besteht. Dieses benötigt zur Laufzeit lediglich einen Webserver, wie nginx oder Apache. Benötigt es für die Entwicklung von CSS nun aber noch einen Präprozessor wie SASS und das JavaScript wird aufgrund seiner Komplexität mit einem modernen SPA-Framework und/oder TypeScript umgesetzt, holt man sich ganz schnell NodeJS als Abhängigkeit mit ins Boot. NodeJS wird nun aber vielleicht zur Laufzeit gar nicht benötigt. Um diesem Szenario Rechnung zu tragen, entstanden häufig mehrere Dockerfiles oder Images wurden mit Interpretern überladen, die es zur Laufzeit gar nicht brauchte.

Beispiel bitte!

Demonstrieren möchte ich die Multi-Stage-Builds bei einem Golang-Projekt, denn auch hier ist die Abhängigkeit zum Bauen der Applikation eine andere, als zur Laufzeit. Den mächtigen Go-Interpreter, der selbst als schmales Alpine-Image noch 78 MB frisst, braucht es nur zum Bauen, anschließend ist die App allein lauffähig. Bei dem fiktiven Projekt handelt es sich um einen einfachen HTTP-Server. Das Dockerfile sieht so aus:

# Stage: Build
FROM golang:1.8-alpine as build
COPY src /usr/src/app
RUN cd /usr/src/app && \
    go build

# Stage: Run
FROM alpine:latest
WORKDIR /root/
COPY --from=build /usr/src/app/app .
RUN chmod +x app
CMD ["./app"]
EXPOSE 80

Was passiert da?

Zunächst wird das offizielle Golang-Image vom Docker Hub geladen. Die Besonderheit in der ersten FROM-Zeile ist die Definition eines Alias mit as build. Der Name ist frei definierbar und findet später Verwendung. Anschließend wird der eigene Source-Code in das Image kopiert und mit go build kompiliert. Das Resultat ist diese Datei: /usr/src/app/app.

Mit dem zweiten FROM wird die zweite Stage eingeleitet, die nun nur noch ein schmales Alpine-Linux OHNE Go-Interpreter nutzt. Nach einem Wechsel des Arbeitsverzeichnisses folgt nun der entscheidende Schritt. Wir kopieren nun, aus der vorigen benannten Stage, die kompilierte Datei. Dies geschieht mit dem bekannten Befehl COPY, der neben dem Quell- und Zielpfad noch den Zusatz --from=build erhält, mit dem wir auf die vorige Stage verweisen, aus der wir die Quelldatei kopieren. Abschließend nur noch ein paar Anweisungen, um die App lauffähig zu bekommen. Das Dockerfile lässt sich, wie gewohnt, mit „docker build“ bauen. Hier ändert sich also nichts. Das Ergebnis ist ein Docker Image basierend auf einem Alpine-Linux und der ausführbaren App-Datei. In meinem Fall resultiert das in einer Image-Größe von 18 MB (!).

But wait a minute!

Nützt mir das auch etwas bei Skriptsprachen, wie PHP, die eine große Laufzeitumgebung benötigen? Selbstverständlich braucht man die Laufzeitumgebung, aber über Werkzeuge, wie Git und Composer, die nur dazu genutzt werden, um Abhängigkeiten zur Build-Zeit aufzulösen, werden im Betrieb nicht benötigt. Man sollte sich nur bei der Entwicklung keine Steine in den Weg legen, indem man auf essentielle Werkzeuge verzichtet. Meine Empfehlung wäre, zunächst die Best Practices zu befolgen, bevor man diesen Weg verfolgt. Abschließend lässt sich festhalten, dass die Multi-Stage-Builds eine sinnvolle Ergänzung zum Bau schlanker Images darstellen.

Patrick Baber

Als erfahrener Programmierer löst Patrick jeden Gordischen Knoten, findet die geeignete Methode und entwickelt damit flexible Software-Systeme.