Les builds Docker multi-étapes sont une technique fondamentale pour créer des images de conteneurs légères et prêtes pour la production, en séparant clairement l'environnement de construction de l'environnement d'exécution. Lorsqu'ils sont intégrés aux pipelines CI/CD, ils permettent une création d'images automatisée, reproductible et efficace. Ce guide propose une approche résolument pratique : configuration de votre environnement, écriture d'un Dockerfile multi-étapes sécurisé, configuration des pipelines dans GitHub Actions et GitLab CI, vérification des builds, reprise après des échecs courants et maintien du processus grâce à une liste de contrôle opérationnelle.

## Introduction

La livraison de logiciels moderne exige rapidité, fiabilité et sécurité. Les builds Docker multi-étapes répondent à ces trois exigences en permettant aux développeurs d'utiliser des outils de construction lourds dans des étapes intermédiaires, puis de ne copier que les artefacts nécessaires dans une image finale minimale. L'automatisation de ces builds en CI/CD supprime les étapes manuelles, assure la cohérence entre les environnements et rend chaque build auditable.

Cet article est résolument pratique. Vous allez inventorier votre environnement, écrire et tester un Dockerfile multi-étapes, configurer un pipeline CI/CD avec des optimisations de cache, vérifier l'image résultante, gérer les modes de défaillance courants et adopter une liste de contrôle de déploiement. Des commandes concrètes, des extraits de configuration et les sorties attendues sont fournis à chaque étape, afin que vous puissiez suivre dans vos propres dépôts.

## Inventaire des versions et de l'environnement

Avant d'automatiser les builds multi-étapes, confirmez que votre environnement répond aux exigences. Cette section répertorie les composants nécessaires, les versions et la topologie typique. Le responsable de la configuration de l'environnement doit être un ingénieur DevOps ou plateforme désigné, avec une revue trimestrielle.

### Prérequis

| Composant | Version recommandée | Objectif |
|---|---|---|
| Moteur Docker | 20.10+ avec BuildKit activé | Construction d'images avec prise en charge multi-étapes |
| Plateforme CI/CD | GitHub Actions, GitLab CI, Jenkins ou équivalent | Automatisation de l'exécution du pipeline |
| Registre de conteneurs | Docker Hub, AWS ECR, GCR, GHCR, etc. | Stockage des images construites |
| Dépôt source | Basé sur Git (GitHub, GitLab, Bitbucket) | Contrôle de version du Dockerfile et du code applicatif |
| Images de base | Par exemple, `node:18-alpine`, `golang:1.21-alpine` | Dépendances de l'étape de construction |

Les builds multi-étapes nécessitent Docker 17.05 ou supérieur, mais BuildKit fournit une mise en cache avancée des couches et une exécution parallèle des étapes. BuildKit est activé par défaut dans Docker 23.0+ ; pour les versions antérieures, définissez `DOCKER_BUILDKIT=1` dans votre environnement ou votre runner CI.

### Topologie

Une configuration typique comprend les composants suivants :

- **Poste de travail du développeur** : pour les tests locaux du Dockerfile et de l'application.
- **Runner CI/CD** : exécute les tâches du pipeline ; doit avoir Docker installé ou un accès à un démon Docker (par exemple, Docker-in-Docker pour GitLab).
- **Registre** : stocke les images construites pour le déploiement.
- **Cible de déploiement** : où l'image finale s'exécute (Kubernetes, VM, plateforme serverless, etc.).

Vérifiez Docker et BuildKit sur votre runner avec ces commandes :

```bash
docker version --format '{{.Server.Version}}'
# Exemple de sortie : 24.0.7

docker buildx version
# Exemple de sortie : github.com/docker/buildx v0.11.2
```

Si `docker buildx version` renvoie une erreur, installez le plugin buildx ou utilisez une image de runner qui l'inclut. Tous les exemples CI de ce guide supposent que BuildKit est disponible.

## Chemin de configuration sûr

Commencez par une configuration minimale et inspectable avant de passer à plusieurs services. Les étapes suivantes vous guident dans la création d'un Dockerfile multi-étapes et son intégration en CI/CD.

### Étape 1 : Créer un Dockerfile multi-étapes

Prenons l'exemple d'une application Node.js. La première étape installe toutes les dépendances et construit l'application ; la seconde étape ne copie que les artefacts de production et les dépendances d'exécution, laissant de côté les outils de construction.

Enregistrez ceci sous le nom `Dockerfile` à la racine de votre dépôt :

```dockerfile
# syntax=docker/dockerfile:1
# Étape 1 : Construction
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Étape 2 : Production
FROM node:18-alpine
ENV NODE_ENV=production
WORKDIR /app
COPY --from=builder /app/package*.json ./
RUN npm ci --only=production
COPY --from=builder /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/server.js"]
```

Points clés :

- La déclaration `AS builder` crée un alias utilisé dans `COPY --from=builder`.
- `npm ci` est utilisé au lieu de `npm install` pour des installations reproductibles basées sur le fichier de verrouillage.
- L'étape finale copie `package*.json` et exécute `npm ci --only=production`, puis ne copie que le répertoire compilé `dist`. Cela maintient un `node_modules` léger et exclut les dépendances de développement.
- `node:18-alpine` est utilisé dans les deux étapes, mais l'étape de production pourrait passer à une image d'exécution encore plus petite comme `node:18-alpine` (déjà minimale) ou `distroless` si votre application le permet.

### Étape 2 : Tester localement

Construisez l'image localement pour vous assurer que le Dockerfile fonctionne avant de toucher à la CI :

```bash
docker build -t myapp:test .
# Sortie attendue (tronquée) :
# => [builder 1/5] FROM docker.io/library/node:18-alpine
# => [builder 2/5] WORKDIR /app
# => [builder 3/5] COPY package*.json ./
# => [builder 4/5] RUN npm ci
# => [builder 5/5] COPY . .
# => [builder 6/6] RUN npm run build
# => [stage-1 1/4] FROM docker.io/library/node:18-alpine
# => [stage-1 2/4] WORKDIR /app
# => [stage-1 3/4] COPY --from=builder /app/package*.json ./
# => [stage-1 4/4] RUN npm ci --only=production
# => exporting to image
# => => naming to docker.io/library/myapp:test
```

L'indicateur clé est la ligne `COPY --from=builder` apparaissant dans la deuxième étape. Si cette ligne échoue, vérifiez le chemin source à l'intérieur de l'étape de construction.

Exécutez un conteneur à partir de l'image et vérifiez que l'application répond :

```bash
docker run -p 3000:3000 myapp:test
# Attendu : Server started on port 3000
```

Si vous voyez une erreur comme `Error: Cannot find module`, vous avez probablement manqué une dépendance d'exécution dans l'étape finale.

### Étape 3 : Configurer le pipeline CI/CD

Automatisez maintenant le build. Voici des exemples complets pour GitHub Actions et GitLab CI.

#### GitHub Actions

Créez `.github/workflows/build.yml` :

```yaml
name: Build and Push
on:
  push:
    branches: [ main ]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
      - name: Login to Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ghcr.io/${{ github.repository }}:latest
          cache-from: type=gha
          cache-to: type=gha, mode=max
```

Ce workflow se déclenche sur les poussées vers `main`, extrait le code, configure BuildKit, se connecte au registre de conteneurs GitHub en utilisant le `GITHUB_TOKEN` intégré, puis construit et pousse l'image. Les options `cache-from` et `cache-to` activent le cache GitHub Actions pour la mise en cache des couches, ce qui accélère considérablement les builds suivants.

Note de revue : Pour la production, envisagez d'utiliser un secret dédié comme `GHCR_PAT` au lieu de `GITHUB_TOKEN` si vous avez besoin de permissions plus larges, et évitez de pousser `latest` dans un environnement strict ; utilisez plutôt des tags basés sur le SHA du commit ou des versions sémantiques.

#### GitLab CI

Créez `.gitlab-ci.yml` :

```yaml
image: docker:24
services:
  - docker:24-dind
variables:
  DOCKER_BUILDKIT: 1
stages:
  - build
build-image:
  stage: build
  script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - docker build -t $CI_REGISTRY_IMAGE:latest .
    - docker push $CI_REGISTRY_IMAGE:latest
```

Cela utilise Docker-in-Docker (`dind`) pour fournir un démon Docker à l'intérieur du runner. La variable `DOCKER_BUILDKIT: 1` active BuildKit. Après la mise en scène, il se connecte au registre de conteneurs GitLab et construit/pousse. Pour utiliser le cache de GitLab, ajoutez un bloc `cache` avec `key: build-cache` et `paths: [.cache]` ou utilisez `--cache-from` avec une image de cache stockée dans le registre.

Responsable : La configuration du pipeline doit appartenir au responsable de l'équipe plateforme, avec des modifications revues par pull request.

### Étape 4 : Pousser vers le registre et vérifier

Après une exécution réussie du pipeline, l'image est disponible dans votre registre. Téléchargez-la et exécutez-la localement pour confirmer :

```bash
docker pull ghcr.io/votrenom/votrerepo:latest
docker run -p 3000:3000 ghcr.io/votrenom/votrerepo:latest
# Attendu : Server started on port 3000
```

Si l'image ne s'exécute pas, revenez aux tests locaux pour déterminer si le problème vient du Dockerfile ou du pipeline.

## Vérification et diagnostics

Des vérifications automatisées sont essentielles pour garantir que les builds sont corrects et efficaces. Cette section couvre la vérification post-build et les commandes de diagnostic que vous pouvez exécuter dans le pipeline ou localement.

### Comparaison de la taille des images

Les builds multi-étapes doivent produire des images nettement plus petites que les builds à une seule étape. Comparez les tailles :

```bash
docker images myapp
# Exemple de sortie :
# REPOSITORY   TAG       IMAGE ID       CREATED          SIZE
# myapp        test      abc123def456   2 minutes ago    150MB
```

Un build Node à une seule étape avec toutes les dépendances de développement ferait généralement 400 Mo ou plus. Une image de 150 Mo suggère que l'approche multi-étapes fonctionne, bien que vous puissiez encore réduire davantage (par exemple, en utilisant des images de base `distroless`).

Si l'image est anormalement volumineuse, inspectez le Dockerfile pour détecter des directives `COPY --from` manquées ou des fichiers inutiles copiés (par exemple, un `.dockerignore` n'excluant pas `node_modules` du contexte de build).

### Inspecter les couches de l'image

Utilisez `docker history` pour voir la taille des couches et confirmer que seuls les fichiers nécessaires sont inclus :

```bash
docker history myapp:test
# Exemple de sortie (tronquée) :
# IMAGE          CREATED          CREATED BY                                      SIZE
# abc123def456   2 minutes ago    CMD ["node", "dist/server.js"]                 0B
# 789ghi012jkl   2 minutes ago    EXPOSE map[3000/tcp:{}]                       0B
# 456mno345pqr   2 minutes ago    COPY /app/dist ./dist # buildkit                12MB
# 123stu456vwx   2 minutes ago    RUN /bin/sh -c npm ci --only=production # b...  80MB
# ...
```

Recherchez les couches anormalement grandes. La couche `npm ci --only=production` peut encore être volumineuse si les dépendances sont lourdes, mais elle ne doit pas inclure d'outils de développement comme `webpack` ou des frameworks de test.

### Exécuter le conteneur et vérifier les journaux

Démarrez le conteneur en mode détaché et inspectez les journaux :

```bash
docker run -d --name myapp-test -p 3000:3000 myapp:test
docker logs myapp-test
# Attendu : "Server started on port 3000"
```

Si le conteneur se termine immédiatement, exécutez-le sans `-d` pour voir l'erreur au premier plan, ou utilisez `docker logs` pour afficher le message de plantage.

### Vérifier le point de terminaison de l'application

Utilisez `curl` à l'intérieur du réseau du conteneur ou depuis l'hôte pour tester le service :

```bash
curl http://localhost:3000/health
# Attendu : {"status":"ok"}
```

Pour une vérification plus rigoureuse, ajoutez une étape de test de fumée à votre pipeline CI après le build. Par exemple, dans GitHub Actions, ajoutez une étape qui exécute le conteneur et interroge le point de terminaison avant de pousser ou après avoir tiré l'image.

### Journaux du pipeline

En CI/CD, examinez les journaux de build pour détecter les avertissements. Recherchez :

- Les avertissements d'installation de dépendances (par exemple, packages obsolètes).
- Les arguments de build inutilisés.
- Les tailles de couches volumineuses signalées par BuildKit.
- Les échecs de cache (si vous attendiez des succès de cache).

Pour GitHub Actions, consultez les journaux de l'étape "Build and push". Pour GitLab, examinez les journaux des tâches dans la vue du pipeline. Les informations de succès/échec de cache sont souvent affichées lors de l'utilisation de `cache-from` et `cache-to`.

## Pièges courants et comment les éviter

Même les équipes expérimentées commettent des erreurs avec les builds multi-étapes en CI/CD. Voici les pièges les plus fréquents, leurs causes et des étapes de récupération concrètes.

### 1. Ne pas utiliser de fichier `.dockerignore`

**Pourquoi cela arrive :** Les développeurs supposent que Docker ne copie que ce qui est nécessaire, mais le contexte de build inclut tous les fichiers du répertoire (sauf exclusion). Cela peut gonfler le contexte, ralentir les builds et même divulguer des secrets.

**Comment éviter :** Créez un `.dockerignore` à la racine du dépôt avant de construire. Un `.dockerignore` Node.js typique :

```
node_modules
npm-debug.log
.git
.env
*.md
```

Cela empêche le `node_modules` local d'écraser l'installation du conteneur et garde les secrets hors de l'image.

### 2. Fichier de verrouillage manquant dans l'étape de construction

**Pourquoi cela arrive :** Les développeurs utilisent `npm install` au lieu de `npm ci`, ou oublient de commiter `package-lock.json`.

**Comment éviter :** Commitez toujours le fichier de verrouillage. Dans le Dockerfile, utilisez `npm ci` dans les deux étapes. Si vous oubliez, le build peut réussir mais avec des dépendances non reproductibles, ou échouer avec une erreur claire concernant le fichier de verrouillage manquant.

Récupération : Si le fichier de verrouillage est manquant, générez-le localement (`npm install --package-lock-only`), commitez-le et relancez le pipeline.

### 3. Copier depuis la mauvaise étape ou le mauvais chemin

**Pourquoi cela arrive :** Des fautes de frappe dans les alias d'étape ou des chemins incorrects à l'intérieur de l'étape de construction. Par exemple, `COPY --from=builder /app/dist ./dist` échoue si la sortie de build se trouve dans `/app/build`.

**Comment éviter :** Testez localement d'abord. Utilisez `docker build --progress=plain` pour voir les commandes de copie exactes. En CI, le même indicateur peut être défini avec des `build-args` ou des variables d'environnement si vous utilisez `docker build` directement.

Récupération : Examinez le journal de la couche en échec, corrigez le chemin et commitez la correction.

### 4. Ignorer la configuration du cache de build

**Pourquoi cela arrive :** Les équipes s'appuient sur le cache par défaut, qui peut ne pas persister entre les exécutions CI, entraînant des builds lents et une bande passante inutile.

**Comment éviter :** Configurez explicitement la mise en cache. Dans GitHub Actions, utilisez `cache-from: type=gha` et `cache-to: type=gha, mode=max`. Dans GitLab, utilisez `--cache-from` avec une image de cache stockée dans le registre (par exemple, `docker build --cache-from $CI_REGISTRY_IMAGE:cache`).

Récupération : Vérifiez les succès de cache en recherchant "CACHED" dans le journal de build. S'il n'y a pas de succès, assurez-vous que le cache est bien poussé et tiré, et que l'ordre du Dockerfile n'invalide pas le cache prématurément (par exemple, `COPY . .` avant `RUN npm ci`).

### 5. Exécuter en tant que root dans l'étape finale

**Pourquoi cela arrive :** L'utilisation de l'utilisateur `root` par défaut est courante mais viole le principe du moindre privilège et peut être un risque de sécurité.

**Comment éviter :** Ajoutez un utilisateur non-root dans l'étape finale :

```dockerfile
FROM node:18-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
WORKDIR /app
COPY --from=builder /app/package*.json ./
RUN npm ci --only=production
COPY --from=builder /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/server.js"]
```

Cela réduit la surface d'attaque si le conteneur est compromis.

### 6. Ne pas étiqueter correctement les images

**Pourquoi cela arrive :** N'utiliser que `latest` rend le retour en arrière et l'audit difficiles.

**Comment éviter :** Étiquetez les images avec un identifiant unique tel que le SHA du commit ou une version sémantique. Dans GitHub Actions, vous pouvez définir `tags: ghcr.io/${{ github.repository }}:${{ github.sha }}` avec `latest`. Dans GitLab, utilisez `$CI_COMMIT_SHORT_SHA`.

Récupération : Si vous n'avez que `latest`, vous pourrez peut-être re-étiqueter une ancienne image du registre si vous connaissez son empreinte, mais il est bien préférable d'implémenter dès maintenant un étiquetage approprié.

## Modes de défaillance et récupération

Même avec une configuration soignée, des échecs surviennent. Cette section identifie les problèmes courants et fournit des étapes de récupération concrètes.

### Modes de défaillance courants

| Défaillance | Cause possible | Récupération |
|---|---|---|
| Échec du build lors de l'installation des dépendances | Problème réseau ou fichier de verrouillage manquant | Vérifiez la connectivité réseau, assurez-vous que le fichier de verrouillage est commité. Relancez la tâche après correction du réseau. |
| Fichiers manquants dans l'image finale | Chemin `COPY --from` incorrect | Vérifiez les chemins dans le Dockerfile, reconstruisez avec `--progress=plain` pour voir les commandes de copie. |
| L'image s'exécute mais l'application plante | Dépendance d'exécution manquante ou mauvais point d'entrée | Inspectez les journaux du conteneur, testez localement, ajustez l'étape finale. |
| CI/CD ne peut pas pousser vers le registre | Échec d'authentification | Reconfigurez les informations d'identification du registre, vérifiez les secrets. Pour GitHub Actions, assurez-vous que `GITHUB_TOKEN` a la permission `packages: write`. |
| Le cache de build ne fonctionne pas | BuildKit non activé ou configuration de cache incorrecte | Assurez-vous que BuildKit est activé, ajustez les paramètres de cache. Vérifiez les journaux CI pour les lignes d'exportation/importation de cache. |
| Contexte de build trop volumineux | Pas de `.dockerignore` ou fichiers inutiles | Ajoutez un `.dockerignore`, optimisez le contexte. |

### Stratégies de retour en arrière

Si une nouvelle image est défectueuse, revenez à une image précédente connue comme bonne en utilisant l'une de ces stratégies :

**Retour en arrière basé sur les tags :** Conservez les tags de version dans le registre. Déployez en utilisant un tag spécifique, pas `latest`.

```bash
docker pull ghcr.io/votrenom/votrerepo:v1.2.3
docker run -p 3000:3000 ghcr.io/votrenom/votrerepo:v1.2.3
```

**Retour en arrière au registre :** La plupart des registres prennent en charge les politiques de rétention et les empreintes d'image. Utilisez un tag précédent ou re-étiquetez une image connue comme bonne en utilisant son empreinte :

```bash
docker tag ghcr.io/votrenom/votrerepo@sha256:abc123... ghcr.io/votrenom/votrerepo:rollback
```

**Retour en arrière du pipeline :** Revenez sur le commit qui a introduit le Dockerfile ou le changement de pipeline problématique, puis relancez le pipeline.

Responsable : Le gestionnaire de version ou l'ingénieur d'astreinte est propriétaire de la décision de retour en arrière, exécutée dans les 15 minutes suivant la détection de l'échec.

### Vérifications de récupération

Après un retour en arrière, vérifiez les points suivants :

- Le conteneur s'exécute avec la version attendue (vérifiez avec `docker inspect` ou le point de terminaison de version de l'application).
- Les journaux ne montrent aucune erreur.
- Le point de terminaison de santé répond correctement.
- Le pipeline CI/CD passe sur un commit de test.
- Les tableaux de bord de surveillance affichent des métriques normales.

## Liste de contrôle opérationnelle

Utilisez cette liste de contrôle pour assurer le bon fonctionnement continu des builds multi-étapes en CI/CD. L'ingénieur plateforme doit revoir cette liste mensuellement et la mettre à jour si nécessaire.

- [ ] Le Dockerfile utilise la syntaxe multi-étapes et est versionné dans le contrôle de source.
- [ ] Le build local réussit avec `docker build`.
- [ ] Le pipeline CI/CD est configuré et se déclenche sur les événements appropriés (par exemple, poussée vers main, pull requests).
- [ ] Les informations d'identification du registre sont stockées en toute sécurité (par exemple, GitHub Secrets, variables CI GitLab).
- [ ] Le cache de build est activé et efficace ; vérifiez les journaux du pipeline pour les succès de cache.
- [ ] La taille de l'image est dans la plage attendue (par exemple, moins de 200 Mo pour un service Node typique) ; taille rapportée dans les artefacts ou journaux du pipeline.
- [ ] Le démarrage du conteneur est testé dans le pipeline via une étape de test de fumée.
- [ ] La procédure de retour en arrière est documentée et testée au moins une fois.
- [ ] Revue régulière du Dockerfile pour les mises à jour et les correctifs de sécurité (par exemple, mises à jour mensuelles de l'image de base).
- [ ] Surveillance et alertes pour les échecs de registre et de déploiement.
- [ ] Utilisateur non-root configuré dans l'étape finale.
- [ ] Tags immuables utilisés pour la production, avec `latest` uniquement pour la commodité du développement.

## Conclusion

L'automatisation des builds Docker multi-étapes en CI/CD rationalise le chemin du code à l'artefact déployable. En suivant les étapes de ce guide, vous pouvez créer des images légères, réduire les temps de build grâce à la mise en cache et maintenir un pipeline fiable.

Commencez par un projet pilote étroit, comme recommandé : choisissez un seul service, implémentez le Dockerfile et le pipeline, vérifiez minutieusement, puis étendez à d'autres services. N'oubliez pas de surveiller et d'itérer en continu. Attribuez une propriété claire pour la configuration du pipeline et les procédures de retour en arrière afin d'éviter toute ambiguïté.

Prochaines étapes :

- Appliquez le Dockerfile multi-étapes à l'une de vos applications.
- Configurez le pipeline CI/CD et poussez vers un registre.
- Exécutez les commandes de vérification et passez en revue la liste de contrôle.
- Documentez et testez votre plan de retour en arrière.
- Partagez l'approche avec votre équipe et recueillez des commentaires.

En adoptant ces pratiques, vous améliorerez l'efficacité de livraison de votre équipe et réduirez les risques de production.