Minimales Docker Image für Golang

Minimales Docker Image für Golang

Für das Deployment von Microservices haben sich Docker-Container, die jeweils einen Service beinhalten, etabliert. So lassen sich die Services sauber trennen und auch einzeln verwalten.

Container in so solch einem Umfeld sollten nur noch die minimal benötigten Abhängigkeiten beinhalten. Das spart Plattenplatz, Übertragungsgschwindigkeit und ist auch aus Sicherheitsgründen angebracht. Was nicht installiert ist, kann auch nicht als potentielles Eintrittstor verwendet werden.

In diesem Artikel geht es um den Bau eines minimalen Docker Image für die eigene Anwendung. Bei der Beispiel-Anwendung handelt es sich in diesem Fall um eine Go “Hello World” Application, die nur den Bau eines Images zeigen soll.

package main

import "fmt"

func main() {
	fmt.Printf("Hello World\n")
}

Golang Docker Image

Wer für Go Anwendungen Docker Images baut stolpert früher oder später über das offizielle Golang Docker Image. Dies beinhaltet den Go Compiler und kann in vielen Versionen bezogen werden. Die neuste Version erhält man immer mittels:

docker pull golang:latest

Die erste Idee ist natürlich genau dieses Image als Basis für die Anwendung zu benutzen.

FROM golang:latest

COPY *.go .

RUN go build -o app *.go

CMD ["/go/app"]

Das Image läßt sich über folgenden Befehl bauen und als Container ausführen:

$ docker build -t go-first .

$ docker run --rm go-first
Hello World

Das war einfach! Allerdings sieht man, wenn man sich die Größe des Images anschaut:

kkoehler@precision:~$ docker images
REPOSITORY  TAG      IMAGE ID       CREATED    SIZE
go-first    latest   9b8c70ab2d00   <date>     774MB

774 MB! Das kann man eher nicht als minimales Images bezeichnen…

Multistage Build für Go

Go Anwendungen werden statisch gelinkt. Das heißt, dass der Compiler alle Abhängigkeiten in das resultierende Binary packt. Neben dem Binary wird nichts mehr benötigt. Durch dieses Feature ist das Übertragen der gebauten Anwendung auf eine andere Umgebung recht einfach. Das Kopieren dieses Binaries reicht, neben eventueller Konfigurationsdateien, völlig aus.

Die Idee ist also recht einfach: Die Anwendung wird mit einem offiziellen Go-Docker Image gebaut allerdings nicht in dieser ausgeführt. Das soll in einem zweiten, “größenoptimierten”, Container erfolgen. Docker kennt für solche Szenarien ein Feature namens Multistage Build.

Multistage builds are useful to anyone who has struggled to optimize Dockerfiles while keeping them easy to read and maintain.

Bis zur Einführung dieses Features mussten 2 Dockerfiles gepflegt werden. Eines für den Bau der Anwendung, das andere für die Produktionsumgebung. Der Vorgang wurde auch als “Builder Pattern” bezeichnet. Für den Bau der Images wurde dann meist zusätzlich noch ein Shell-Skript benötigt, mit dem man den Ablauf gesteuert hat.

Beim Einsatz eines Multistage-Builds können Dockerfiles mehrere FROM Statements enthalten, die für eine einzelne Stage stehen. Jede Stage kann sich in ihrem FROM Statement auf ein anderes Base-Image basieren. Zwischen den Stages können Daten kopiert werden und somit nur die wirklich benötigten Artefakte von einem Image zum nächsten kopiert werden.

Im Beispiel würde das bedeuten, dass im ersten Container gebaut wird und nur das gebaute Artefakt in das nächste Image kopiert wird. Dieses kann dann “größenoptimiert” sein.

Jede Stage kann einen Namen zugeordnet bekommen, auf den man sich später beziehen kann.

FROM golang as builder

Bei einem späteren Kopieren von Dateien kann angegeben werden von welcher Stage die Dateien kommen sollen.

COPY --from=builder /go/bin/docker-hello .

Für unsere einfache Go Anwendung sieht das fertige Dockerfile dann so aus:

FROM golang as builder

WORKDIR $GOPATH/src/docker-hello

COPY *.go .

RUN CGO_ENABLED=0 go install

# Build the image to run
FROM alpine:latest

COPY --from=builder /go/bin/docker-hello .

CMD ["./docker-hello"]

Beim Einsatz von Alpine-Linux muss die Golang Anwendung mit dem CGO_ENABLED flag übersetzt werden, da Alpine-Linux nicht mit den benötigten glibcs ausgeliefert wird sondern mit musl.

Gebaut werden kann das Image dann mit einem einzigen Befehl:

$ docker build -t go-first-small .

Lässt man sich die Größe des Images anzeigen sieht das schon eher nach einem kleinen Docker Image aus: 7.5MB

kkoehler@precision:~$ docker images
REPOSITORY      TAG      IMAGE ID       CREATED    SIZE
go-first-small  latest   8895c16f2166   <date>     7.54MB

Lauffähig ist das ganze noch immer ;-)

$ docker build -t go-first-small .

$ docker run --rm go-first-small
Hello World

Viel Erfolg beim Dockern!

16.05.2019

 

Der Author auf Twitter: