Maîtrisez la Mise en Cache des Couches Dockerfile pour des Constructions de Conteneurs Ultra-Rapides
Accélérez vos constructions Docker et rationalisez votre flux de développement en maîtrisant la mise en cache des couches Dockerfile. Ce guide complet révèle les meilleures pratiques pour optimiser l'ordre des instructions, tirer parti des constructions multi-étapes et comprendre les mécanismes de cache afin de réduire considérablement les temps de construction. Apprenez à rendre vos constructions Docker ultra-rapides et à améliorer l'efficacité de votre CI/CD.
Maîtrisez la Mise en Cache des Couches Dockerfile pour des Constructions de Conteneurs Ultra-Rapides
La mise en cache des couches Dockerfile fait la différence entre une construction qui se termine pendant que vous réfléchissez encore au changement et une construction qui rend chaque commit coûteux. Le cache n'est pas compliqué, mais il est impitoyable : copiez les mauvais fichiers trop tôt, et Docker n'a d'autre choix que de relancer les étapes lentes.
L'habitude principale est simple. Placez le travail stable tôt, placez le travail qui change fréquemment tard, et gardez le contexte de construction propre. Une fois que vous comprenez cela, la plupart des problèmes de performance des Dockerfiles deviennent visibles dans la sortie de construction.
Comprendre la Mise en Cache des Couches Docker
Docker construit les images de conteneurs en couches. Chaque instruction dans votre Dockerfile (comme RUN, COPY, ADD) crée une nouvelle couche. Lorsque vous construisez une image, Docker vérifie s'il a déjà exécuté cette instruction spécifique avec le même contexte (par exemple, les mêmes fichiers pour COPY) dans une construction précédente. Si une correspondance de cache se produit, Docker réutilise la couche existante de son cache au lieu d'exécuter à nouveau l'instruction. Cela peut faire gagner un temps considérable, en particulier pour les opérations coûteuses en calcul ou lors de la copie de fichiers volumineux.
Concepts Clés :
- Couche : Un instantané du système de fichiers immuable créé par une instruction Dockerfile.
- Correspondance de Cache : Lorsque Docker trouve une couche identique dans son cache pour une instruction donnée.
- Absence de Cache : Lorsque Docker ne trouve pas de couche correspondante et doit exécuter l'instruction, invalidant le cache pour toutes les instructions suivantes.
Comment Fonctionne le Cache Docker : Les Mécanismes
Docker détermine les correspondances de cache en fonction de l'instruction elle-même et des fichiers impliqués. Pour les instructions comme RUN echo 'hello', la chaîne de l'instruction est la clé de cache principale. Pour les instructions comme COPY ou ADD, Docker ne considère pas seulement l'instruction mais calcule également une somme de contrôle des fichiers copiés. Si l'instruction ou la somme de contrôle des fichiers change, cela entraîne une absence de cache.
Cela signifie que tout changement dans une instruction Dockerfile ou les fichiers associés invalidera le cache pour cette instruction et toutes les instructions suivantes. C'est un point crucial pour l'optimisation.
Optimiser les Dockerfiles pour une Utilisation Maximale du Cache
L'art de tirer parti du cache de construction de Docker réside dans la structuration de votre Dockerfile pour minimiser l'invalidation du cache, en particulier pour les instructions qui changent fréquemment. Le principe général est de placer les instructions qui sont moins susceptibles de changer plus tôt dans le Dockerfile, et celles qui changent plus fréquemment plus tard.
1. Ordonnez Vos Instructions de Manière Stratégique
La Règle d'Or : Placez les instructions stables en premier.
Considérez un Dockerfile d'application web typique. Vous pourriez avoir des étapes pour installer les dépendances, copier le code de l'application, puis exécuter une construction ou démarrer un serveur.
Exemple Inefficace (Invalidation du Cache) :
FROM ubuntu:latest
# Installe les paquets système (change rarement)
RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \
python3-pip \
&& rm -rf /var/lib/apt/lists/*
# Copie le code de l'application (change TRÈS souvent)
COPY . .
# Installe les dépendances Python (change souvent)
RUN pip install --no-cache-dir -r requirements.txt
# ... autres instructions
Dans cet exemple, chaque fois que vous modifiez une seule ligne de code d'application (parce que COPY . . est exécutée), le cache pour COPY . . et toutes les instructions suivantes (RUN pip install ...) sera invalidé. Cela signifie que pip install sera réexécuté même si requirements.txt n'a pas changé, entraînant des temps de construction plus longs.
Exemple Optimisé (Maximisation du Cache) :
FROM ubuntu:latest
# Installe les paquets système (change rarement)
RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \
python3-pip \
&& rm -rf /var/lib/apt/lists/*
# Copie UNIQUEMENT les fichiers de dépendances en premier (change moins souvent)
COPY requirements.txt .
# Installe les dépendances Python (met en cache si requirements.txt n'a pas changé)
RUN pip install --no-cache-dir -r requirements.txt
# Copie le reste du code de l'application (change TRÈS souvent)
COPY . .
# ... autres instructions
En copiant d'abord requirements.txt et en exécutant pip install immédiatement après, Docker peut mettre en cache la couche d'installation des dépendances. Si seul le code de l'application change (et que requirements.txt reste le même), l'étape pip install sera mise en cache, accélérant considérablement la construction.
2. Tirez Parti des Constructions Multi-Étapes
Les constructions multi-étapes sont une technique puissante pour réduire la taille de l'image, mais elles bénéficient également indirectement aux temps de construction en gardant les environnements de construction intermédiaires séparés. Chaque étape peut avoir ses propres couches mises en cache.
# Étape 1 : Constructeur
FROM golang:1.20 AS builder
WORKDIR /app
COPY go.mod ./
COPY go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o myapp
# Étape 2 : Image finale
FROM alpine:latest
WORKDIR /root/
COPY --from=builder /app/myapp .
CMD ["./myapp"]
Dans ce scénario, si seul le code source de l'application change (mais go.mod et go.sum ne changent pas), l'étape go mod download dans l'étape du constructeur sera mise en cache. Même si l'étape du constructeur doit réexécuter la compilation, l'étape finale sera toujours basée sur l'image alpine:latest qui est probablement mise en cache et seule l'instruction COPY --from=builder sera réexécutée si l'artefact myapp a changé.
3. Utilisez ADD et COPY à Bon Escient
COPYest généralement préféré pour copier des fichiers locaux dans l'image. C'est simple et prévisible.ADDa plus de fonctionnalités, comme la capacité d'extraire des archives et de récupérer des URL distantes. Cependant, ces fonctionnalités supplémentaires peuvent parfois entraîner un comportement inattendu et pourraient affecter l'invalidation du cache différemment. Tenez-vous en àCOPYsauf si vous avez explicitement besoin des fonctionnalités avancées d'ADD.
Lorsque vous utilisez COPY, soyez granulaire. Au lieu de COPY . ., envisagez de copier des répertoires ou des fichiers spécifiques qui changent à des rythmes différents, comme illustré dans l'exemple optimisé ci-dessus.
4. Nettoyez dans la Même Instruction RUN
Pour éviter le gonflement du cache et réduire la taille de l'image, nettoyez toujours les artefacts (comme les caches du gestionnaire de paquets) dans la même instruction RUN où ils ont été créés.
Mauvaise Pratique :
RUN apt-get update && apt-get install -y some-package
RUN rm -rf /var/lib/apt/lists/*
Ici, la commande rm est une instruction RUN séparée. Si some-package a été mis à jour (provoquant une absence de cache pour la première RUN), la deuxième RUN serait toujours exécutée, même si le nettoyage n'était pas strictement nécessaire pour la nouvelle couche. Plus important encore, la couche de cache intermédiaire créée par la première RUN pourrait encore contenir les listes de paquets téléchargés avant qu'elles ne soient nettoyées par la deuxième RUN.
Bonne Pratique :
RUN apt-get update && apt-get install -y some-package && rm -rf /var/lib/apt/lists/*
Cela garantit que tous les fichiers temporaires créés lors de l'installation du paquet sont supprimés immédiatement, et que la couche de cache créée représente un état du système de fichiers plus propre.
5. Évitez d'Installer les Dépendances à Chaque Fois
Comme démontré, copier les fichiers de définition des dépendances (requirements.txt, package.json, Gemfile, etc.) et installer les dépendances avant de copier votre code source d'application est une optimisation fondamentale de la mise en cache.
6. Invalidation Forcée du Cache (Quand Nécessaire)
Bien que l'objectif soit de maximiser la mise en cache, parfois vous voulez forcer une reconstruction du cache. C'est ce qu'on appelle l'invalidation forcée du cache. Les techniques courantes incluent :
- Changer un commentaire : Les commentaires Dockerfile (
#) sont ignorés, donc cela ne fonctionnera pas. - Ajouter un argument factice : Vous pouvez utiliser
ARGpour introduire une variable que vous modifiez pour casser le cache.
Vous construirez ensuite avecARG CACHEBUST=1 RUN echo "Invalidation du cache : ${CACHEBUST}" # Cette instruction sera réexécutée si CACHEBUST changedocker build --build-arg CACHEBUST=$(date +%s) . - Modifier une commande
RUNantérieure : Si vous modifiez une commande qui est plus tôt dans le Dockerfile, cela cassera le cache pour toutes les instructions suivantes.
L'invalidation forcée du cache doit être utilisée avec parcimonie, généralement lorsque vous devez garantir un téléchargement frais de ressources externes ou une construction propre de quelque chose qui n'est pas bien géré par le mécanisme de cache standard.
Docker BuildKit et Mise en Cache Améliorée
Les versions récentes de Docker ont introduit BuildKit comme moteur de construction par défaut. BuildKit offre des améliorations significatives en matière de mise en cache, notamment :
- Mise en Cache Distante : La capacité de partager le cache de construction entre différentes machines et exécuteurs CI/CD.
- Mise en cache plus granulaire : Meilleure identification de ce qui a changé.
- Exécution parallèle des constructions : Accélère les constructions même sans correspondances de cache.
BuildKit est généralement activé par défaut et offre souvent une meilleure mise en cache prête à l'emploi. Cependant, comprendre les principes décrits ci-dessus vous permettra toujours d'optimiser vos Dockerfiles pour BuildKit également.
Conseils pour une Mise en Cache Efficace des Dockerfiles
- Gardez les Dockerfiles propres et organisés : La lisibilité aide à identifier les opportunités d'optimisation.
- Testez votre cache : Après avoir apporté des modifications, observez la sortie de votre construction Docker. Recherchez les balises
[internal]ouCACHEDpour confirmer les correspondances de cache. - Utilisez
.dockerignore: Empêchez les fichiers inutiles (commenode_modules,.git, artefacts de construction) d'être copiés dans le contexte de construction, ce qui peut accélérer les instructionsCOPYet réduire le risque d'invalidation involontaire du cache. - Éliminez régulièrement votre cache Docker : Avec le temps, votre cache peut devenir volumineux. Utilisez
docker builder prunepour supprimer les couches de cache de construction inutilisées.
Un Exemple Réel Node.js
Le problème de mise en cache est plus facile à voir dans un projet Node.js. Ce Dockerfile fonctionne, mais il fait perdre du temps :
FROM node:22-slim
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build
CMD ["npm", "start"]
Chaque changement de source invalide COPY . ., ce qui signifie que npm ci s'exécute à nouveau. Une meilleure version sépare les métadonnées des dépendances du code de l'application :
FROM node:22-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
CMD ["npm", "start"]
Maintenant, un changement dans src/routes/account.ts n'invalide pas la couche d'installation des dépendances. Un changement dans package-lock.json le fait toujours, ce qui est exactement ce que vous voulez.
Pour les images de production, combinez cela avec une construction multi-étapes :
FROM node:22-slim AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
FROM node:22-slim AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM node:22-slim AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
CMD ["node", "dist/server.js"]
C'est plus long, mais cela sépare trois préoccupations : le téléchargement des dépendances, la construction de l'application et le contenu de l'exécution. Les exécuteurs CI peuvent mettre en cache les parties coûteuses, et l'image finale n'a pas besoin de tout l'arbre source ou des dépendances de développement.
Utilisez .dockerignore dans le Cadre de la Stratégie de Cache
La mise en cache des couches ne concerne pas seulement l'ordre des instructions Dockerfile. Le contexte de construction compte aussi. Si vous envoyez .git, les résultats de test locaux, les captures d'écran, les rapports de couverture ou node_modules dans le contexte de construction, Docker a plus de fichiers à vérifier et plus de chances d'invalider un COPY.
Un .dockerignore typique pour une application pourrait inclure :
.git
node_modules
dist
coverage
.env
*.log
tmp
.DS_Store
Soyez prudent avec .env. Vous voulez généralement l'exclure de l'image pour des raisons de sécurité et de reproductibilité. La configuration d'exécution doit être passée au démarrage du conteneur, pas intégrée dans la construction, sauf s'il s'agit d'une configuration de construction véritablement non sensible.
Sachez Quand le Cache Vous Ment
La mise en cache peut cacher des erreurs. L'exemple courant est celui des commandes du gestionnaire de paquets qui dépendent de dépôts externes. Si vous écrivez :
RUN apt-get update
RUN apt-get install -y curl
la couche apt-get update mise en cache peut devenir obsolète tandis que l'étape d'installation ultérieure s'attend à des métadonnées de paquets à jour. Gardez la mise à jour et l'installation ensemble :
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
La même idée s'applique aux dépendances linguistiques. Les fichiers de verrouillage rendent les constructions plus reproductibles et aident la mise en cache à se comporter de manière prévisible. Utilisez package-lock.json, pnpm-lock.yaml, poetry.lock, Gemfile.lock, go.sum, ou l'équivalent pour votre pile. Si votre Dockerfile installe des versions flottantes de dépendances depuis le réseau, les correspondances de cache peuvent être rapides mais les reconstructions peuvent vous surprendre plus tard.
Montages de Cache BuildKit pour les Gestionnaires de Paquets
BuildKit prend en charge les montages de cache qui accélèrent les gestionnaires de paquets sans mettre leurs caches dans la couche d'image finale. Par exemple, npm :
# syntax=docker/dockerfile:1.7
FROM node:22-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
COPY . .
RUN npm run build
Pour Python :
# 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 --no-cache-dir -r requirements.txt
COPY . .
Cela aide ou non en fonction de votre configuration CI. Un exécuteur de courte durée sans cache distant démarrera toujours à froid. Un exécuteur qui préserve le cache BuildKit entre les travaux peut faire gagner beaucoup de temps.
Cache Distant dans le CI
La mise en cache locale est facile ; la mise en cache CI nécessite une configuration délibérée. Avec docker buildx, vous pouvez pousser et tirer les métadonnées du cache depuis un registre :
docker buildx build \
--cache-from=type=registry,ref=registry.example.com/myapp:buildcache \
--cache-to=type=registry,ref=registry.example.com/myapp:buildcache,mode=max \
-t registry.example.com/myapp:${GIT_SHA} \
--push .
Cela permet à un nouvel exécuteur de réutiliser les couches construites par un exécuteur précédent. C'est particulièrement utile pour l'installation des dépendances et les grandes étapes de compilation. Gardez à l'esprit les contrôles d'accès : le cache de construction peut contenir des instantanés du système de fichiers provenant d'étapes intermédiaires, traitez donc le cache du registre privé comme faisant partie de votre infrastructure de construction, pas comme un espace de travail public.
Déboguer une Construction Docker Lente
Lorsqu'une construction est étonnamment lente, lisez la sortie plutôt que de deviner. BuildKit imprime les étapes qui sont mises en cache et celles qui sont exécutées. Si une étape d'installation de dépendances n'est pas mise en cache, regardez la ligne COPY ou RUN précédente. Quelque chose avant a changé.
Vérifications utiles :
docker build --progress=plain .
docker buildx du
docker builder prune --filter until=24h
N'élaguez pas aveuglément sur un hôte de construction partagé si d'autres travaux dépendent du cache. L'élagage est utile lorsque l'utilisation du disque est élevée, mais il peut ralentir la construction suivante.
La Liste de Vérification Pratique pour la Révision
Lors de la révision d'un Dockerfile pour des constructions plus rapides, parcourez de haut en bas et demandez :
- Les étapes de configuration de base stables sont-elles avant les fichiers source qui changent fréquemment ?
- Les manifestes de dépendances sont-ils copiés avant l'application complète ?
.dockerignoreexclut-il le bruit local et les secrets ?- Les caches du gestionnaire de paquets sont-ils gérés dans la même couche ou avec des montages de cache BuildKit ?
- L'image finale contient-elle uniquement ce dont elle a besoin pour fonctionner ?
- Le CI utilise-t-il un cache distant si les exécuteurs sont jetables ?
Les constructions Docker rapides sont généralement le résultat de nombreux petits choix ennuyeux. Mettez le travail lent et stable tôt. Mettez le travail qui change fréquemment tard. Gardez le contexte de construction propre. Ensuite, vérifiez avec la sortie de construction au lieu de supposer que le cache est utilisé.