Toute équipe connaît ce moment : vous modifiez une ligne de code applicatif et votre « reconstruction rapide » se transforme en un pèlerinage de 12 minutes à travers l’installation de dépendances, les mises à jour de paquets OS et une quantité suspecte de « Sending build context ».
Ce n’est rarement Docker qui est « lent ». C’est votre cache qui est invalidé — parfois à raison, parfois par accident, parfois parce que votre CI traite les caches comme des secrets embarrassants. Rendre les builds rapidement prédictibles sans se contenter d’empiler des flags aléatoires, c’est possible.
Table des matières
- Le modèle mental : ce qu’est réellement le cache
- Faits et contexte historique intéressants (à ressortir en société)
- Pourquoi vous avez des cache misses : la mécanique réelle
- Mode d’emploi de diagnostic rapide (premiers/deuxièmes/troisièmes contrôles)
- Tâches pratiques : commandes, sorties et décisions (12+)
- Conception de Dockerfile qui préserve le cache
- BuildKit : le moteur moderne de cache et comment l’utiliser
- Réalités CI : caches distants, runners éphémères et bon sens
- Trois mini-récits du monde d’entreprise
- Erreurs courantes : symptôme → cause racine → correctif
- Checklists / plan pas à pas
- FAQ
- Prochaines étapes qui font vraiment la différence
Le modèle mental : ce qu’est réellement le cache
Le caching des builds Docker ressemble à de la magie jusqu’à ce que vous le considériez à la fois comme un système de fichiers et comme un compilateur. Le cache n’est pas « une mémoire générale de votre build ». C’est un ensemble de résultats de build immuables adressés par leurs entrées. Changez les entrées, vous obtenez un autre résultat. Pas de drame. Pas d’exception. Juste des larmes occasionnelles.
Les couches sont des résultats adressés par contenu des instructions
Les builds Docker traditionnels exécutent votre Dockerfile ligne par ligne. Chaque instruction produit un instantané du système de fichiers (« couche ») et des métadonnées. La clé de cache pour cette instruction est essentiellement un digest de :
- L’instruction elle-même (y compris les chaînes exactes, les valeurs ARG en scope, les variables d’environnement en scope).
- Le digest de la couche parente (ce qui précède).
- Pour
COPY/ADD, le contenu et les métadonnées des fichiers copiés dans le build.
Si l’un de ces éléments change, cette instruction fait un cache miss, et chaque instruction suivante échouera aussi au cache parce que leur couche parente a changé. C’est l’effet « invalide le reste du build ».
Le contexte de build fait partie de la surface d’entrée
Quand vous lancez docker build ., Docker envoie un contexte de build au builder. Si votre contexte est énorme, le build peut être lent avant même de commencer à construire. De plus, quand vous faites COPY . ., vous dites à Docker : « hashez pratiquement tout. » Un seul timestamp modifié ou un fichier généré peut empoisonner votre cache.
BuildKit change la donne, mais pas la physique
BuildKit est le moteur de build plus récent. Il ajoute la parallélisation, un meilleur caching, l’export/import de cache et des fonctionnalités comme les cache mounts et les secrets. Il ne rend pas magiquement de mauvais Dockerfile de meilleure qualité. Il fait juste arriver les conséquences plus rapidement.
Idée paraphrasée (attribution) : Gene Kim soutient souvent que la fiabilité vient des boucles de rétroaction et de la récupération rapide, pas des héros. Le caching Docker est aussi un problème de boucle de rétroaction.
Faits et contexte historique intéressants (à ressortir en société)
- Le moteur de build initial de Docker (le « classic builder ») était mono-thread et assez littéral ; BuildKit a introduit plus tard un graphe de build basé sur un DAG qui peut paralléliser les étapes indépendantes.
- Le caching par couches préexistait à Docker ; les systèmes de fichiers union et les instantanés copy-on-write existaient sous diverses formes bien avant que les conteneurs ne deviennent tendance.
RUN --mount=type=cachede BuildKit a changé notre manière de traiter les caches des gestionnaires de paquets : vous pouvez les conserver sans les intégrer à l’image finale.- Historiquement, de nombreux systèmes CI exécutaient des builds Docker en mode privilégié avec des caches disque locaux ; les runners éphémères modernes ont rendu la « réutilisation du cache » un choix volontaire plutôt qu’un accident.
- Le fichier
.dockerignoreexiste parce que les gens envoyaient des gigaoctets de déchets (commenode_modules) au démon et ensuite accusaient Docker. - Les builds multi-étapes ont popularisé une séparation nette entre « dépendances de build » et « image d’exécution », ce qui rend aussi la stratégie de cache plus intentionnelle.
- Les spécifications d’image OCI ont rendu les images plus portables, mais portabilité ne signifie pas que vos caches vous suivent — la localité du cache reste une contrainte pratique.
- Les clés de cache de Docker sont sensibles à l’ordre des instructions ; une petite réorganisation peut vous faire passer de « secondes » à « minutes » sans changer ce que l’image fait.
- L’export/import de cache distant (par ex. via Buildx) est en pratique un « cache d’artefacts de build », similaire en esprit à Bazel ou aux caches de compilateur, mais avec des couches de conteneur comme artefacts.
Pourquoi vous avez des cache misses : la mécanique réelle
1) Vous avez changé quelque chose plus tôt que vous ne le pensiez
La surprise la plus courante d’un build lent est de modifier un fichier utilisé dans un COPY précoce. Si vous faites COPY . . avant d’installer les dépendances, tout changement de code force une réinstallation. Ce n’est pas Docker qui fait la méchante. C’est votre Dockerfile qui est naïf.
2) Votre contexte de build est instable
Fichiers générés. Métadonnées Git. Artéfacts de build locaux. Fichiers swap d’éditeur. Différences de checkout en CI. Tout cela peut modifier le hash de contenu de vos entrées COPY. Si ces fichiers sont dans votre contexte et non ignorés, ils font partie de la clé de cache.
3) Vous utilisez des instructions « toujours changeantes »
RUN apt-get update est un classique qui fait tomber dans un piège en termes de cache. Même si Docker tente de réutiliser le cache, vous ne voulez probablement pas réutiliser une couche créée avec un index de paquets datant de trois semaines. Vous avez des objectifs concurrents : vitesse versus fraîcheur. Choisissez délibérément.
4) Les changements d’ARG/ENV invalident plus que vous ne le pensez
Les valeurs ARG en scope contribuent aux clés de cache. Les réglages ENV pour les instructions suivantes aussi. Si vous définissez ENV BUILD_DATE=... tôt, félicitations : vous avez invalidé votre cache à chaque build, comme prévu.
5) Différents builders, différents caches
Les caches des machines de développeurs locales ne sont pas ceux de la CI. Même en CI, différents runners ne partagent pas les caches sauf si vous les exportez/importez explicitement. Les gens supposent que « le cache est dans le registre ». Non. L’image l’est. Le cache, peut-être pas.
6) Vous ciblez plusieurs plateformes
Les builds multi-arch (linux/amd64 et linux/arm64) produisent des couches différentes. La réutilisation du cache entre architectures est limitée, et certaines étapes (comme la compilation de dépendances natives) sont intrinsèquement spécifiques à la plateforme.
Blague #1 : le caching Docker, c’est comme la mémoire — incroyable quand ça marche, et d’une manière ou d’une autre il oublie exactement ce dont vous aviez besoin il y a cinq secondes.
Mode d’emploi de diagnostic rapide (premiers/deuxièmes/troisièmes contrôles)
Voici le flux « arrêtez de deviner ». Lancez-le quand les builds ralentissent et que Slack commence à sentir le panic.
Premier : identifiez où le temps se passe (contexte vs étapes de build)
- Vérifiez le temps d’upload du contexte de build : si « Sending build context » est lent, vous avez un problème de contexte, pas de dépendances.
- Activez le progress simple : lisez quelle étape est lente et si elle était mise en cache.
Second : confirmez que le cache est bien utilisé
- Cherchez « CACHED » (BuildKit) ou « Using cache » (classic builder).
- Confirmez le builder et les réglages BuildKit : vous pouvez construire avec des moteurs différents localement et en CI.
- Confirmez l’import/export de cache en CI : les runners éphémères commencent vides sauf si vous leur fournissez un cache.
Troisième : trouvez le premier cache miss et corrigez l’ordre des couches
- Trouvez la première étape qui fait un miss ; tout ce qui suit en est un dommage collatéral.
- Recherchez un
COPY . .précoce ou desARGchangeants. - Corrigez avec une couche de dépendances stable : copiez d’abord seulement les lockfiles, installez les dépendances, puis copiez le reste.
Quatrième : si c’est encore lent, regardez stockage et réseau
- Les pulls/pushs du registre sont-ils limités ? DNS instable ? Proxy d’entreprise qui réécrit TLS ?
- Le disque du builder est plein ou sur un stockage lent ? Overlay2 sur un petit disque VM est un hobby coûteux.
Tâches pratiques : commandes, sorties et décisions (12+)
Chaque tâche ci-dessous a : une commande, ce que signifie une sortie typique, et la décision suivante. Exécutez-les sur une machine de dev ou un runner CI (quand possible). Les commandes sont volontairement ennuyeuses. L’ennui, c’est bien.
Task 1: Verify BuildKit is enabled
cr0x@server:~$ docker version --format '{{.Server.Version}}'
27.3.1
cr0x@server:~$ echo $DOCKER_BUILDKIT
1
Ce que ça signifie : Les versions Docker modernes supportent BuildKit ; DOCKER_BUILDKIT=1 signifie que le CLI l’utilisera pour les builds.
Décision : Si BuildKit n’est pas activé, activez-le (localement et en CI) avant toute autre optimisation. Sinon, vous optimisez le mauvais moteur.
Task 2: Run a build with plain progress to see cache hits
cr0x@server:~$ docker build --progress=plain -t demo:cache-test .
#1 [internal] load build definition from Dockerfile
#1 DONE 0.0s
#2 [internal] load .dockerignore
#2 DONE 0.0s
#3 [internal] load metadata for docker.io/library/alpine:3.20
#3 DONE 0.6s
#4 [1/6] FROM docker.io/library/alpine:3.20@sha256:...
#4 CACHED
#5 [2/6] RUN apk add --no-cache bash
#5 CACHED
#6 [3/6] COPY . /app
#6 DONE 0.3s
#7 [4/6] RUN make -C /app build
#7 DONE 24.8s
Ce que ça signifie : Les étapes marquées CACHED ont réutilisé le cache ; celles sans l’ont exécutées. Ici, COPY n’était pas en cache et l’étape build a pris 24,8s.
Décision : Corrigez la première étape non mise en cache qui ne devrait pas changer souvent (généralement l’installation des dépendances ou le téléchargement d’outils).
Task 3: Measure build context size (the silent killer)
cr0x@server:~$ docker build --no-cache --progress=plain -t demo:nocache .
#1 [internal] load build definition from Dockerfile
#1 DONE 0.0s
#2 [internal] load .dockerignore
#2 DONE 0.0s
#3 [internal] load build context
#3 transferring context: 1.42GB 12.1s done
#3 DONE 12.2s
Ce que ça signifie : Vous envoyez 1,42GB au builder à chaque fois. Ce n’est plus un build ; c’est un déménagement.
Décision : Ajoutez/réparez .dockerignore. Si vous ne pouvez pas maîtriser le contexte, aucun truc de cache ne vous sauvera.
Task 4: Confirm what is in your build context (quick and dirty)
cr0x@server:~$ tar -czf - . | wc -c
1523489123
Ce que ça signifie : Ceci approxime la taille compressée du contexte. Si c’est énorme, vous avez probablement inclus node_modules, des sorties de build ou des répertoires vendor.
Décision : Serrez .dockerignore et évitez COPY . . tant que vous n’avez pas des couches stables.
Task 5: Check Docker disk usage and whether the cache is being evicted
cr0x@server:~$ docker system df
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 48 11 19.3GB 12.7GB (65%)
Containers 7 1 412MB 388MB (94%)
Local Volumes 23 8 6.1GB 2.4GB (39%)
Build Cache 214 0 18.9GB 18.9GB (100%)
Ce que ça signifie : Beaucoup de build cache existe mais rien n’est actif ; il peut être obsolète, ou vos builds ne s’y réfèrent pas à cause de changements d’entrées ou de builder.
Décision : Si le disque est contraint, définissez une politique de cache plutôt que de lancer périodiquement docker system prune -a. Si le cache n’est jamais actif, corrigez l’ordre du Dockerfile ou le caching en CI.
Task 6: Inspect builder instances (buildx) and confirm which one you use
cr0x@server:~$ docker buildx ls
NAME/NODE DRIVER/ENDPOINT STATUS BUILDKIT PLATFORMS
default docker
default default running v0.16.0 linux/amd64,linux/arm64
ci-builder* docker-container
ci-builder0 unix:///var/run/docker.sock running v0.16.0 linux/amd64
Ce que ça signifie : Vous avez plusieurs builders. Chaque builder peut avoir son propre cache. Si la CI utilise ci-builder mais votre machine dev utilise default, le comportement du cache diffère.
Décision : Standardisez sur un builder pour la CI et documentez-le. Si vous avez besoin d’un cache distant, préférez un builder driver container avec export/import de cache explicite.
Task 7: Identify the first cache miss precisely (rebuild twice)
cr0x@server:~$ docker build --progress=plain -t demo:twice .
#6 [3/8] COPY package.json package-lock.json /app/
#6 CACHED
#7 [4/8] RUN npm ci
#7 CACHED
#8 [5/8] COPY . /app
#8 DONE 0.4s
#9 [6/8] RUN npm test
#9 DONE 18.2s
Ce que ça signifie : Les étapes de dépendances étaient en cache, mais l’étape de test a tourné. C’est normal si les tests dépendent du code.
Décision : Si npm ci est en cache, vous avez déjà gagné la plupart de la bataille. Sinon, réordonnez les copies et isolez les lockfiles.
Task 8: Confirm which files invalidate your dependency layer
cr0x@server:~$ git status --porcelain
M src/app.js
?? dist/bundle.js
?? .DS_Store
Ce que ça signifie : Les sorties générées (dist/) et les fichiers OS (.DS_Store) sont dans l’arbre de travail.
Décision : Ignorez les artefacts générés dans .dockerignore (et probablement dans .gitignore) pour qu’ils ne déclenchent pas de cache misses lors de copies larges.
Task 9: Test that .dockerignore is actually applied
cr0x@server:~$ printf "dist\nnode_modules\n.git\n.DS_Store\n" > .dockerignore
cr0x@server:~$ docker build --progress=plain -t demo:ignore-test .
#3 [internal] load build context
#3 transferring context: 24.7MB 0.3s done
#3 DONE 0.3s
Ce que ça signifie : Le transfert du contexte est passé de « pénible » à « correct ».
Décision : Gardez .dockerignore revu comme du code de production. C’en est.
Task 10: See image layer history to spot cache-busting patterns
cr0x@server:~$ docker history --no-trunc demo:cache-test | head -n 8
IMAGE CREATED CREATED BY SIZE
sha256:... 2 minutes ago RUN /bin/sh -c make -C /app build 312MB
sha256:... 2 minutes ago COPY . /app 18.4MB
sha256:... 10 minutes ago RUN /bin/sh -c apk add --no-cache bash 8.2MB
sha256:... 10 minutes ago FROM alpine:3.20 7.8MB
Ce que ça signifie : Une grosse couche RUN make suggère que vous produisez des artefacts de build à l’intérieur de l’image. C’est acceptable pour des stages de build, discutable pour des stages runtime.
Décision : Utilisez les builds multi-étapes pour que les grosses couches de compilation restent dans un stage build et ne polluent pas les images runtime (et les pushes).
Task 11: Validate cache export/import in CI (buildx)
cr0x@server:~$ docker buildx build --progress=plain \
--cache-from=type=local,src=/tmp/buildkit-cache \
--cache-to=type=local,dest=/tmp/buildkit-cache,mode=max \
-t demo:cache-export --load .
#10 [4/8] RUN npm ci
#10 CACHED
#11 exporting cache
#11 DONE 0.8s
Ce que ça signifie : Votre build a réutilisé un répertoire de cache local et a exporté les mises à jour dedans.
Décision : En CI, persistez ce répertoire entre les runs via le mécanisme de cache de votre CI. Si vous ne pouvez pas persister le disque, exportez vers un cache-backed registry à la place.
Task 12: Detect if your build is pulling base images every time
cr0x@server:~$ docker images alpine --digests
REPOSITORY TAG DIGEST IMAGE ID CREATED SIZE
alpine 3.20 sha256:4bcff6... 11f7b3... 3 weeks ago 7.8MB
Ce que ça signifie : L’image de base existe localement avec un digest spécifique.
Décision : Si la CI repull constamment des couches de base, envisagez une image runner pré-chauffée avec des bases communes, ou reposez-vous sur un cache côté registre plus proche des runners.
Task 13: Confirm whether --no-cache is being used accidentally
cr0x@server:~$ grep -R --line-number "docker build" .github/workflows 2>/dev/null | head
.github/workflows/ci.yml:42: run: docker build --no-cache -t org/app:${GITHUB_SHA} .
Ce que ça signifie : Quelqu’un a forcé des builds froids en CI. Parfois c’est pour la « fraîcheur ». Souvent c’est de la superstition.
Décision : Enlevez --no-cache sauf si vous avez une raison de sécurité/conformité spécifique et que vous avez accepté le coût.
Task 14: Check for cache-busting build arguments
cr0x@server:~$ grep -nE 'ARG|BUILD_DATE|GIT_SHA|CACHE_BUST' Dockerfile
5:ARG BUILD_DATE
6:ARG GIT_SHA
7:ENV BUILD_DATE=${BUILD_DATE}
Ce que ça signifie : Si BUILD_DATE change à chaque build et est utilisé tôt, vous invalidez le cache pour tout le reste.
Décision : Déplacez les métadonnées volatiles à la fin, ou dans des labels du stage final seulement.
Conception de Dockerfile qui préserve le cache
Le cache n’est pas quelque chose que vous « activez ». C’est quelque chose que vous gagnez en stabilisant les entrées. Le Dockerfile le plus rapide est souvent celui qui admet clairement ce qui change souvent et ce qui ne change pas.
Règle 1 : Séparez la définition des dépendances du code applicatif
Si les dépendances sont définies par des lockfiles, copiez-les d’abord et installez les dépendances avant de copier le dépôt entier. Ainsi, les changements de code ne forcent pas la réinstallation des dépendances.
Mauvais pattern : copier tout, puis installer. Cela garantit des cache misses lors de l’installation des dépendances.
Bon pattern : copier uniquement les manifests de dépendances, installer, puis copier le reste.
Règle 2 : Gardez les args volatiles hors des couches précoces
Oui, vous voulez le SHA Git dans l’image. Non, vous ne voulez pas qu’il détruise le caching pour toute la chaîne de dépendances. Mettez les labels en fin de build, idéalement dans le stage final seulement.
Règle 3 : Arrêtez d’intégrer les caches dans les images ; montez-les à la place
Les cache mounts de BuildKit vous permettent de réutiliser les caches des gestionnaires de paquets sans les baker dans les couches finales. C’est là que BuildKit est véritablement transformateur.
Règle 4 : Utilisez les builds multi-étapes comme outil de cache, pas seulement comme outil d’amaigrissement
Les builds multi-étapes vous permettent d’isoler des opérations lourdes et lentes (compilateurs, builds de dépendances) dans un stage qui change rarement, tout en gardant le stage runtime minimal. Cela réduit aussi la taille des pushes, ce qui compte plus qu’on ne l’admet.
Règle 5 : Epinglez les images de base délibérément
Si vous utilisez des tags flottants comme ubuntu:latest, vous finirez par avoir du churn de cache, des mises à jour surprises et de l’archéologie « ça marche sur ma machine ». Épinglez à un digest pour la reproductibilité quand c’est important ; épinglez à une version mineure stable quand vous voulez des mises à jour contrôlées.
Règle 6 : Utilisez .dockerignore sérieusement
Ignorez .git, les sorties de build, les caches locaux et les répertoires de dépendances qui doivent être installés dans l’image. Votre contexte de build doit ressembler à du code source, pas à l’histoire de vie de votre laptop.
BuildKit : le moteur moderne de cache et comment l’utiliser
BuildKit est l’endroit où les builds Docker ont cessé d’être purement séquentiels « exécuter des instructions » et sont devenus quelque chose de plus proche d’un système de build. Mais il faut utiliser ses fonctionnalités intentionnellement.
Les meilleures armes de BuildKit pour le caching
- Cache mounts : réutilisez les caches des gestionnaires de paquets sans les commettre dans les couches.
- Secret mounts : récupérez des dépendances privées sans fuites de tokens dans les couches (aide aussi le caching en évitant des hacks « token changé »).
- Export/import de cache : rendez les builds CI rapides même sur des runners frais.
- Meilleur affichage de progression : diagnostiquer le comportement du cache devient moins basé sur l’intuition.
Cache mounts : builds rapides, images propres
Docker classique a appris aux gens à supprimer les caches des gestionnaires de paquets pour garder les images petites. C’est acceptable pour les images runtime. C’est terrible pour la vitesse si vous rebuild fréquemment. Les cache mounts vous permettent de garder le cache hors des couches immuables.
Si vous construisez des langages comme Go, Rust, Java, Node, Python, vous pouvez cacher les téléchargements de modules et les caches de compilation. Les mounts exacts diffèrent, mais le principe est le même : gardez les caches mutables en dehors des couches immuables.
Cache distant : vos runners CI ont l’amnésie
Les runners CI éphémères partent de zéro. Si vos builds sont lents en CI mais rapides localement, c’est généralement parce que votre machine locale a un cache chaud et que le runner n’en a pas.
Exporter le cache vers un stockage persistant local est la solution la plus simple. Quand ce n’est pas disponible, exportez vers un cache basé sur un registre. Ce n’est pas gratuit : cela augmente le trafic vers le registre. Mais c’est souvent moins cher que de payer des ingénieurs à regarder des barres de progression.
Blague #2 : la seule chose plus éphémère qu’un runner CI, c’est la confiance de quelqu’un qui vient d’ajouter --no-cache « pour être sûr ».
Réalités CI : caches distants, runners éphémères et bon sens
La CI est l’endroit où les stratégies de cache passent à la casse — sauf si vous les concevez. La différence clé entre builds locaux et CI n’est pas la vitesse. C’est la persistance. Les développeurs ont des disques persistants. Les runners CI souvent non.
Choisissez une des trois stratégies de caching CI
- Cache persistant local au runner : fonctionne quand les runners sont long-lived. Facile, rapide, mais moins reproductible lors de changements de pool.
- Cache artefact CI : stockez le répertoire local de BuildKit comme artefact de cache CI. Fonctionne bien ; dépend des politiques de taille et d’éviction du cache CI.
- Cache registry : export/import du cache via le registre. Portable, fonctionne entre runners, mais augmente le trafic push/pull et peut stresser les registres.
Ne confondez pas couches d’image et couches de cache
Push d’une image ne signifie pas automatiquement que vous pouvez réutiliser son cache au prochain run. Une partie du cache peut être inférée à partir d’images existantes (surtout si vous reconstruisez le même Dockerfile et la même base), mais une réutilisation fiable en CI nécessite généralement un export/import explicite de cache.
Le réseau fait partie du build
Quand les builds sont lents, on accuse « le cache Docker ». Puis vous regardez et vous voyez que l’étape lente est le téléchargement de dépendances depuis Internet via un proxy qui fait de l’inspection TLS et oublie parfois comment gérer les certificats.
Dans ces environnements, les miroirs locaux et les proxys de dépendances ne sont pas un luxe. Ce sont de l’infrastructure de build.
Trois mini-récits du monde d’entreprise
Mini-récit #1 : L’incident causé par une mauvaise hypothèse
L’équipe avait un objectif raisonnable : garder les images de base à jour. Ils utilisaient FROM debian:stable et exécutaient apt-get update && apt-get upgrade -y durant les builds. Quelqu’un a demandé pour le cache ; la réponse fut « C’est bon, Docker cache les couches. » Cette hypothèse est entrée en production comme si elle en était propriétaire.
Puis un nouveau cluster CI a été déployé avec des runners éphémères. Du jour au lendemain, les temps de build ont explosé. Le pipeline a commencé à timeouter, les ingénieurs ont relancé des jobs, et les pics de concurrence ont écrasé le dépôt d’artefacts et les miroirs de paquets OS. Les miroirs ont limité, les builds ont retenté, et la boucle de rétroaction s’est aggravée : les builds lents causaient plus de retries, ce qui ralentissait encore les builds.
La racine du problème n’était pas Docker. C’était de traiter « stable » comme immuable et de considérer le caching comme global. debian:stable a bougé. apt-get update a changé la sortie. Et les runners n’avaient pas de cache chaud. Chaque build était un cold start plus une mise à jour complète de la distribution.
La solution fut peu glamour : épingler les images de base à un digest pour la branche de release, arrêter de mettre à jour tout le système durant le build, et rebuild les images de base selon un planning avec des rollouts contrôlés. Ils ont aussi exporté le cache BuildKit vers un backend de cache partagé. Les temps de build sont redevenus prévisibles, et « prévisible » vaut mieux que « rapide » quand l’horloge de déploiement tourne.
Mini-récit #2 : L’optimisation qui a tourné au fiasco
Une autre organisation a tenté d’accélérer les builds en comprimant les étapes du Dockerfile. Ils ont pris cinq instructions RUN et les ont fusionnées en une mega-commande pour « réduire le nombre de couches ». L’image paraissait plus propre, et quelqu’un a affiché un screenshot de docker history comme s’il s’agissait d’une transformation fitness.
La première semaine fut correcte. Puis une dépendance a changé — une mise à jour de package. Parce que tout était dans un seul RUN, le cache miss a forcé à relancer une longue chaîne : paquets système, installation du runtime, téléchargements de dépendances, setup des outils de build. Le cache avait moins de points d’entrée, donc il était moins réutilisable. Ils avaient optimisé le nombre de couches, pas le temps de rebuild.
En CI ça s’est aggravé. La méga-étape était difficile à diagnostiquer. Avec des étapes plus petites, les logs auraient montré « téléchargement toolchain » ou « installation deps » comme la partie lente. Avec la méga-étape, ce n’était qu’un script shell de 9 minutes qui échouait parfois pour des erreurs réseau transitoires. Quand il échouait, il échouait tard.
Ils ont finalement reverti : garder des couches signifiantes et stables, fusionner seulement quand cela améliore le caching ou la correction, pas l’esthétique. L’image finale est restée mince grâce aux builds multi-étapes, et les builds furent plus rapides car le cache avait plus de frontières réutilisables.
Mini-récit #3 : La pratique ennuyeuse mais correcte qui a sauvé la journée
Une équipe plateforme maintenait un template de build conteneur « golden path ». Ce n’était pas flashy. Il appliquait l’hygiène .dockerignore, les builds multi-étapes, et l’installation des dépendances basée sur les lockfiles. Il imposait aussi que les métadonnées volatiles (timestamp build, git SHA) soient appliquées comme labels dans le stage final seulement.
Quand une alerte supply-chain a frappé l’industrie et que tout le monde a dû rebuild fréquemment, les pipelines de cette équipe sont restés calmes. Pas parce qu’ils avaient de la chance — parce que leurs rebuilds étaient incrémentaux. Les images de base étaient épinglées et rebuildées selon un planning ; les builds d’app réutilisaient les couches de dépendances ; les caches CI étaient exportés vers un stockage persistant.
Les autres équipes rebuildaient depuis zéro et se disputaient la bande passante pour tirer les mêmes dépendances. Les builds de cette équipe atteignaient majoritairement le cache et ne tiraient que ce qui changeait. Pendant la crise, ils ont livré des correctifs de sécurité plus vite et avec moins de pipelines rouges.
La pratique qui les a sauvés n’était pas un flag secret. C’était traiter le Dockerfile comme du code de production et le caching comme un système ingénieré : entrées, sorties et cycle de vie. Ennuyeux. Correct. Efficace.
Erreurs courantes : symptôme → cause racine → fix
1) Symptom: “Sending build context” takes forever
Cause racine : contexte gonflé (node_modules, dist, .git), .dockerignore insuffisant, ou build depuis la racine du repo alors qu’un sous-dossier suffit.
Fix : resserrer .dockerignore, build depuis un répertoire plus étroit, ou restructurer le repo pour que le contexte Docker soit petit et stable.
2) Symptom: dependency install runs on every code change
Cause racine : COPY . . avant l’installation des dépendances ; lockfiles non isolés ; couche dépendances dépendant de tout le source tree.
Fix : copier uniquement les manifests de dépendances d’abord (lockfile, liste de paquets), installer les dépendances, puis copier le code source.
3) Symptom: CI is always cold even though local is fast
Cause racine : runners éphémères sans persistance de cache ; pas d’export/import BuildKit.
Fix : export/import de cache avec buildx ; persister le répertoire local de cache comme artefact CI ; ou utiliser un cache registry.
4) Symptom: builds became slow after “cleanup”
Cause racine : quelqu’un a ajouté docker system prune -a régulièrement, ou a réduit la rétention de cache, évacuant constamment le build cache.
Fix : définir des budgets disque ; prune sélectif ; conserver le cache du builder pour les branches actives ; éviter les prunes totales sur des runners partagés.
5) Symptom: caches miss when only metadata changes
Cause racine : ARG/ENV volatiles (timestamp build, git SHA) définis tôt et utilisés dans les clés de cache.
Fix : déplacer les métadonnées à la fin ; utiliser LABEL dans le stage final ; ne pas intégrer le temps dans les couches précoces.
6) Symptom: multi-arch builds are painfully slow
Cause racine : compilation de dépendances natives pour chaque plateforme ; pas de caches par plateforme ; overhead de QEMU si cross-building.
Fix : utiliser des builders natifs par architecture quand possible ; exporter des caches séparés par plateforme ; réduire la compilation native dans le build Docker lorsque faisable.
7) Symptom: image pushes are slow even when builds are fast
Cause racine : le stage runtime inclut des artefacts de build ; grosses couches changent fréquemment ; multi-stage mal utilisé.
Fix : garder l’image runtime minimale ; ne copier que les outputs construits ; s’assurer que les caches/outils de build restent dans le stage builder.
8) Symptom: “It used to be cached yesterday” mystery
Cause racine : instance de builder différente, version Docker différente, digest de base changé, ou contexte modifié par des fichiers générés.
Fix : standardiser le builder ; épingler les bases ; vérifier .dockerignore ; contrôler quels fichiers ont changé ; éviter les tags flottants sur des chemins critiques.
Checklists / step-by-step plan
Checklist A: Fix a slow build today (30–90 minutes)
- Relancez le build avec
--progress=plainet identifiez la première étape lente non mise en cache. - Mesurez la taille du transfert de contexte ; si >100MB, considérez cela comme un bug.
- Ajoutez/réparez
.dockerignorepour exclure :.git,node_modules,dist,target,build, déchets d’éditeur, artefacts de test. - Refactorez le Dockerfile pour que les manifests de dépendances soient copiés en premier ; installez les dépendances ; puis copiez le source.
- Déplacez les args/labels volatils vers le stage final et aussi tard que possible.
- Rebuild deux fois ; la seconde exécution devrait être beaucoup plus rapide et montrer
CACHEDpour les étapes lourdes.
Checklist B: Make CI caching real (half day)
- Confirmez que la CI utilise BuildKit et un builder cohérent (
docker buildx). - Choisissez une stratégie de persistance du cache : artefact CI ou cache registry.
- Ajoutez
--cache-fromet--cache-toaux étapes de build CI. - Assurez-vous que les clés de cache incluent une stratégie par branche ou mainline (pour éviter de polluer le cache entre changements incompatibles).
- Définissez des politiques de rétention/éviction pour que le cache ne soit pas vidé quotidiennement.
- Ajoutez une métrique de pipeline : répartition de la durée de build (temps de contexte vs temps de build vs temps de push).
Checklist C: Keep it fast over months (operational discipline)
- Revoyez les changements de Dockerfile comme vous revoyez la config de production : diff pour risque d’invalidation du cache.
- Epinglez les images de base pour les branches de release ; mettez-les à jour selon un planning.
- Maintenez un template
.dockerignorestandard par écosystème de langage. - Auditez périodiquement l’usage disque du builder ; prunez avec intention, pas colère.
- Exécutez les builds dans un environnement contrôlé : versions Docker/BuildKit stables sur toute la flotte CI.
FAQ
1) Pourquoi modifier un seul fichier source reconstruit-il tout après une certaine étape ?
Parce que le premier cache miss change le digest de la couche parente. Chaque étape suivante dépend de ce digest, donc elles ratent toutes le cache. Corrigez le premier miss en réordonnant et en réduisant les entrées de COPY.
2) Est-ce que .dockerignore affecte le caching ou seulement la taille du contexte ?
Les deux. Il réduit le temps de transfert et réduit l’ensemble de fichiers pouvant invalider les étapes COPY. Moins d’entropie d’entrée signifie plus de hits de cache.
3) Est-ce mauvais d’utiliser apt-get update dans les Dockerfiles ?
Non, mais c’est souvent mal utilisé. Combinez update et install dans la même couche, et n’attendez pas un caching stable si vous voulez des index frais. Pour la reproductibilité, épinglez les paquets ou utilisez des images de base curated.
4) Pourquoi mon build local est rapide mais la CI est lente ?
Votre laptop a un cache builder persistant. Les runners CI généralement non. Export/importez le cache BuildKit, ou fournissez un stockage persistant pour le builder.
5) Les builds multi-étapes vont-ils accélérer les builds ?
Ils peuvent. Les builds multi-étapes permettent de cacher des étapes de build coûteuses dans un stage dédié et de garder les images runtime petites. Des images runtime plus petites poussent/tirent aussi plus vite, ce qui domine souvent le temps CI.
6) Dois-je fusionner les RUN pour réduire le nombre de couches ?
Seulement si cela améliore la correction ou le caching. Moins de couches peut signifier moins de points de réutilisation du cache, ce qui peut rallonger les rebuilds. Optimisez pour le temps de rebuild et la débugabilité, pas l’esthétique.
7) Quelle est la différence entre une image et un cache ?
Une image est un artefact exécutable que vous poussez et déployez. Un cache est des métadonnées de build et des résultats intermédiaires utilisés pour accélérer les builds futurs. Ils se recoupent parfois, mais se fier à ce recoupement en CI est peu fiable.
8) Changer le tag de l’image de base invalide-t-il tous les caches ?
Si le digest résolu change, oui : la couche parente change, donc tout en aval rate le cache. Epingler des digests permet une cache et des rollouts prévisibles.
9) Les cache mounts sont-ils sûrs ? Vont-ils fuir dans l’image finale ?
Les cache mounts ne sont pas commités dans les couches d’image par défaut. C’est le but. Le cache vit sur l’hôte builder (ou dans un cache exporté), pas dans l’instantané du système de fichiers runtime.
10) Quel est le correctif à ROI le plus élevé pour des builds Docker lents ?
Arrêtez de copier le dépôt entier avant l’installation des dépendances. Isolez les lockfiles, installez les dépendances dans une couche stable, puis copiez le code. Tout le reste est secondaire.
Prochaines étapes qui font vraiment la différence
Si vous voulez des builds plus rapides, cessez de traiter le caching Docker comme une vibe et commencez à le traiter comme du hachage d’entrées.
- Lancez un build avec
--progress=plainet notez le premier expensive cache miss. - Mesurez la taille du contexte. Si elle est grande, corrigez
.dockerignoreen priorité. Toujours. - Refactorez votre Dockerfile pour que l’installation des dépendances dépende uniquement des lockfiles, pas de tout le source tree.
- Activez BuildKit partout, puis ajoutez l’export/import de cache pour la CI afin que les runners froids cessent de ruiner votre journée.
- Déplacez les métadonnées volatiles à la fin et gardez les images runtime petites avec des builds multi-étapes.
Faites ces cinq choses et vos builds ne seront pas seulement plus rapides — ils seront prévisibles. Des builds prévisibles, c’est ce qui permet de livrer à l’heure sans soudoyer les dieux du pipeline.