Certaines constructions Docker sont lentes pour de bonnes raisons : vous compilez un monstre, vous téléchargez une partie d’internet ou vous faites de la cryptographie à grande échelle. Mais la plupart des « builds lents » sont auto-infligés. Le schéma habituel : vous payez pour le même travail à chaque commit, sur chaque portable, sur chaque runner CI, pour toujours.
BuildKit peut régler ça — si vous arrêtez de considérer le cache comme « Docker qui se souvient des choses » et si vous commencez à le voir comme une chaîne d’approvisionnement. C’est un guide de terrain pour la mise en cache robuste en production : quoi mesurer, quoi changer et ce qui va se retourner contre vous.
Le modèle mental : le cache BuildKit n’est pas magique, ce sont des adresses
Les builds Docker classiques (le builder legacy) fonctionnaient comme une pile de couches : chaque instruction créait un snapshot du système de fichiers. Si l’instruction et ses entrées n’avaient pas changé, Docker pouvait réutiliser la couche. Cette histoire reste en grande partie vraie, mais BuildKit a changé la mécanique et rendu le cache beaucoup plus expressif.
BuildKit traite votre build comme un graphe d’opérations. Chaque opération produit une sortie, et cette sortie peut être mise en cache par une clé dérivée des entrées (fichiers, args, environnement, images de base, et parfois des métadonnées). Si la clé correspond, BuildKit peut sauter le travail et réutiliser la sortie.
La dernière phrase cache le piège : le cache n’est bon que tant que vos clés le sont. Si vous incluez par erreur « l’horodatage du jour » dans votre clé, vous avez un miss du cache à chaque fois. Si vous incluez « le dépôt entier » comme entrée d’une étape précoce, vous invalidez le monde quand quelqu’un modifie un README.
Que signifie « accélère vraiment »
Il y a trois problèmes de vitesse distincts que les gens confondent souvent :
- Travail répété sur une même machine : reconstruire le même Dockerfile localement encore et encore.
- Travail répété sur plusieurs machines : portables, runners CI éphémères, flottes de build autoscalées.
- Étapes lentes même en cache hit : contextes volumineux, pulls d’images lents, décompression, et « installer les dépendances » qui n’est jamais mis en cache.
Le cache BuildKit résout les trois — mais seulement si vous choisissez le bon mécanisme :
- Layer cache (classique) : bon pour les étapes immuables qui dépendent d’entrées stables.
- Métadonnées de cache inline : stocke les infos de cache dans le manifeste de l’image pour que d’autres builders puissent les réutiliser.
- Cache externe/distant : exporte le cache vers registre/local/artifact pour que les builders éphémères ne repartent pas à froid.
- Cache mounts (
RUN --mount=type=cache) : accélèrent les « install dependencies » sans graver les caches dans l’image finale.
Une règle opérationnelle : si vous ne pouvez pas expliquer pourquoi une étape est mise en cache, elle ne l’est pas. C’est juste de la chance temporaire.
Idée paraphrasée de Werner Vogels (CTO d’Amazon) : « Tout échoue, tout le temps. » Les misses de cache sont un mode d’échec. Conceptez en conséquence.
Faits intéressants et petite histoire (parce que ça compte)
- BuildKit a commencé comme un projet séparé pour remplacer le builder legacy de Docker par un moteur en graphe, permettant le parallélisme et une mise en cache plus riche.
- Le cache de couches classique de Docker précède BuildKit et était fortement lié à « une instruction = un snapshot de couche ». Ce modèle est simple, mais il ne peut pas exprimer proprement des caches éphémères.
- BuildKit peut exécuter des étapes indépendantes en parallèle (par exemple, tirer des images de base pendant le transfert du contexte), c’est pourquoi les logs différent et le chronométrage peut s’améliorer sans changement du Dockerfile.
.dockerignoreest plus ancien que BuildKit mais est devenu critique à mesure que les dépôts ont grandi : les contexts lourds ne ralentissent pas seulement les builds, ils polluent les clés de cache en changeant les entrées.- Le cache inline était un compromis pragmatique : stocker des indices de cache dans l’image pour que le build suivant puisse réutiliser des couches même sur une autre machine.
- Les cache mounts BuildKit ont été un changement de philosophie : « le cache est une préoccupation au moment du build » plutôt que « le cache est gravé dans l’image ». C’est la différence entre des builds rapides et des images surchargées.
- Les builds multi-étapes ont changé le comportement du cache dans les équipes réelles : vous pouvez isoler des toolchains coûteux dans des stages de build et garder les stages runtime stables et favorables au cache.
- L’export de cache distant est devenu nécessaire quand le CI est passé à des runners éphémères et des builders autoscalés où le cache disque local disparaît à chaque exécution.
Blague #1 : La mise en cache Docker, c’est comme le parking au bureau — tout le monde pense avoir une place réservée jusqu’au lundi matin qui prouve le contraire.
Playbook de diagnostic rapide : trouver le goulot en 10 minutes
Ceci est l’ordre de triage qui fait gagner du temps. N’optimisez pas le Dockerfile avant de savoir lequel de ces éléments est le vrai coupable.
1) BuildKit est-il activé ?
Si vous êtes sur une version Docker récente, généralement oui, mais « généralement » n’est pas un plan. Le builder legacy se comporte différemment et manque des fonctionnalités clés comme les cache mounts.
2) Le contexte de build est-il énorme ou instable ?
Si vous envoyez des gigaoctets, vous serez lent même avec un cache parfait. Et si le contexte change à chaque commit (logs, sorties de build, dossiers vendor), les clés de cache churnent.
3) Avez-vous des hits de cache là où vous vous y attendez ?
Regardez la sortie : CACHED devrait apparaître sur les étapes coûteuses. Si votre étape d’installation de dépendances s’exécute à chaque fois, c’est votre cible.
4) Le CI démarre-t-il à froid à chaque exécution ?
Les runners éphémères signifient que le cache local est perdu. Sans exportateur/importateur de cache distant, vous reconstruisez de zéro peu importe à quel point votre Dockerfile est « favorable au cache ».
5) Êtes-vous bloqué par le réseau ou le CPU ?
Téléchargements de dépendances, pulls d’images de base et mises à jour d’index sont gourmands en réseau. Les compilations sont gourmandes en CPU. La correction diffère.
6) Les secrets/SSH provoquent-ils des misses de cache ?
Les secrets ne font pas partie de la clé de cache par conception, mais la façon dont vous connectez des dépendances privées change souvent des commandes ou introduit de la nondéterminisme, ce qui ruine la réutilisation du cache.
7) Bousculez-vous accidentellement le cache ?
Coupables fréquents : ADD . trop tôt, paquets non figés, build args changeant à chaque exécution, horodatages, et des « nettoyages » qui modifient les mtimes des fichiers de façon inattendue.
Tâches pratiques : commandes, sorties, décisions (12+)
Ce ne sont pas des « commandes pour jouer ». Ce sont celles que vous exécutez quand votre pipeline de build est en feu et que vous devez décider quoi changer ensuite.
Task 1: Confirmer que BuildKit est activé (et quel builder est utilisé)
cr0x@server:~$ docker build --progress=plain -t demo:bk .
#1 [internal] load build definition from Dockerfile
#1 transferring dockerfile: 1.12kB done
#2 [internal] load metadata for docker.io/library/alpine:3.19
#2 DONE 0.8s
#3 [internal] load .dockerignore
#3 transferring context: 2B done
#3 DONE 0.0s
#4 [1/3] FROM docker.io/library/alpine:3.19
#4 resolve docker.io/library/alpine:3.19 done
#4 DONE 0.0s
Ce que ça signifie : Les étapes numérotées et les phases « internal » sont du style BuildKit. Si vous voyez l’ancien style « Step 1/… », vous n’utilisez pas BuildKit.
Décision : Si la sortie est legacy, activez BuildKit via une variable d’environnement ou la config du daemon avant toute autre chose. Sinon vous optimiserez le mauvais moteur.
Task 2: Afficher la version de Docker et les fonctionnalités du serveur
cr0x@server:~$ docker version
Client: Docker Engine - Community
Version: 26.1.3
API version: 1.45
Server: Docker Engine - Community
Engine:
Version: 26.1.3
API version: 1.45 (minimum version 1.24)
Experimental: false
Ce que ça signifie : Les capacités de BuildKit varient selon les versions de l’engine/buildx. Les engines très anciens peuvent être « BuildKit-ish » mais sans certaines fonctionnalités clés.
Décision : Si vous êtes plusieurs versions majeures en retard, mettez à jour d’abord. Les correctifs de cache sur un engine obsolète, c’est comme régler un carburateur sur une voiture qui a besoin d’un nouveau moteur.
Task 3: Vérifier la disponibilité de buildx et le builder actuel
cr0x@server:~$ docker buildx ls
NAME/NODE DRIVER/ENDPOINT STATUS BUILDKIT PLATFORMS
default docker
default default running v0.12.5 linux/amd64,linux/arm64
Ce que ça signifie : Vous avez buildx et une instance de builder. La version de BuildKit importe pour certains exporteurs/importeurs de cache.
Décision : Si buildx manque ou que le builder est cassé, réparez ça. Le cache distant est plus simple avec buildx.
Task 4: Mesurer la taille du contexte de build (le tueur silencieux)
cr0x@server:~$ docker build --no-cache --progress=plain -t demo:ctx .
#1 [internal] load build definition from Dockerfile
#1 transferring dockerfile: 2.01kB done
#2 [internal] load .dockerignore
#2 transferring context: 2B done
#3 [internal] load build context
#3 transferring context: 812.4MB 12.3s done
#3 DONE 12.4s
Ce que ça signifie : Transfert de contexte de 812Mo. Même si tout est en cache, vous venez de perdre 12 secondes avant que le build commence le vrai travail.
Décision : Corrigez .dockerignore et/ou utilisez un chemin de contexte plus restreint. N’acceptez pas « ça va sur ma machine » ici ; le CI paiera plus cher.
Task 5: Inspecter ce qui est dans votre contexte de build (vérification rapide)
cr0x@server:~$ tar -czf - . | wc -c
853224921
Ce que ça signifie : Votre contexte est ~853Mo compressé. Ce sont généralement des sorties de build, node_modules, virtualenvs ou des artefacts de tests qui se glissent dedans.
Décision : Ajoutez des exclusions, ou construisez depuis un sous-répertoire ne contenant que ce dont l’image a besoin.
Task 6: Vérifier les hits de cache étape par étape
cr0x@server:~$ docker build --progress=plain -t demo:cachecheck .
#7 [2/6] COPY package.json package-lock.json ./
#7 CACHED
#8 [3/6] RUN npm ci
#8 CACHED
#9 [4/6] COPY . .
#9 DONE 0.9s
#10 [5/6] RUN npm test
#10 DONE 34.6s
Ce que ça signifie : L’installation des dépendances est en cache ; les tests ne le sont pas (et probablement ne devraient pas l’être). Vous avez séparé les « entrées stables » des « entrées changeantes ».
Décision : Si l’étape coûteuse d’installation des dépendances n’est pas en cache, restructurez le Dockerfile ou utilisez des cache mounts.
Task 7: Montrer l’utilisation du cache sur l’hôte
cr0x@server:~$ docker system df
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 41 6 9.12GB 6.03GB (66%)
Containers 12 2 311MB 243MB (78%)
Local Volumes 8 3 1.04GB 618MB (59%)
Build Cache 145 0 3.87GB 3.87GB
Ce que ça signifie : Un cache de build existe et est conséquent ; « ACTIVE 0 » suggère qu’il n’est pas épinglé par des builds en cours. Ce cache peut être nettoyé agressivement par des scripts.
Décision : Si le cache disparaît sans cesse, arrêtez d’exécuter « docker system prune -a » dans des images CI ou sur des runners partagés sans comprendre l’impact.
Task 8: Inspecter l’utilisation disque du builder BuildKit
cr0x@server:~$ docker buildx du
ID RECLAIMABLE SIZE LAST ACCESSED
m5p8xg7n0f3m2b0c6v2h7w2n1 true 1.2GB 2 hours ago
u1k7tq9p4y2c9s8a1b0n6v3z5 true 842MB 3 days ago
total: 2.0GB
Ce que ça signifie : Buildx a sa propre comptabilité. Si c’est vide sur le CI, vous n’avez pas de cache persistant entre les exécutions.
Décision : Pour des builders éphémères, prévoyez l’export/import de cache distant.
Task 9: Prouver un coupable de bust du cache (build args)
cr0x@server:~$ docker build --progress=plain --build-arg BUILD_ID=123 -t demo:arg .
#6 [2/5] RUN echo "build id: 123" > /build-id.txt
#6 DONE 0.2s
cr0x@server:~$ docker build --progress=plain --build-arg BUILD_ID=124 -t demo:arg .
#6 [2/5] RUN echo "build id: 124" > /build-id.txt
#6 DONE 0.2s
Ce que ça signifie : Cette étape ne sera jamais mise en cache entre différentes valeurs de BUILD_ID. Si cet arg est utilisé tôt, il invalide tout ce qui suit.
Décision : Déplacez les args volatils en fin de Dockerfile, ou arrêtez d’intégrer des IDs de build dans les couches du système de fichiers sauf si c’est vraiment nécessaire.
Task 10: Confirmer que le pull de l’image de base est un goulot
cr0x@server:~$ docker pull ubuntu:22.04
22.04: Pulling from library/ubuntu
Digest: sha256:4f2...
Status: Image is up to date for ubuntu:22.04
docker.io/library/ubuntu:22.04
Ce que ça signifie : « up to date » indique qu’elle était déjà présente. Si les pulls sont lents et fréquents en CI, vous manquez peut-être un miroir de registre partagé ou un cache local.
Décision : Si le CI tire toujours les images, envisagez de mettre en cache les images de base sur les runners ou d’utiliser un registre plus proche des runners.
Task 11: Builder avec export/import de cache explicite vers un répertoire local
cr0x@server:~$ docker buildx build --progress=plain \
--cache-to type=local,dest=/tmp/bkcache,mode=max \
--cache-from type=local,src=/tmp/bkcache \
-t demo:localcache --load .
#11 [4/7] RUN apt-get update && apt-get install -y build-essential
#11 DONE 39.4s
#12 exporting cache to client directory
#12 DONE 0.6s
Ce que ça signifie : Vous avez exporté un cache réutilisable vers /tmp/bkcache. Au prochain run, ces étapes coûteuses devraient afficher CACHED.
Décision : Si le cache local aide mais que le CI reste lent, vous avez besoin d’un export de cache distant (registre/artifact) plutôt que local seulement.
Task 12: Valider la mise en cache au second run (preuve, pas impressions)
cr0x@server:~$ docker buildx build --progress=plain \
--cache-from type=local,src=/tmp/bkcache \
-t demo:localcache --load .
#11 [4/7] RUN apt-get update && apt-get install -y build-essential
#11 CACHED
#13 exporting to docker image format
#13 DONE 1.1s
Ce que ça signifie : L’étape apt coûteuse est en cache. Vous avez prouvé que le mécanisme fonctionne.
Décision : Maintenant vous pouvez investir dans un cache distant en toute sécurité, parce que vous savez que votre Dockerfile est cachable.
Task 13: Attraper la non-déterminisme dans l’installation de paquets
cr0x@server:~$ docker build --progress=plain -t demo:apt .
#9 [3/6] RUN apt-get update && apt-get install -y curl
#9 DONE 18.7s
cr0x@server:~$ docker build --progress=plain -t demo:apt .
#9 [3/6] RUN apt-get update && apt-get install -y curl
#9 DONE 19.4s
Ce que ça signifie : Il a reconstruit les deux fois, ce qui peut arriver si quelque chose d’antérieur a invalidé la couche, si vous avez utilisé --no-cache, ou si les entrées système de fichiers ont changé.
Décision : Assurez-vous que l’étape RUN apt-get ... vienne après des entrées stables. Envisagez aussi de figer les paquets ou d’utiliser une image de base qui contient déjà les outils communs.
Task 14: Identifier un « ADD . » trop tôt (test d’invalidation de cache)
cr0x@server:~$ git diff --name-only HEAD~1
README.md
cr0x@server:~$ docker build --progress=plain -t demo:readme .
#6 [2/6] COPY . .
#6 DONE 0.8s
#7 [3/6] RUN npm ci
#7 DONE 45.1s
Ce que ça signifie : Changer le README a déclenché COPY . ., qui a invalidé npm ci parce que vous avez copié le dépôt entier avant d’installer les dépendances.
Décision : Copiez d’abord seulement les manifests de dépendances ; copiez le reste ensuite. Ce n’est pas une « micro-optimisation » ; c’est la différence entre une reconstruction de 20s et une de 2 minutes.
Patterns Dockerfile qui rendent le cache réel
BuildKit récompense la discipline. Si votre Dockerfile est un tiroir à bazar, votre cache se comportera comme tel : plein, coûteux, et ne contenant pas ce dont vous avez besoin.
Pattern 1 : Séparer les manifests de dépendances du code source
Ceci est la correction classique parce que c’est l’erreur la plus courante. Vous voulez que l’installation des dépendances soit indexée sur le lockfile, pas sur chaque fichier du dépôt.
cr0x@server:~$ cat Dockerfile
# syntax=docker/dockerfile:1.7
FROM node:20-bookworm AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
COPY . .
RUN npm run build
FROM node:20-bookworm-slim
WORKDIR /app
COPY --from=build /app/dist ./dist
CMD ["node","dist/server.js"]
Pourquoi ça marche : l’étape coûteuse npm ci dépend principalement de package-lock.json. Un changement à README.md ne devrait pas réinstaller tout l’écosystème.
Pattern 2 : Mettre les étapes volatiles en fin de Dockerfile
Tout ce qui change à chaque build — IDs de build, SHA git, horodatage de version — devrait être près de la fin. Sinon vous invalidez tout le cache en aval.
Mauvais :
cr0x@server:~$ cat Dockerfile.bad
FROM alpine:3.19
ARG GIT_SHA
RUN echo "$GIT_SHA" > /git-sha.txt
RUN apk add --no-cache curl
Bon :
cr0x@server:~$ cat Dockerfile.good
FROM alpine:3.19
RUN apk add --no-cache curl
ARG GIT_SHA
RUN echo "$GIT_SHA" > /git-sha.txt
Pourquoi ça marche : vous gardez des couches stables réutilisables. Votre tampon existe toujours, mais il n’invalide que lui-même.
Pattern 3 : Les builds multi-étapes comme frontières de cache
Les builds multi-étapes ne servent pas seulement à réduire la taille des images runtime. Ils permettent aussi d’isoler le « churn » des toolchains de la « stabilité runtime ».
- Stage de build : compilateurs, gestionnaires de paquets, caches, headers.
- Stage runtime : petit, stable, moins de pièces mobiles, moins d’invalidations de cache.
Pattern 4 : Soyez délibéré sur les tags d’images de base
Si vous utilisez des tags flottants comme latest, votre image de base peut changer sans que vous touchiez le Dockerfile. C’est un événement d’invalidation du cache et un problème de reproductibilité.
Utilisez des tags explicites, et dans des environnements à haute assurance, préférez l’épinglage par digest. C’est un choix de politique : commodité versus répétabilité.
Pattern 5 : Minimiser le churn d’apt-get update
apt-get update est une source fréquente de comportements de cache imprévisibles. Pas parce qu’il ne peut pas être mis en cache — parce qu’il est souvent placé dans des couches qui sont invalidées par des changements non liés.
Aussi : combinez toujours update et install dans une seule couche. Les couches séparées causent des index obsolètes et des 404 mystérieux ensuite.
Pattern 6 : Garder le contexte de build propre
Si vous ne contrôlez pas votre contexte, vous ne contrôlez pas vos clés de cache. Utilisez .dockerignore de façon agressive : artefacts de build, répertoires de dépendances, logs, rapports de tests, fichiers d’environnement locaux et tout ce qui est produit par le CI lui-même.
Blague #2 : Si votre contexte de build inclut node_modules, votre daemon Docker fait du crossfit — il soulève du lourd encore et encore sans raison.
Cache distant qui survit au CI : registre, local et runners
Le cache local est agréable. Le cache distant est ce qui fait que les équipes arrêtent de se plaindre dans Slack. Si votre runner CI est éphémère, il se réveille avec l’amnésie à chaque exécution. Ce n’est pas un défaut moral ; c’est la façon dont ces systèmes sont conçus.
Cache inline : partager des indices via l’image
Le cache inline stocke des métadonnées de cache dans la config de l’image afin que des builds ultérieurs puissent tirer l’image et réutiliser des couches. C’est le « minimum viable » du caching inter-machines.
Exemple de build (push d’image omis ici ; la mécanique est ce qui compte) :
cr0x@server:~$ docker buildx build --progress=plain \
--build-arg BUILDKIT_INLINE_CACHE=1 \
-t demo:inlinecache --load .
#15 exporting config sha256:4c1d...
#15 DONE 0.4s
#16 writing image sha256:0f9a...
#16 DONE 0.8s
Ce que ça signifie : L’image porte maintenant des métadonnées de cache. Un autre builder peut les utiliser avec --cache-from pointant vers cette référence d’image.
Décision : Utilisez le cache inline quand vous poussez déjà des images et que vous voulez un chemin de cache simple. Ce n’est pas toujours suffisant, mais c’est une bonne base.
Cache registre : exporter le cache séparément (plus puissant)
BuildKit peut exporter le cache vers une référence de registre. C’est souvent mieux que le cache inline car il peut stocker plus de détails et ne nécessite pas de « promouvoir » une image juste pour utiliser son cache.
cr0x@server:~$ docker buildx build --progress=plain \
--cache-to type=registry,ref=registry.internal/demo:buildcache,mode=max \
--cache-from type=registry,ref=registry.internal/demo:buildcache \
-t registry.internal/demo:app --push .
#18 exporting cache to registry
#18 DONE 2.7s
Ce que ça signifie : Le cache est stocké comme un artefact OCI dans votre registre. Le build suivant l’importe, même sur un runner neuf.
Décision : Si le CI est éphémère et que les builds sont lents, c’est généralement la bonne solution. Le compromis est la croissance du stockage dans le registre et des soucis occasionnels d’autorisations.
Cache répertoire local : bon pour la réutilisation sur un seul runner
Les exports de cache locaux sont excellents quand vous avez un runner persistant avec un workspace qui survit aux runs, ou quand vous pouvez attacher un volume persistant.
C’est aussi une bonne étape « prouver que ça marche » avant de gérer l’auth du registre et les politiques CI.
Choisir un mode de cache
mode=min: cache plus petit, moins de résultats intermédiaires. Bien quand le stockage est limité.mode=max: cache plus complet, meilleurs taux de hit. Bien quand vous voulez la vitesse et pouvez accepter le stockage.
En pratique : commencez par mode=max dans le CI pendant une semaine, surveillez la croissance du registre et les taux de hit, puis décidez si vous devez restreindre.
Cache mounts qui accélèrent vraiment les installations de dépendances
Le layer caching est grossier. Les gestionnaires de dépendances sont subtils. Ils veulent un répertoire de cache qui persiste entre les runs, mais vous ne voulez pas graver ce cache dans l’image finale. Les cache mounts de BuildKit sont l’outil adapté.
npm / yarn / pnpm
L’exemple npm a été montré plus haut. L’idée : monter un répertoire de cache vers /root/.npm (ou le chemin de cache utilisateur) pendant l’installation.
apt
Vous pouvez mettre en cache les listes et archives apt. C’est utile quand vous avez des installations répétées entre builds et que votre réseau CI n’est pas top. Ce n’est pas une panacée ; les métadonnées apt changent fréquemment.
cr0x@server:~$ cat Dockerfile.aptcache
# syntax=docker/dockerfile:1.7
FROM ubuntu:22.04
RUN --mount=type=cache,target=/var/cache/apt \
--mount=type=cache,target=/var/lib/apt/lists \
apt-get update && apt-get install -y --no-install-recommends curl ca-certificates \
&& rm -rf /var/lib/apt/lists/*
Note opérationnelle : mettre en cache /var/lib/apt/lists peut accélérer les choses, mais peut aussi masquer des changements de dépôt de manière surprenante si vous déboguez la disponibilité des paquets. Utilisez-le en connaissance de cause.
pip
Les builds Python sont des contrevenants classiques : des wheels téléchargées à chaque fois, des compilations répétées, et quelqu’un finit par « corriger ça » en copiant tout le venv dans l’image (ne faites pas ça).
cr0x@server:~$ cat Dockerfile.pipcache
# syntax=docker/dockerfile:1.7
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt ./
RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt
COPY . .
CMD ["python","-m","app"]
Go
Go a deux caches clés : le téléchargement des modules et le build cache. BuildKit peut les persister sans polluer l’image runtime.
cr0x@server:~$ cat Dockerfile.go
# syntax=docker/dockerfile:1.7
FROM golang:1.22 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
COPY . .
RUN --mount=type=cache,target=/root/.cache/go-build \
go build -o /out/app ./cmd/app
FROM gcr.io/distroless/base-debian12
COPY --from=build /out/app /app
CMD ["/app"]
Pourquoi c’est important : le cache de téléchargement de modules économise le réseau ; le build cache économise le CPU. Différents goulots, même mécanisme.
Secrets, SSH, et pourquoi votre cache disparaît
Les dépendances privées sont l’endroit où « ça marche en local » meurt. Les gens contournent le problème avec ARG TOKEN=... et intègrent accidentellement des secrets dans des couches (mauvais) ou cassent le cache d’une façon qui n’apparaît que sur CI (aussi mauvais).
Utilisez les secrets BuildKit au lieu d’ARG pour les identifiants
Les secrets montés pendant le build ne sont pas stockés dans les couches d’image. Ils ne deviennent pas non plus des clés de cache. C’est une fonctionnalité et un piège : votre étape peut être mise en cache même si le secret change, donc assurez-vous que la sortie est déterministe.
cr0x@server:~$ cat Dockerfile.secrets
# syntax=docker/dockerfile:1.7
FROM alpine:3.19
RUN apk add --no-cache git openssh-client
RUN --mount=type=secret,id=git_token \
sh -c 'TOKEN=$(cat /run/secrets/git_token) && echo "token length: ${#TOKEN}" > /tmp/token-info'
Décision : Si vous avez besoin de clones git privés, utilisez --mount=type=ssh avec l’agent transféré ou une clé de déploiement, pas un token dans ARG.
Mounts SSH : relativement sûrs, mais attention à la reproductibilité
Les mounts SSH évitent de graver les secrets. Ils introduisent aussi un nouveau mode d’échec : votre build dépend du réseau et de l’état du dépôt distant. Épinglez des commits/tags pour la déterminisme, sinon votre cache sera « correctement » invalidé par des changements en amont.
Trois mini-récits d’entreprise depuis les tranchées du cache
1) Incident causé par une mauvaise hypothèse : « le CI met en cache les couches Docker par défaut »
Une société de taille moyenne a migré son CI de VMs longue durée vers des runners éphémères. La migration a été présentée comme une victoire : environnements propres, moins de builds instables, moins de maintenance disque. Tout cela était vrai. Mais quelqu’un a supposé que le cache de couches Docker « serait là » comme sur les anciennes VMs.
Le premier jour après la coupure, les temps de build ont triplé. Pas un peu plus lent — suffisamment pour rater des fenêtres de déploiement. Les ingénieurs ont répondu comme des ingénieurs : paralléliser les jobs, ajouter des runners, jeter de l’argent. La ferme de build a grossi ; les builds sont restés lents.
Le vrai problème était ennuyeux. Les anciennes VMs avaient des caches Docker chauds. Les nouveaux runners démarraient vides à chaque fois. Chaque build tirait les images de base, téléchargeait des dépendances et recompilait le même code. Le cache existait, mais il s’évaporait à la fin de chaque job.
La solution était tout aussi ennuyeuse : export/import de cache distant avec buildx et une référence de cache registre. Du jour au lendemain, les temps de build sont revenus près de la baseline précédente. L’« optimisation » n’était pas un truc malin de Dockerfile : c’était reconnaître la réalité d’infrastructure : les runners éphémères nécessitent un état externe si vous voulez la réutilisation.
2) Optimisation qui a foiré : « mettre tout le cache dans l’image pour accélérer les builds »
Une autre organisation avait un build qui exécutait pip install et prenait une éternité sur les portables des développeurs. Quelqu’un a décidé de « résoudre » le problème en copiant le cache pip et les artefacts de build dans la couche image après l’installation. Ça marchait — au sens étroit. Les rebuilds étaient rapides sur cette machine.
Puis l’image a commencé à gonfler. Elle est devenue incohérente : deux ingénieurs produisaient des images différentes pour le même commit parce que les répertoires de cache contenaient des wheels spécifiques à la plateforme et des restes. QA a trouvé des comportements « ça marche seulement en staging » qui ne se reproduisaient pas localement.
Sécurité est intervenue parce que le cache incluait des artefacts téléchargés non suivis dans le lock des dépendances, compliquant l’analyse de provenance. Maintenant l’entreprise avait un build rapide et une réponse aux incidents lente. Ce n’est pas un compromis souhaitable.
Le rollback a été douloureux. Ils sont passés aux cache mounts BuildKit pour pip, ont gardé les images runtime légères, et ont introduit une politique de verrouillage des dépendances. Les builds sont restés rapides et les artefacts produits sont redevenus déterministes pour le débogage.
3) Pratique ennuyeuse mais correcte qui a sauvé la situation : « clés de cache basées sur les lockfiles, pas l’état du dépôt »
Pendant un trimestre chargé, une équipe a livré beaucoup de petits changements : tweaks de config, corrections de texte, flags feature. Leurs builds de service étaient Node et dépendance-lourds, historiquement lents. Mais leur pipeline est resté stable.
La raison n’était pas héroïque. Des mois plus tôt, quelqu’un avait refactoré les Dockerfiles pour copier d’abord uniquement les manifests de dépendances, lancer l’installation des dépendances puis copier le code applicatif. Ils ont aussi imposé une règle : les changements de lockfile nécessitent une revue explicite.
Ainsi, quand une rafale de changements non-code est arrivée, la plupart des builds ont touché le cache sur l’installation des dépendances et les couches de base. Le CI construisait et testait toujours, mais il ne retéléchargeait pas l’internet. La file de déploiement est restée courte même avec un volume élevé de commits.
Cette pratique n’a pas l’air impressionnante dans une démo. Elle ne figure pas sur une slide. Mais elle a évité un mode d’échec très prévisible : « petit changement, coût de build élevé ». En exploitation, l’ennuyeux est souvent le plus louable.
Erreurs courantes : symptôme → cause racine → correctif
1) Symptom : « Chaque build relance l’installation des dépendances »
Cause racine : Vous copiez le dépôt entier avant d’installer les dépendances, donc tout changement de fichier invalide la couche.
Fix : Copiez seulement package-lock.json/requirements.txt/go.sum d’abord, exécutez install/download, puis copiez le reste.
2) Symptom : « Le CI est toujours lent, le local va bien »
Cause racine : Les runners CI sont éphémères ; la machine locale a un cache chaud.
Fix : Utilisez docker buildx build avec --cache-to/--cache-from (registre ou stockage d’artefacts). Le cache inline aide mais n’est pas toujours suffisant.
3) Symptom : « Le transfert du contexte de build prend une éternité »
Cause racine : Contexte énorme (node_modules, dist, logs, .git) ou fichiers instables inclus.
Fix : Reserrez .dockerignore. Construisez depuis un répertoire plus restreint. N’envoyez pas l’univers au daemon.
4) Symptom : « Hits de cache localement, misses en CI même avec cache distant »
Cause racine : Build args/platforms/targets différents, ou digests d’images de base différents. Les clés de cache divergent.
Fix : Standardisez les build args, la platform (--platform) et les targets. Épinglez les images de base. Assurez-vous que le CI importe la même référence de cache qu’il exporte.
5) Symptom : « Le cache marche jusqu’à ce que quelqu’un lance un job de nettoyage »
Cause racine : Un pruning agressif supprime le build cache (docker system prune -a), ou les runners réinitialisent le stockage.
Fix : Arrêtez de tout nuker sans discernement. Utilisez des politiques de nettoyage ciblées, et comptez sur le cache distant pour le CI si les disques des runners ne sont pas persistants.
6) Symptom : « Le build est lent même quand tout affiche CACHED »
Cause racine : Vous passez du temps à tirer des images de base, à exporter des images, à compresser des couches ou à charger dans le daemon.
Fix : Mesurez : cherchez du temps dans « exporting », « writing image », et les pulls. Envisagez --output type=registry en CI plutôt que --load si vous n’avez pas besoin de l’image localement.
7) Symptom : « Misses de cache aléatoires »
Cause racine : Commandes non déterministes (apt-get update sans ordre stable, dépendances non figées), horodatages intégrés dans les sorties de build, ou fichiers générés inclus tôt.
Fix : Rendre les étapes déterministes quand c’est possible, figer les versions, et isoler les artefacts générés dans des stages plus tardifs.
8) Symptom : « On a activé le cache inline et rien n’a changé »
Cause racine : Vous n’avez pas réellement importé le cache (--cache-from), ou l’image n’est pas disponible/tirée sur le builder, ou vous construisez pour une plateforme différente.
Fix : En CI, importez explicitement le cache. Vérifiez dans les logs que les étapes sont CACHED et que le builder peut atteindre la référence.
Checklists / plan étape par étape
Checklist A : Rendre les builds locaux rapides (machine unique)
- Activez BuildKit et utilisez
--progress=plainpour voir le comportement du cache. - Réduisez le contexte de build au strict minimum avec
.dockerignore. - Restructurez le Dockerfile : copier les manifests → installer les dépendances → copier le code source.
- Utilisez des cache mounts pour les gestionnaires de dépendances (
npm,pip,go,aptsi pertinent). - Déplacez les args/stamps volatils en fin de build.
- Exécutez deux builds consécutifs et vérifiez que les étapes coûteuses sont
CACHED.
Checklist B : Rendre les builds CI rapides (runners éphémères)
- Confirmez que le CI utilise buildx et la sortie BuildKit (pas legacy).
- Choisissez un backend de cache distant : le cache registre est généralement le plus simple opérationnellement.
- Ajoutez
--cache-toet--cache-fromaux builds CI. - Standardisez
--platformentre jobs CI ; ne mélangez pas amd64 et arm64 à moins que ce soit voulu. - Épinglez les images de base à des tags stables (ou des digests quand requis).
- Surveillez le temps passé à exporter les images ; préférez pousser directement depuis buildx plutôt que
--loaden CI. - Établissez une politique de rétention/pruning du cache dans le registre ; la croissance incontrôlée du cache est une panne lente.
Checklist C : Quand le travail de performance en vaut la peine
- Si votre build est <30 secondes et se produit rarement, ne lancez pas une croisade sur la mise en cache.
- Si votre build bloque des merges, des déploiements ou des interventions on-call, traitez la mise en cache comme un travail de fiabilité.
- Si l’installation des dépendances s’exécute à chaque fois, corrigez ça d’abord ; c’est le ROI le plus élevé dans la plupart des stacks.
FAQ
1) Pourquoi changer un README déclenche-t-il une reconstruction complète ?
Parce que vous avez copié le dépôt entier dans l’image avant les étapes coûteuses. Les clés de cache incluent les entrées de fichiers. Copiez moins, plus tard.
2) Le caching BuildKit est-il la même chose que le caching de couches Docker ?
Le layer caching est un mécanisme. BuildKit généralise le build en graphe et ajoute des cache mounts, l’export/import de cache distant, et un meilleur parallélisme.
3) Dois-je toujours utiliser --mount=type=cache ?
Utilisez-le pour les caches des gestionnaires de dépendances et des compilateurs. Ne l’utilisez pas pour masquer de la nondéterminisme ou pour « accélérer les tests » en réutilisant des sorties obsolètes.
4) Quelle est la différence entre inline cache et registry cache ?
Le cache inline stocke des métadonnées de cache dans l’image elle-même. Le cache registre exporte le cache comme un artefact séparé. Le cache registre est souvent plus flexible et efficace pour le CI.
5) Mon CI utilise docker build. Ai-je besoin de buildx ?
Vous pouvez obtenir certains bénéfices avec docker build si BuildKit est activé, mais buildx rend le caching distant et les builds multi-plateformes beaucoup plus faciles à gérer.
6) Pourquoi mes hits de cache disparaissent après un nettoyage Docker ?
Parce que quelqu’un a supprimé le build cache ou que le filesystem du runner est éphémère. Corrigez la politique de nettoyage, ou comptez sur le cache distant plutôt que l’état local.
7) L’utilisation de secrets désactive-t-elle le cache ?
Les secrets eux-mêmes ne font pas partie de la clé de cache. Mais les commandes que vous exécutez avec ces secrets peuvent rester nondéterministes ou dépendre de cibles mouvantes, ce qui cause des reconstructions.
8) Puis-je partager le cache entre architectures (amd64 et arm64) ?
Pas directement. Les clés de cache incluent la plateforme. Vous pouvez stocker des caches pour plusieurs plateformes sous la même référence de cache, mais ce sont des entrées séparées.
9) Pourquoi l’export de l’image est-il lent même quand tout est en cache ?
Parce que l’export doit toujours assembler des couches, les compresser et les écrire/pousser. Si le CI n’a pas besoin de l’image localement, poussez directement et évitez --load.
10) Quand dois-je épingler les images de base par digest ?
Quand la reproductibilité et le contrôle de la chaîne d’approvisionnement comptent plus que la commodité. Les digests réduisent les busts de cache surprises et les résultats « même Dockerfile, image différente ».
Conclusion : prochaines étapes qui s’autofinancent
Rendez ça ennuyeux. L’ennui est rapide.
- Exécutez un build avec
--progress=plainet notez quelle étape est lente et si elle estCACHED. - Réduisez la taille du contexte de build en premier. C’est la taxe que vous payez avant que le cache n’ait son mot à dire.
- Restructurez les Dockerfiles pour que les installations de dépendances dépendent des lockfiles, pas de votre dépôt entier.
- Ajoutez des cache mounts pour les gestionnaires de dépendances afin d’arrêter de retélécharger et recompilier.
- Si le CI est éphémère, implémentez l’export/import de cache distant. Sinon vous optimisez un cache qui s’évapore au succès.
- Standardisez les build args, la platform et la politique d’images de base pour que les clés de cache correspondent entre environnements.
Si vous faites ces six choses, vous cesserez d’« optimiser les builds Docker » et vous commencerez à gérer un système de build qui se comporte comme la production : prévisible, mesurable et rapide pour les bonnes raisons.