## 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 / ADD sont 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 .dockerignore serré 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' ARG ou ENV changeants fréquemment avant les couches lourdes.

- Ne faites pas COPY de tout le dépôt avant d'installer les dépendances clés par les lockfiles.

- Gardez les commandes RUN dé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.

- COPY uniquement les lockfiles d'abord, puis installez les dépendances. COPY le reste du code source après.

- Utilisez des commandes propres et favorables au cache : npm ci pour Node.js ; pip avec cache de wheels ; go mod download avant 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=max stocke plus de métadonnées pour une meilleure réutilisation entre les changements.

- Gardez CACHE_REF stable entre les exécutions pour le même repo/service.

- Récupérez cache-from avant 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 .dockerignore exclut : node_modules , venv , artefacts de build, logs, tmp , .git (sauf si vous en avez besoin).

- Vérifiez l'ordre des COPY

- COPY les lockfiles d'abord ; lancez les installations ; COPY la 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 RUN non déterministes (timestamps, graines aléatoires, curl | sh sans é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/pip après un build. Pour npm, /root/.npm .

- Vérifiez la dérive de l'image de base

- Si le digest FROM change 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-from pointe 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=1 et BUILDKIT_PROGRESS=plain

- COPY les lockfiles d'abord ; installez les dépendances ; puis COPY la 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.