Docker buil 101
Comme beaucoup d’entre vous, j’ai pesté devant un docker build
qui télécharge pour la 101ème fois les mêmes paquets apt install
.
Plutôt que de continuer à subir, je vous propose une série d’articles de mieux comprendre docker build
et d’explorer quelques pistes pour le rendre plus efficace.
Pour ce premier article, nous allons commencer par les bases.
Un image Docker est une succession de “couches” (layers) chacune représentant des modifications apportées au système de fichier. Chaque ligne du Dockerfile
indique une directive qui va construire une nouvelle couche, qui va venir s’empiler sur la précédente.
Un postulat de Docker, c’est qu’une commande passée sur un état bien défini du système de fichier en cours de construction donnera toujours le même résultat. C’est ce qui lui permet de ne pas rejouer l’intégralité des commandes à chaque docker build
, mais juste ce qui a été modifié.
Voilà; maintenant, tout ça ne vient pas sans quelques impacts…
Les premières commandes d’un Dockerfile
sont généralement du type:
FROM ubuntu
RUN apt update && apt install machin bidule chose
Il est rarissime de tomber sur un Dockerfile
dans lequel les dépendances système incluent une version exacte du paquet à installer. C’est bien et pas bien à la fois
- Si on reconstruit l’image depuis un environnemnt tout neuf, on aura les dépendances système à jour sans devoir courrir après les derniers numéros de version et toucher au Dockerfile.
- Si on a déjà cette commande en cache, on restera sur des dépendances devenues obsolètes
Il va donc faloir trouver un compromis … je suggère de faire un docker build --no-cache
de temps en temps, genre chaque Lundi pendant la pause déjeuner, et je vous laisse le soin de construire un joli workflow de mise à jour sur cette base :P
A titre personnel, je trouverai plus “propre” de mettre des versions aux dépendances système, et d’avoir un outil de type vulnerability scanner qui m’informe qu’un paquet plus récent existe, ou plutôt qui me fait une Pull-Request en mode dependabot pour rafraîchir mon Dockerfile… bref
Rien à voir avec les Tuches. Du fait de l’empilement des couches, une suppression n’en est pas réellement une. Un fichier supprimé est bien absent du système de fichier final, mais il existe dans les couches intermédiaires et alourdit l’image.
Résultat, vous verrez un peu partout ce genre de chose:
RUN apt-get update \
&& apt-get install -y --no-install-recommends machin
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
Ce mix de commande installe un paquet système, après avoir mis à jour les métadonnées, puis fait le ménage des divers caches pour ne pas embarquer de fichiers inutiles dans la couche en cours de construction. C’est bien optimisé tout ça, mais bye bye la lisibilibté!
La syntaxe proposée (à titre experimental) par BuildKit sous forme de “here-document” nous économise les \
et autres &&
mais bon, le problème reste assez présent:
# syntax = docker/dockerfile:1.3-labs
...
RUN <<eof
apt-get update
apt-get install -y --no-install-recommends machin
apt-get clean
rm -rf /var/lib/apt/lists/*
eof
L’invalidation des caches est l’un des problèmes les plus complexe de notre industrie, avec le nommage des variables.
Dans le cas d’un Dockerfile
, docker build
doit savoir quel layer reconstruire et lequel peut être utilisé tel quel depuis le cache. Là où ça devient compliqué c’est quand il doit ajouter du contentu externe à l’image.
Historiquement nous avions la directive ADD
. Bon, oubliez là tout de suite. Elle permet de télécharger un truc pour mettre dans l’image, mais du coup, comment savoir s’il faut re-télécharger? Faudrait il respected les headers HTTP “Last-Modified”? Amusez vous à lire dans les issues Docker les kilomètre de débats sur ce sujet.
La commande COPY
permet d’inclure un fichier local dans l’image. Ici c’est plus simple, si le fichier est le même, le résultat sera (devrait être?) le même, et le cache n’est pas invalidé. S’il est modifié, plus rien n’est sur, le cache est invalide et toutes les commandes à suivre doivent être rejouées.
Et c’est pour ça qu’on évite ce genre de choses:
FROM maven
COPY . /work
RUN mvn package
En faisant ça, le moindre changement d’un fichier dans le projet va relancer un build maven complet, avec les 200Mo de téléchargements que cela implique.
On vous recommandra donc des choses nettement plus élaborées, comme:
COPY pom.xml /work
RUN mvn dependency:go-offline
COPY . /work
RUN mvn package
Cette astuce permet de lier l’étape de téléchargement des dépendances au seul fichier de description du projet (pom.xml
, package.json
, Gemfile
, etc). Tant que celui-ci n’est pas modifié, seul le second COPY
va invalider le cache.
Je crois sincèrement que c’est ce qui a propulsé la commande Maven dependency:go-offline
bien au delà de son cas d’usage initial qui était “je vais bosser dans le TGV” à une époque ou le Wifi à bord tennait de la science fiction.
On va en rester là pour ce premier article, il reste beaucoup à dire, mais on va prendre le temps de s’attaquer a chaque sujet séparément.
A+