Docker Multi Stage Builds

Le multi-stage builds est une fonctionnalité très intéressante présente dans Docker depuis la release 17.05.

Elle permet de décrire dans un dockerfile 2 images l’une à la suite de l’autre. La première image ne sert qu’à constuire les données nécessaires à l’utilisation de la deuxième. Par exemple pour un programme en golang on aura besoin que la première image possède le compilateur go, git, et diverses bibliothèques qui permettront de générer le binaire executable. L’environnement de compilation peut être bien plus lourd pour une stack Java…

Bref, jusqu’à présent ces outils étaient déployés dans une seule image mise en production. Le multi-stage build permet d’utiliser une image jetable qui va servir à la compilation de son service, ensuite il suffira de copier les exécutables générés dans une images vide bien plus légère. De plus cela permet de réduire très fortement la surface d’attaque puisqu’il n’y a plus d’environnement de compilation en production.

Voici un exemple avec mon précédent Dockerfile de drone server :

FROM arm64v8/golang:1.9.2-alpine
MAINTAINER Frederic Logier <fredix@protonmail.com>

RUN apk add -U --no-cache ca-certificates git sqlite-dev build-base

RUN go get github.com/drone/drone/cmd/drone-server

ENV DATABASE_DRIVER=sqlite3
ENV DATABASE_CONFIG=/var/lib/drone/drone.sqlite
ENV GODEBUG=netdns=go
ENV XDG_CACHE_HOME /var/lib/drone

EXPOSE 8000 9000 80 443
ENTRYPOINT ["drone-server"]

Ce Dockerfile utilise une image alpine qui contient le compilateur Go. J’y ajoute les paquets git slite-dev et build-base afin de pouvoir récupérer le code source et le compiler. A la fin le service est lancé.
Voici la version qui utilise le multi-stage :

FROM arm64v8/golang:1.9.2-alpine as builder
RUN apk add -U --no-cache git build-base sqlite-dev
RUN go get github.com/drone/drone/cmd/drone-server

FROM arm64v8/alpine:3.7
MAINTAINER Frederic Logier <fredix@protonmail.com>

COPY --from=builder /go/bin/drone-server /usr/bin/

RUN apk add -U --no-cache ca-certificates

ENV DATABASE_DRIVER=sqlite3
ENV DATABASE_CONFIG=/var/lib/drone/drone.sqlite
ENV GODEBUG=netdns=go
ENV XDG_CACHE_HOME /var/lib/drone

EXPOSE 8000 9000 80 443

ENTRYPOINT ["drone-server"]

Le mot clé as builder est ajouté à la première image. Une fois les paquets de développement ajoutés, le go get récupère le code, le compile et génère le binaire dans /go/bin.
La deuxième image utilise la même architecture arm64/alpine, ensuite avec COPY je récupère le binaire généré par la précédente image vers la nouvelle. Il ne reste qu’à ajouter un paquet nécessaire au service, créer les variables d’environnements, exposer les ports et lancer le service.

Je suis ainsi passé d’une image de 196Mo à … 11Mo. la preuve dans mon dépôt : https://hub.docker.com/r/fredix/arm64v8-alpine-drone-server/tags/ la version tagué 0.8.2 avec l’ancien Dockerfile et la latest (drone 0.8.3) avec le nouveau.

Un autre exemple de Vincent RABAH : Comment utiliser Docker multi-stage build avec Golang