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