Docker Tutorial [Teil 9 - Multi-Stage Builds]

Datum

Einer der Vorteile von Containern ist die Größe der Images. Virtuelle Maschinen können eine Größe von ca. 800MB aufwärts erreichen, während ein docker Image meist nur wenige MB groß ist.

Das bekannteste Beispiel ist hierbei das docker Image von Alpine-Linux, welches keine 10 MB groß ist.

Beispiel: docker pull alpine:latest

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
alpine              latest              055936d39205        5 weeks ago         5.53MB

Was bedeutet „Multi-Stage Builds“?

Bei Multi-Stage Builds werden neben den regulären „Zwischenimages“, welche bei jedem Aufruf eines Befehls im Dockerfile erstellt werden, ein oder mehrere Base-Images verwendet und wenn Dateien in einem der Base-Images erstellt wurde können diese in ein anderes Base-Image eingefügt werden.
So kann man ein Image eine Binary kompilieren lassen und dann die fertige Binary in ein anderes Image verschieben. Somit ist das letzte Image, in welches die Binary verschoben wurde theoretisch das kleinste, da dort nicht die Artefakte des Kompilierungsvorganges enthalten sind.

Bauen eines Multi-Stage Builds

Aufbau

Um später auf Dateien aus einem bestimmten Images innerhalb des Multi-Stage Builds zugreifen zu können, wird dem entsprechenden Image per as ein Name zugeordnet, welcher frei wählbar ist.
Beispiel:

FROM centos as mein_build_image
RUN ...

Nun kann man Dateien aus diesem Image in anderes Image kopieren (im gleichen Dockerfile):

FROM alpine
COPY --from=mein_build_image /var/compile /opt

Hierbei kopieren wir den Ordner /var/compile aus dem centos Image in das alpine Image.
Somit erhalten wir am Ende ein kleines alpine Image, welches nur die relevanten Dateien enthält.

Ich zeige hier mal ein praktisches Beispiel, bei welchem ich das in Golang geschriebene Backup-Tool restic (siehe auch: Restic… Ein Loblied) in einem Image kompiliere und die daraus entstehende Binary in einen Alpine-Linux Container verschiebe.
Vorher nochmal ein Beispiel ohne Multi-Stage Build um einen Größenvergleich zu haben.

Beispiel ohne Multi-Stage Builds

Ohne Multi-Stage Builds sieht der Vorgang wie folgt aus:

Dockerfile:

FROM golang as build_image
WORKDIR /tmp
RUN git clone https://github.com/restic/restic.git && cd restic && go run build.go
CMD /tmp/restic/restic version

Hieraus baue ich mir nun per docker build -t simple_stage_image:0.1 . ein entsprechendes Image.

$  docker images
REPOSITORY           TAG                 IMAGE ID            CREATED             SIZE
simple_stage_image   0.1                 92a5d60b80e6        4 minutes ago       1.18GB
golang               latest              9fe4cdc1f173        6 days ago          774MB

Nun haben wir zwar ein Image, welches unsere Binary enthält, aber die Größe ist mit 1,18GB nicht gerade handlich.

Nun testen wir noch die Erstellung eines Containers:

$ docker run -it --rm simple_stage_image:0.1
restic 0.9.5 (v0.9.5-30-g5bd5db42) compiled with go1.12.6 on linux/amd64
Beispiel eines Multi-Stage Builds

Dockerfile:

FROM golang as build_image
WORKDIR /tmp
RUN git clone https://github.com/restic/restic.git && cd restic && go run build.go

FROM alpine
COPY --from=build_image /tmp/restic/restic /bin
ENTRYPOINT ["restic"]
CMD ["version"]

Auch hier baut man das Image ganz regulär per docker build -t multi_stage_image:0.1 .

Und erhalte dann folgendes Image:

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
multi_stage_image   0.1                 1eb76838b610        12 seconds ago      24.6MB

Der Größenunterschied ist schon beachtlich und ein entsprechender Container aus diesem Image funktioniert ordnungsgemäß:

$ docker run -it multi_stage_image:0.1
restic 0.9.5 (v0.9.5-30-g5bd5db42) compiled with go1.12.6 on linux/amd64

Autor
Kategorien container, docker

PRTG Map