Docker multi-stage

Jun 18, 2017 · 885 words · 5 minutes read docker spring java

Depuis la version 17.05, Docker propose une nouvelle fonctionnalité : le build Multistage, ce qui rendra le build d’application plus attrayant et lisible.

Le but sera de contruire une application via Maven (image de taille imposante : 620 Mo), puis de construire une image légère ne contenant que le JRE (image légère : 81,4 Mo sans l’application - OpenJDK ne contenant que le JRE et basé sur alpine).

L’application

L’application est juste le support à cet exemple, les sources sont disponible au niveau du repos github :: ptitbob/Dockerfile-multi-stage_server.

Elle expose 2 méthodes, qui renvoient soit pong, soit le numéro de version. Que vous pouvez atteindre via cURL (curl -i localhost:8080/ping & curl -i localhost:8080/ping/version) ou par HTTPie (http :8080/ping & http :8080/ping/version)

Le repo possède 2 branches et pour la branche principale 2 tags de version qui seront utilisés pour les deux exemples sur le build multistage.

Dockerfile lié aux sources

La première des solutions est d’intégrer le Dockerfile directement aux sources. C’est le cas de la branche docker des sources de l’application.

Le Dockerfile est constitué de deux partie (source complet du Dockerfile) :

La première permet le build :

  • On fixe l’image d’origine, et on nomme cette étape builder (... as builder)
  • Nous nous plaçons dans le repertoire de travail.
  • On copie le fichier projet maven puis les sources.
  • On lance via un RUN le packaging du projet.

Le seconde partie…

… va permettre de créer l’image finale de notre application.

  • On prend comme base l’image contenant le JRE et on se place dans le repertoire root
  • On copie depuis l’image temporaire qui a servi à faire le build le jar créé dans le repertoire root de l’image
  • et le plus simplement du monde, on déclare la commande de lancement de l’application.

Pour lancer le build, rien de plus simple :

docker build -t shipstone/multistage:0.9.0-docker .

Une fois le build executé, vous pouvez voir l’image créée ainsi que l’image anonyme ayant servi à construire l’application :

[~]$ docker image ls
REPOSITORY                        TAG                 IMAGE ID            CREATED             SIZE
shipstone/multistage              0.9.0-docker        fbeb9b0781da        31 seconds ago      95.9MB
<none>                            <none>              b85a5ce074da        34 seconds ago      635MB
openjdk                           8u131-jre-alpine    58ce9579eac6        2 weeks ago         81.4MB
maven                             3.5.0-jdk-8         66091267e43d        4 weeks ago         620MB

Vous pouvez supprimer l’image anonyme via un petit docker image prune des familles. On remarque aussi que l’image créée fait la taille de l’image du jre plus la taille de l’application. La taille de l’image maven n’apparait pas :)

Cependant, cela pose un petit problème : le fichier Dockerfile est lié au sources, ce qui n’est pas satisfaisant et surtout sujet à des erreurs de manipulations. Une solution simple existe, créer un Dockerfile excentré.

Dockerfile séparé

La solution que j’ai implémenté est simple, mon nouveau Dockerfile, hors des sources, télécharge les sources de l’application depuis le repo git ( qui est présent par défaut dans toutes les bonne distri ;) ). Et histoire d’être le plus fléxible possible, je variabilise la version. Version qui est symbolisé dans mon application (branche princiale) par des tags (1.0.0 & 1.1.0).

Le Dockerfile est sensiblement le même que précédement, il est exposé dans le repo github :: ptitbob/Dockerfile-multi-stage_dockerfile :

Il prends en argument le nom du tag de version (Je vous l’accorde, on peut aller nettement plus loin)

Je déclare une variable de ligne de comande Docker version

ARG version qui pourra être appelé dans la ligne de commande: --build-arg version=1.0.0

Je fixe une variable d’envirronement avec la version passé en paramètre, et si elle n’est pas fixée elle prendra alors la valeur 1.0.0

ENV version ${version:-1.0.0}

Il ne me reste plus qu’a cloner le tag et lancer ensuite le build de l’application cloné dans le repertoire.

Comme je n’ai pas configuré de nommage standard pour l’application, le fichier executable est versionné, et donc je vais redéclarer une variable de build et d’envirronement comme pour l’image de build, sauf que je construits le nom du fichier à recupérer depuis l’image de build. That’a all !!

Pour lancer le build (penser à bien nommer vos tag) :

$ docker build -t shipstone/multistage:1.0.0 -t shipstone/multistage:latest .

équivalent à :

docker build --build-arg version=1.0.0 -t shipstone/multistage:1.0.0 -t shipstone/multistage:latest .

Et ensuite pour la version 1.1.0 :

docker build --build-arg version=1.1.0 -t shipstone/multistage:1.1.0 -t shipstone/multistage:latest .

Je construit volontairement deux tag d’image (qui référence en réalité la même image), le tag de version de mon application et le tag de dernier build (latest).

Maintenant j’ai comme images (j’ai fait un docker image prune avant histoire de supprimer les images de build) :

docker image ls
REPOSITORY                        TAG                 IMAGE ID            CREATED             SIZE
shipstone/multistage              1.1.0               f00b35c1f6e6        15 seconds ago      95.9MB
shipstone/multistage              latest              f00b35c1f6e6        15 seconds ago      95.9MB
shipstone/multistage              1.0.0               7ea6490f40f4        21 minutes ago      95.9MB
shipstone/multistage              0.9.0-docker        fbeb9b0781da        23 hours ago        95.9MB
openjdk                           8u131-jre-alpine    58ce9579eac6        2 weeks ago         81.4MB
maven                             3.5.0-jdk-8         66091267e43d        4 weeks ago         620MB

Et voilà, vous avez un Dockerfile générique pour votre application, afin de la builder et la rendre disponible empacké gentiment dans un image basé sur une distri légère.

Pour créer un container basé sur la dernière image et l’éxécuter, simple :

docker run -p 8080:8080 --name multistage shipstone/multistage:latest

équivalent à :

docker run -p 8080:8080 --name multistage shipstone/multistage:1.1.0

On pourrait aller beaucoup plus loin dans la généricité/standardisation du build via un petit script shell ou python, mais ce n’était pas l’objet de mon exemple