đł Dockerfile : multi-stage, cache et secrets pour des images rapides et sĂ»res
đ Introduction
Un Dockerfile qui « marche » est facile Ă Ă©crire. Un Dockerfile qui construit vite, produit une image lĂ©gĂšre et ne fuit aucun secret, câest un autre mĂ©tier. Or câest exactement lĂ que se joue le confort au quotidien : un build de trois minutes au lieu de trente secondes, une image de 1,2 Go au lieu de 80 Mo, ou un token dâAPI qui traĂźne dans une couche publique â tout ça se dĂ©cide dans le Dockerfile.
Cet article part du principe que les bases sont acquises (si ce nâest pas le cas, commencez par les fondamentaux de Docker) et se concentre sur les quatre techniques qui font la diffĂ©rence : lâordre des couches pour le cache, le build multi-stage, les cache mounts BuildKit et les secrets de build. Toutes reposent sur des fonctionnalitĂ©s stables et documentĂ©es â rien dâexotique.
đ§± Lâordre des couches nâest pas anodin
Chaque instruction dâun Dockerfile crĂ©e une couche, et Docker met ces couches en cache. Tant quâune instruction et ses fichiers dâentrĂ©e ne changent pas, Docker rĂ©utilise la couche au lieu de la reconstruire. Mais dĂšs quâune couche est invalidĂ©e, toutes les suivantes le sont aussi.
DâoĂč la rĂšgle dâor : ordonner du moins changeant au plus changeant. Lâerreur classique est de copier tout le code avant dâinstaller les dĂ©pendances :
# â Ă Ă©viter : le moindre changement de code rĂ©installe tout
COPY . .
RUN npm ciEn copiant dâabord le manifeste de dĂ©pendances, on ne rĂ©installe que lorsquâil change rĂ©ellement :
# â
le cache de npm ci survit aux changements de code
COPY package*.json ./
RUN npm ci
COPY . .ComplĂ©ment indispensable : un fichier .dockerignore. Sans lui, COPY . . embarque node_modules, .git, les fichiers dâenvironnement et les artefacts locaux dans le contexte de build â ce qui gonfle lâimage et invalide le cache pour rien.
đ Comment fonctionne le cache de build Docker ?
En une phrase : Docker calcule une empreinte de chaque instruction (la commande et le contenu des fichiers quâelle copie), et si cette empreinte a dĂ©jĂ Ă©tĂ© construite, il rĂ©utilise la couche existante au lieu de lâexĂ©cuter.
ConcrĂštement, la cascade dâinvalidation explique tout : si vous modifiez une ligne Ă lâĂ©tape 3, les Ă©tapes 1 et 2 restent en cache, mais 3, 4, 5⊠sont rejouĂ©es. Câest pourquoi placer les instructions lentes et stables (installation de paquets systĂšme, dĂ©pendances) en haut, et les instructions volatiles (copie du code applicatif) en bas, change radicalement le temps de build au quotidien.
𧩠Multi-stage : séparer le build du runtime
Câest la technique la plus rentable pour allĂ©ger une image. Le principe : une premiĂšre Ă©tape contient tout lâoutillage de compilation ; une seconde, minimale, ne reçoit que le produit fini.
# Ătape de build : compilateurs, dĂ©pendances de dev, etc.
FROM node:22 AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Image finale : rien que le nécessaire pour exécuter
FROM node:22-alpine
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/server.js"]Le COPY --from=build va chercher uniquement les fichiers utiles dans lâĂ©tape prĂ©cĂ©dente. Lâimage livrĂ©e ne contient ni le code source, ni les dĂ©pendances de dĂ©veloppement, ni la chaĂźne de compilation. On passe couramment de plusieurs centaines de Mo Ă quelques dizaines. Jâai dĂ©taillĂ© une application concrĂšte de ce pattern dans le multi-stage build pour Laravel.
⥠Cache mounts BuildKit : accélérer les dépendances
Lâordre des couches met en cache le rĂ©sultat dâun npm ci ou dâun composer install â mais dĂšs que le manifeste change, tout est retĂ©lĂ©chargĂ©. Les cache mounts de BuildKit rĂ©solvent ça : ils montent un cache persistant qui survit dâun build Ă lâautre, sans ĂȘtre inclus dans lâimage.
# syntax=docker/dockerfile:1
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ciIci, le cache de npm (/root/.npm) est conservĂ© entre les builds. MĂȘme quand npm ci doit se relancer, il retrouve les paquets dĂ©jĂ tĂ©lĂ©chargĂ©s au lieu de tout retirer du rĂ©seau. Sur des dĂ©pendances lourdes, le gain se compte en minutes.
La directive # syntax=docker/dockerfile:1 en premiÚre ligne est obligatoire pour débloquer la syntaxe --mount. Elle active le frontend Dockerfile de BuildKit, qui est le moteur de build par défaut de Docker moderne.
đ Secrets de build : jamais dans une couche
Voici lâerreur la plus dangereuse. Pour rĂ©cupĂ©rer une dĂ©pendance privĂ©e pendant le build, la tentation est de passer un token via ARG ou ENV :
# â le token finit inscrit dans l'image, lisible via `docker history`
ARG NPM_TOKEN
RUN npm ciLe problĂšme : tout ce qui passe par ARG ou ENV est gravĂ© dans les couches de lâimage et ressort avec docker history. Le token est exposĂ© Ă quiconque rĂ©cupĂšre lâimage.
La bonne mĂ©thode, ce sont les secrets de build BuildKit. Le secret est montĂ© le temps dâune instruction RUN, dans un fichier sous /run/secrets/, et nâest jamais persistĂ© :
# syntax=docker/dockerfile:1
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN --mount=type=secret,id=npmtoken \
NPM_TOKEN="$(cat /run/secrets/npmtoken)" npm ciEt Ă la construction, on fournit le secret depuis un fichier ou une variable :
docker build --secret id=npmtoken,src=./npm_token.txt .Le secret est disponible pendant le RUN, puis disparaĂźt. Aucune trace dans les couches, aucune fuite via docker history. En CI, on branche ce mĂ©canisme sur le coffre Ă secrets du pipeline â jâen parle cĂŽtĂ© GitHub Actions.
â ïž Quelques prĂ©cautions
- Toujours un
.dockerignore. Sans lui, le cache et la taille dâimage partent en fumĂ©e, et des fichiers sensibles peuvent se retrouver dans le contexte de build. - Ăpinglez vos images de base.
node:22-alpinereste prĂ©visible ;node:latestchange sous vos pieds et casse le cache de façon imprĂ©visible. - Un secret nâest pas un
ARG. Répétons-le :ARG/ENVpour un token est une fuite. Utilisez--mount=type=secret. - Attention au
sharingdes cache mounts. Pour des gestionnaires sensibles aux accĂšs concurrents (apt, par exemple), ajoutezsharing=lockedafin dâĂ©viter les corruptions lors de builds parallĂšles.
đ Conclusion
Un bon Dockerfile nâest pas quâune liste dâinstructions qui aboutit : câest un artefact quâon optimise. Ordonner les couches du stable au volatile pour exploiter le cache, sĂ©parer build et runtime en multi-stage pour allĂ©ger lâimage, monter des cache mounts BuildKit pour accĂ©lĂ©rer les dĂ©pendances, et passer les secrets via --mount=type=secret pour ne rien laisser fuiter â ces quatre rĂ©flexes transforment lâexpĂ©rience de build.
Aucun ne demande dâoutil supplĂ©mentaire : tout est dans BuildKit, activĂ© par dĂ©faut. Le seul prĂ©requis, câest la ligne # syntax=docker/dockerfile:1 en tĂȘte de fichier. Ă partir de lĂ , vos images sont plus rapides Ă construire, plus lĂ©gĂšres Ă dĂ©ployer et plus sĂ»res Ă distribuer.
đ Liens utiles
/faq
Questions fréquentes
Comment réduire la taille d'une image Docker ?
+
La technique la plus efficace est le build multi-stage : on compile l'application dans une premiÚre étape avec tous les outils de build, puis on copie uniquement le résultat dans une image finale minimale. L'image livrée ne contient ni compilateur, ni dépendances de développement, ni code source superflu.
Pourquoi ne faut-il pas mettre un secret dans un ARG ou un ENV Docker ?
+
Parce que les valeurs passées via ARG ou ENV sont inscrites dans les couches de l'image et visibles avec « docker history ». Un token ou un mot de passe y reste lisible par quiconque récupÚre l'image. Il faut utiliser les secrets de build BuildKit (RUN --mount=type=secret), qui ne sont jamais persistés dans une couche.
Comment accélérer un build Docker avec le cache ?
+
Deux leviers : ordonner les instructions du moins au plus changeant (copier le manifeste de dépendances avant le code source) pour maximiser les couches réutilisées, et utiliser les cache mounts BuildKit (RUN --mount=type=cache) pour conserver les caches de gestionnaires de paquets entre deux builds.