Introduction
Les builds Docker lents grèvent les budgets CI et masquent les vrais échecs derrière de longues files d'attente. Le gain le plus rapide vient de la compréhension de la manière dont Docker décide de réutiliser le travail, puis de l'organisation de votre Dockerfile et de votre CI pour toucher le cache de façon prévisible. Ce guide montre comment :
- Diagnostiquer les manques de cache et lire les logs de build
- Ordonner les couches pour la réutilisation
- Utiliser les montages de cache BuildKit pour les caches d'outils spécifiques à chaque langage
- Verrouiller l'installation des dépendances avec des lockfiles
- Persister le cache entre les exécuteurs CI avec
docker buildx - Tester les changements localement avec Docker Compose et déployer en toute sécurité
Vue d'ensemble du flux de travail
Utilisez cette séquence de bout en bout pour garder les changements sûrs et mesurables :
Chronométrez un build propre et un rebuild sans changement. Enregistrez la sortie couche par couche pour la comparaison.
- Reference
Activez BuildKit localement et dans la CI. Passez aux logs de progression simples pour la visibilité.
- Activer BuildKit
Réorganisez les instructions pour que les couches stables viennent en premier. Ajoutez un .dockerignore strict pour éviter les invalidations bruyantes.
- Corriger l'ordre des couches et le contexte
Utilisez RUN --mount=type=cache pour persister les caches d'outils (pip, npm, modules Go, apt).
- Ajouter des montages de cache pour les dépendances
Utilisez docker buildx avec --cache-to et --cache-from ciblant un registre.
- Persister le cache entre les exécutions CI
Parcourez une liste de contrôle pour attraper les manques courants.
- Dépanner les restes
Validez la performance et la correction sur un service d'abord.
- Piloter localement, puis déployer
Comment fonctionne le cache de build Docker
Chaque instruction du Dockerfile crée une couche. Docker peut réutiliser une couche mise en cache si :
- Le texte de l'instruction est identique
- Les arguments de build et l'environnement qui affectent cette instruction sont identiques
- Tous les fichiers que l'instruction lit depuis le contexte de build sont inchangés
Implications clés :
COPY/ADDsont sensibles au cache : changer un fichier référencé invalide cette couche et toutes les couches suivantes.- L'ordre des instructions compte : quand une couche manque, toutes les couches suivantes doivent être reconstruites.
- Un
.dockerignoreserré réduit les manques de cache accidentels en excluant les logs, builds locaux, venvs,node_modules, couverture, tmp et autres bruits.
Exemple de .dockerignore :
.git
**/__pycache__/
**/*.pyc
node_modules/
.env
build/
dist/
.coverage
coverage/
tmp/
.DS_Store
Ordonner les couches pour une réutilisation maximale
Arrangez votre Dockerfile pour que le travail stable se fasse avant le travail volatile :
- Stable : image de base, paquets OS, runtimes de langage
- Semi-stable : installation des dépendances clé sur les lockfiles
- Volatile : code source de l'application
Schéma général :
# syntax=docker/dockerfile:1.7
FROM python:3.11-slim AS app
# 1) Dépendances système d'abord
RUN apt-get update \
&& apt-get install -y --no-install-recommends build-essential \
&& rm -rf /var/lib/apt/lists/*
# 2) Copier uniquement les lockfiles, puis installer les dépendances
WORKDIR /app
COPY requirements.txt ./
# Installation des dépendances ici (voir montage de cache ci-dessous)
# 3) Copier le reste seulement après l'installation des dépendances
COPY . .
# 4) Commande finale
CMD ["python", "-m", "my_service" ]
Pourquoi cela aide : modifier un fichier source ne force pas la réinstallation des dépendances car la couche de dépendances ne dépend que de requirements.txt.
Évitez les pièges courants :
- Ne placez pas d'
ARGouENVchangeants fréquemment avant les couches lourdes. - Ne faites pas
COPYde tout le dépôt avant d'installer les dépendances clés par les lockfiles. - Gardez les commandes
RUNdéterministes (versions épinglées, listes de paquets triées).
Montages de cache BuildKit qui aident
BuildKit peut persister les caches pour les outils sans les cuire dans l'image finale. Activez-le et utilisez --mount=type=cache.
Activer BuildKit et les logs simples :
export DOCKER_BUILDKIT=1
export BUILDKIT_PROGRESS=plain
Exemple Python avec cache de wheels pip
# syntax=docker/dockerfile:1.7
FROM python:3.11-slim AS app
WORKDIR /app
RUN --mount=type=cache,target=/root/.cache/pip \
pip install --upgrade pip
COPY requirements.txt ./
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txt
COPY . .
CMD ["python", "-m", "my_service" ]
Exemple Node.js avec cache npm
# syntax=docker/dockerfile:1.7
FROM node:20-alpine AS app
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci --prefer-offline --no-audit --no-fund
COPY . .
CMD ["node", "server.js" ]
Exemple Go avec cache de modules
# syntax=docker/dockerfile:1.7
FROM golang:1.22-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
COPY . .
RUN --mount=type=cache,target=/go/pkg/mod \
CGO_ENABLED=0 go build -o /out/app ./cmd/app
FROM gcr.io/distroless/base
COPY --from=builder /out/app /app
ENTRYPOINT [
/app
]
Exemple de cache apt
# syntax=docker/dockerfile:1.7
FROM debian:stable-slim
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/*
Notes :
- Le montage de cache persiste entre les builds sur la même machine. Pour persister entre les machines CI, exportez/importez le cache via buildx (section suivante).
- Alignez les cibles de cache avec les défauts de vos outils (pip :
/root/.cache/pip, npm :/root/.npm, Go :/go/pkg/mod).
Patterns d'installation des dépendances
Suivez ces règles pour une réutilisation fiable :
- Utilisez toujours des lockfiles (
requirements.txt,poetry.lock,Pipfile.lock,package-lock.json,yarn.lock,go.sum). Épinglez les versions. COPYuniquement les lockfiles d'abord, puis installez les dépendances.COPYle reste du code source après.- Utilisez des commandes propres et favorables au cache :
npm cipour Node.js ; pip avec cache de wheels ;go mod downloadavant le build. - Évitez le travail réseau dans les couches ultérieures. Placez les téléchargements dans des couches précoces et mises en cache.
- Pour les builds multi-étapes, gardez les dépendances de build uniquement dans l'étape builder et copiez seulement les artefacts vers l'étape finale.
Exemple multi-étapes pour un front React derrière une API Python :
# syntax=docker/dockerfile:1.7
FROM node:20-alpine AS ui
WORKDIR /ui
COPY ui/package*.json ./
RUN --mount=type=cache,target=/root/.npm npm ci --prefer-offline
COPY ui/. .
RUN npm run build
FROM python:3.11-slim AS api
WORKDIR /app
COPY requirements.txt ./
RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt
COPY . .
COPY --from=ui /ui/build ./static
CMD ["python", "-m", "api" ]
Accélérations CI avec cache de registre
Utilisez docker buildx pour persister le cache entre les exécutions CI.
Initialisez buildx localement (les exécuteurs CI le font souvent pour vous) :
docker buildx create --use --name ci-builder || docker buildx use ci-builder
docker buildx inspect --bootstrap
Build avec export de cache vers un registre :
# Build et push de l'image et du cache
IMAGE=registry.example.com/my/app:$(git rev-parse --short HEAD)
CACHE_REF=registry.example.com/my/app:buildcache
docker buildx build \
--progress=plain \
--push \
--tag "$IMAGE" \
--cache-to type=registry,ref=$CACHE_REF,mode=max \
--cache-from type=registry,ref=$CACHE_REF \
-f Dockerfile .
Notes :
mode=maxstocke plus de métadonnées pour une meilleure réutilisation entre les changements.- Gardez
CACHE_REFstable entre les exécutions pour le même repo/service. - Récupérez
cache-fromavant de builder pour que les exécuteurs distants en bénéficient immédiatement.
Docker Compose pour les tests locaux (cibles d'étape de build) :
services:
app:
build:
context: .
dockerfile: Dockerfile
target: app
environment:
- PYTHONUNBUFFERED=1
ports:
- "8080:8080"
command: ["python", "-m", "my_service" ]
Liste de contrôle de dépannage
Parcourez cette liste quand une étape n'est pas mise en cache :
- Activez les logs simples
export DOCKER_BUILDKIT=1
export BUILDKIT_PROGRESS=plain
Vérifiez quelles étapes disent CACHED et lesquelles se réexécutent.
- Vérifiez la taille et le bruit du contexte
- Lancez :
docker buildx build . --progress=plain 2>&1 | head -n 50 - Assurez-vous que
.dockerignoreexclut :node_modules,venv, artefacts de build, logs,tmp,.git(sauf si vous en avez besoin).
- Vérifiez l'ordre des COPY
COPYles lockfiles d'abord ; lancez les installations ;COPYla source en dernier.- Si vous
COPY .avant d'installer les dépendances, chaque changement de code casse le cache de la couche de dépendances.
- Stabilisez ARG/ENV
- Les arguments de build qui changent par commit doivent être placés après les couches lourdes.
- Évitez les étapes
RUNnon déterministes (timestamps, graines aléatoires,curl | shsans épingler les versions).
- Alignez les montages de cache
- Confirmez que votre outil utilise le chemin que vous avez monté. Pour pip, vérifiez
/root/.cache/pipaprès un build. Pour npm,/root/.npm.
- Vérifiez la dérive de l'image de base
- Si le digest
FROMchange fréquemment, les couches de dépendances peuvent être invalidées. Envisagez d'épingler sur un digest (ex:python:3.11-slim@sha256:...).
- Multi-étapes et cache-from
- Si vous utilisez le multi-étapes, assurez-vous que
cache-frompointe vers la bonne image ou le bon nom d'étape que vous avez poussé précédemment.
- Reproduisez sur une machine propre
- Récupérez
cache-from, puis buildez. Si ça manque encore, les entrées de la clé de cache changent entre les exécutions.
Plan de pilote local
Choisissez un service aux builds lents et lancez cette expérience étroite et mesurable :
Objectif : Réduire le temps de rebuild répété de 40%+ sans changer le comportement à l'exécution.
Périmètre : Uniquement le cache d'installation des dépendances pour un service (par exemple, wheels pip ou cache npm) et le réordonnancement des couches du Dockerfile autour des lockfiles.
Étapes :
Chronométrez deux builds dos à dos :
- Reference
time docker build -t pilot:baseline .
time docker build -t pilot:rebuild .
Enregistrez le deuxième temps ; c'est la référence de cache hit.
- Activez BuildKit et réordonnez les couches
- Ajoutez
# syntax=docker/dockerfile:1.7 - Exportez
DOCKER_BUILDKIT=1etBUILDKIT_PROGRESS=plain COPYles lockfiles d'abord ; installez les dépendances ; puisCOPYla source.
- Ajoutez les montages de cache
- Pour pip :
--mount=type=cache,target=/root/.cache/pip - Pour npm :
--mount=type=cache,target=/root/.npm
- Rebuildez et mesurez à nouveau
time docker build -t pilot:optimized .
time docker build -t pilot:optimized2 .
Attendez-vous à ce que le deuxième build optimisé soit beaucoup plus rapide.
- Vérification fonctionnelle avec Docker Compose
docker compose up --build
# Visitez le service ou lancez une commande de smoke test rapide
Optionnel : testez le cache distant Poussez --cache-to et rebâtissez sur un exécuteur frais avec --cache-from.
Critères de sortie :
- Le temps de rebuild répété réduit de 40%+
- Le service démarre et passe un smoke test basique
Si le pilote atteint les objectifs, appliquez le pattern aux autres services.
Conclusion
Vous pouvez réduire les temps de build CI en combinant quatre pratiques : le bon ordonnancement des couches, les montages de cache BuildKit pour les dépendances, un .dockerignore strict et un cache de registre pour la réutilisation entre exécuteurs. Commencez par un petit pilote local, mesurez les gains et déployez le pattern service par service.