## Introduction

La mise à niveau et la migration des builds multi-étapes Docker ne se résument pas à une simple opération de copier-coller. Il s'agit d'une séquence d'observations, de modifications ciblées et d'étapes de vérification qui protègent votre pipeline de build et les images qu'il produit. Ce guide vous accompagne du début à la fin – depuis la vérification de vos versions actuelles de Docker et BuildKit jusqu'à la restauration en toute sécurité en cas de problème.

Vous apprendrez à :

- Inventorier votre environnement Docker actuel et la configuration du build multi-étapes
- Identifier les exigences de compatibilité et choisir la bonne version de mise à niveau
- Tester les changements de manière contrôlée avant de toucher à la production
- Valider que le nouveau build produit une image correcte et plus légère
- Récupérer après des échecs courants sans perdre de données ni d'historique de build

Les exemples utilisent une application Node.js réaliste, mais les modèles s'appliquent à n'importe quel langage ou framework. Les commandes sont présentées avec les sorties attendues et les signaux d'échec afin que vous puissiez comparer ce que vous voyez avec ce qui devrait se produire.

## Inventaire des versions et de l'environnement

Avant de changer quoi que ce soit, documentez l'état exact de votre installation Docker et des builds que vous migrez. Cela vous donne une base de comparaison et un point de restauration.

### Vérifier le moteur Docker et la CLI

Exécutez ces commandes et enregistrez la sortie :

```bash
docker version
```

La sortie attendue comprend des sections distinctes pour le client et le serveur avec les numéros de version. Exemple :

```
Client: Docker Engine - Community
 Version:           24.0.7
 API version:       1.43
 Go version:        go1.20.10
 Git commit:        311b9ff
 Built:             Thu Oct 26 09:08:01 2023
 OS/Arch:           linux/amd64
 Context:           default

Server: Docker Engine - Community
 Engine:
  Version:          24.0.7
  API version:      1.43 (minimum version 1.12)
  Go version:       go1.20.10
  Git commit:       311b9ff
  Built:            Thu Oct 26 09:08:01 2023
  OS/Arch:          linux/amd64
  Experimental:     false
```

Si la section Serveur est absente ou affiche une erreur, le démon Docker n'est pas en cours d'exécution. Démarrez-le avec `sudo systemctl start docker` (Linux) ou Docker Desktop (Windows/macOS).

### Vérifier le support de BuildKit

Les builds multi-étapes utilisent BuildKit pour des fonctionnalités avancées comme le cache mount et les secrets. Vérifiez si BuildKit est activé :

```bash
docker buildx version
```

Sortie attendue :

```
github.com/docker/buildx v0.12.0 4b6b4b8
```

Si vous voyez `docker: 'buildx' is not a docker command`, installez le plugin buildx ou activez le builder hérité. Pour la plupart des versions modernes de Docker (20.10+), BuildKit est le builder par défaut. Vérifiez avec :

```bash
docker buildx ls
```

Exemple de sortie :

```
NAME/NODE       DRIVER/ENDPOINT STATUS  BUILDKIT PLATFORMS
default *       docker
  default       default         running 20.10.24 linux/amd64, linux/arm64
desktop-linux   docker
  desktop-linux desktop-linux   running 23.0.6  linux/amd64, linux/arm64
```

### Identifier votre Dockerfile multi-étapes actuel

Examinez le Dockerfile que vous prévoyez de migrer. Un build multi-étapes typique pour une application Node.js peut ressembler à ceci :

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

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

Notez les noms des étapes (`builder`, `production`), les images de base et les copies de fichiers. Ce seront les points focaux des changements de mise à niveau.

### Vérification des données et volumes avant la mise à niveau

Si votre processus de build utilise des volumes pour la mise en cache (par exemple, les caches npm ou Maven), confirmez où ils sont stockés. Un volume nommé comme `npm-cache:/root/.npm` persiste entre les builds et peut être réutilisé. Un montage bind comme `./.npm-cache:/root/.npm` dépend de l'existence du répertoire hôte.

Pour tester la persistance, exécutez un build avec la mise en cache activée, puis redémarrez le démon Docker et reconstruisez. Comparez les temps de build et vérifiez si le cache a été réutilisé. Exemple :

```bash
docker buildx build --push -t myapp:test --build-arg BUILDKIT_INLINE_CACHE=1 .
# Notez le temps de build, puis reconstruisez après le redémarrage
time docker buildx build --push -t myapp:test --build-arg BUILDKIT_INLINE_CACHE=1 .
```

Si le deuxième build est presque aussi rapide que le premier et que les journaux montrent `CACHED`, votre configuration de volume est correcte. Si le deuxième build ré-exécute toutes les étapes, le cache n'a pas été persisté et vous devez ajuster les montages de volume.

## Chemin de configuration sûr

Cette section couvre les étapes réelles de migration : mise à jour du Dockerfile, de l'environnement et de la configuration du builder de manière à pouvoir être restaurée.

### Choisir la version cible

Consultez les notes de version de Docker et BuildKit pour les changements cassants. Par exemple, Docker Engine 23.0 a déprécié le builder hérité et a fait de BuildKit le défaut. Si vous passez d'une version plus ancienne, prévoyez de tester BuildKit explicitement.

Exemple de version cible :

- Actuel : Docker 20.10.24, BuildKit 0.8.2
- Cible : Docker 24.0.7, BuildKit 0.12.0

Vérifiez que votre runner CI ou hôte de production peut installer la version cible. Pour les systèmes basés sur apt :

```bash
sudo apt-get update
sudo apt-get install docker-ce=5:24.0.7-1~ubuntu.22.04~jammy docker-ce-cli=5:24.0.7-1~ubuntu.22.04~jammy
```

Épinglez toujours la version exacte pour éviter des mises à niveau accidentelles.

### Mettre à jour les définitions d'étapes du Dockerfile

Migrez les changements de syntaxe. Par exemple, si vous utilisiez le format hérité `FROM ... AS`, il est inchangé. Mais si vous utilisiez une ancienne syntaxe de cache mount, mettez-la à jour.

Ancienne syntaxe (avant BuildKit 0.9) :

```dockerfile
RUN --mount=type=cache,target=/root/.npm npm ci
```

Nouvelle syntaxe :

```dockerfile
RUN --mount=type=cache,target=/root/.npm npm ci
```

(Pas de changement ici, mais assurez-vous de ne pas utiliser de drapeaux dépréciés comme `--stream` dans `docker build`.)

Plus important : si vous passez d'un build mono-étape à multi-étapes, divisez le build. Pour une application Python :

Avant :

```dockerfile
FROM python:3.9
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
CMD ["python", "app.py"]
```

Après :

```dockerfile
FROM python:3.9 AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user -r requirements.txt

FROM python:3.9-slim
WORKDIR /app
COPY --from=builder /root/.local /root/.local
COPY . .
ENV PATH=/root/.local/bin:$PATH
CMD ["python", "app.py"]
```

Cela réduit la taille de l'image en excluant les outils de build et les caches.

### Ajuster les arguments de build et l'environnement

Si votre build utilise `ARG` pour des dépendances versionnées, mettez-les à jour pour correspondre à la nouvelle image de base. Exemple :

```dockerfile
ARG NODE_VERSION=18
FROM node:${NODE_VERSION} AS builder
```

Lors de la migration vers Node 20, changez la valeur par défaut :

```dockerfile
ARG NODE_VERSION=20
```

Puis construisez avec :

```bash
docker build --build-arg NODE_VERSION=20 -t myapp:node20-test .
```

Ne codez pas en dur les versions à l'intérieur du Dockerfile sauf si nécessaire ; utilisez des valeurs par défaut ARG et surchargez au moment du build pour plus de flexibilité.

### Tester le nouveau build isolément

Avant de remplacer les images existantes, construisez avec un tag différent et exécutez des tests de fumée.

```bash
docker build -t myapp:upgrade-test .
docker run -d --name upgrade-test -p 3001:3000 myapp:upgrade-test
curl http://localhost:3001/health
```

Attendu : `{"status":"ok"}`.

Vérifiez les journaux :

```bash
docker logs upgrade-test --tail 20
```

Si l'application ne démarre pas, inspectez le conteneur :

```bash
docker inspect upgrade-test | jq '.[0].State'
```

Recherchez `"Running": false` et `"ExitCode": 1` avec un message d'erreur.

### Exécuter un build parallèle et comparer les images

Comparez les anciennes et nouvelles images en taille et en couches :

```bash
docker images myapp
```

Exemple de sortie :

```
REPOSITORY   TAG             IMAGE ID       CREATED          SIZE
myapp        old-prod        1a2b3c4d5e6f   2 weeks ago      450MB
myapp        upgrade-test    a1b2c3d4e5f6   10 minutes ago   320MB
```

Une migration multi-étapes réussie devrait réduire la taille de l'image, parfois de 50 % ou plus. Utilisez `docker history myapp:upgrade-test` pour vérifier que seuls les fichiers de l'étape finale sont présents.

## Vérification et diagnostics

Après la mise à niveau, confirmez que le nouveau build est correct et que le conteneur résultant se comporte comme prévu.

### Vérification automatisée du build

Exécutez le build avec des vérifications qui échouent rapidement en cas d'erreur :

```bash
docker build --progress=plain --no-cache -t myapp:verify . 2>&1 | tee build.log
```

Inspectez `build.log` pour tout avertissement ou erreur. Problèmes courants :

- `WARNING: No output specified for stage X` - une étape est inutilisée ou mal définie
- `ERROR: failed to solve: failed to compute cache key` - les chemins de fichiers ne correspondent pas
- `ERROR: executor failed running [/bin/sh -c npm run build]: exit code: 1` - le script de build a échoué

Chacun de ces problèmes nécessite une correction différente : vérifiez les noms d'étapes, confirmez les chemins COPY ou déboguez la commande de build.

### Commandes de vérification à l'exécution

Pour le conteneur en cours d'exécution, exécutez des vérifications de santé :

```bash
docker exec upgrade-test sh -c "curl -s http://localhost:3000/health"
```

Attendu : `{"status":"ok"}`.

Vérifiez que le conteneur utilise les couches d'image de base attendues :

```bash
docker inspect upgrade-test | jq '.[0].Config.Image'
```

Attendu : `myapp:upgrade-test` (ou le digest de l'image de base).

### Comparaison des performances et des ressources

Mesurez le temps de démarrage et l'utilisation de la mémoire entre les anciens et nouveaux conteneurs.

```bash
time docker run --rm myapp:old-prod true
time docker run --rm myapp:upgrade-test true
```

Pour la mémoire :

```bash
docker stats --no-stream upgrade-test
```

Enregistrez les valeurs. Un build multi-étapes ne devrait pas augmenter l'utilisation des ressources ; souvent, il la diminue car l'image finale est plus légère.

### Valider le contenu de l'image

Assurez-vous que l'image finale ne contient pas d'outils de build ou de code source. Utilisez un conteneur temporaire pour explorer :

```bash
docker run --rm -it myapp:upgrade-test sh
```

À l'intérieur, exécutez :

```bash
ls -la /app
```

Vous devriez voir uniquement la sortie compilée et les dépendances d'exécution. Si vous voyez des artefacts de build comme `node_modules` avec des dépendances de développement, la copie multi-étapes est trop large.

Corrigez en copiant uniquement les répertoires nécessaires :

```dockerfile
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
```

Ne copiez pas `/app` en entier.

## Modes d'échec et récupération

Même avec une planification minutieuse, les mises à niveau peuvent échouer. Voici les modes d'échec courants et comment récupérer.

### Échec de build dû à des fonctionnalités BuildKit manquantes

Si le build échoue avec une erreur comme :

```
ERROR: BuildKit is enabled but the buildx component is missing or broken
```

Ou :

```
ERROR: failed to solve: rpc error: code = Unknown desc = failed to load cache key
```

Cela se produit souvent lorsque la version de Docker est plus ancienne que les fonctionnalités utilisées dans le Dockerfile. Vérifiez la version de BuildKit et envisagez d'utiliser le builder hérité comme solution de repli temporaire :

```bash
DOCKER_BUILDKIT=0 docker build -t myapp:legacy .
```

Mais ne comptez pas sur le mode hérité à long terme ; mettez à niveau Docker à la place.

### Échec de tirage de l'image de base

La nouvelle image de base peut ne pas exister ou être incompatible avec votre architecture.

Erreur :

```
ERROR: failed to solve: node:20: not found
```

Vérifiez les tags disponibles :

```bash
docker manifest inspect node:20
```

Si aucun manifeste, utilisez un tag différent comme `node:20-alpine`. Restauration : revenez à la ligne `FROM` du Dockerfile précédent et reconstruisez.

### Échec d'installation des dépendances dans l'étape de build

Une étape d'installation de paquets échoue en raison de conflits de versions ou de bibliothèques natives manquantes.

Exemple avec Node :

```
> node-gyp rebuild
make: g++: No such file or directory
```

Correction : ajoutez des outils de build uniquement à l'étape de build :

```dockerfile
FROM node:18 AS builder
RUN apt-get update && apt-get install -y python3 make g++
```

Ces outils ne sont pas inclus dans l'étape finale, donc la taille de l'image reste petite.

### Crash à l'exécution après un build réussi

Le conteneur se construit mais se termine immédiatement avec un code non nul.

Vérifiez les journaux :

```bash
docker logs upgrade-test
```

Erreur courante : bibliothèque partagée manquante ou variable d'environnement. Par exemple :

```
Error: Cannot find module 'express'
```

Cela signifie que les dépendances de production n'ont pas été installées. Dans le Dockerfile, assurez-vous que `npm ci --only=production` s'exécute dans l'étape finale ou copiez `node_modules` depuis le builder.

Restauration : arrêtez et supprimez le conteneur cassé, puis exécutez l'ancienne image :

```bash
docker stop upgrade-test && docker rm upgrade-test
docker run -d --name prod-old -p 3000:3000 myapp:old-prod
```

### Perte de données due à une mauvaise configuration de volume

Si le nouveau conteneur ne trouve pas les données existantes, vérifiez les montages de volume.

```bash
docker inspect upgrade-test | jq '.[0].Mounts'
```

Si le volume attendu est manquant, recréez le conteneur avec le bon drapeau `-v` :

```bash
docker run -d --name upgrade-test -v app_data:/var/lib/app -p 3000:3000 myapp:upgrade-test
```

Ne supprimez jamais les volumes nommés tant que vous n'avez pas vérifié que le nouveau conteneur fonctionne.

## Liste de contrôle des opérations

Utilisez cette liste de contrôle avant et pendant une mise à niveau de build multi-étapes. Chaque élément a un responsable et une fréquence de révision.

| Étape | Responsable | Action | Vérifier | Revisiter |
|------|-------------|--------|--------|---------|
| 1. Documenter les versions actuelles | Ingénieur DevOps (ex. Priya Shah) | Exécuter `docker version` et `docker buildx version`, sauvegarder la sortie | Versions enregistrées dans le runbook | Annuellement ou avant toute mise à niveau |
| 2. Examiner le Dockerfile pour les modèles multi-étapes | Développeur d'application (ex. Marcus Lee) | Identifier toutes les lignes `FROM`, `AS`, `COPY --from` | Aucun nom d'étape manquant | À chaque changement de Dockerfile |
| 3. Choisir la version cible Docker/BuildKit | Ingénieur DevOps | Vérifier les notes de version et la matrice de compatibilité | Version cible notée dans le ticket | Par cycle de mise à niveau |
| 4. Construire l'image de test avec la nouvelle version | Pipeline CI | Exécuter `docker build -t myapp:upgrade-test .` en staging | Le build se termine sans erreur | À chaque build |
| 5. Exécuter des tests de fumée sur le conteneur de test | Ingénieur QA (ex. Sofia Garcia) | Exécuter la vérification de santé et les tests fonctionnels | Tous les tests passent | À chaque candidat de version |
| 6. Comparer les tailles d'images et les couches | Ingénieur DevOps | `docker images` et `docker history` | Taille de la nouvelle image <= taille de l'ancienne ou justifiée | À chaque build |
| 7. Plan de restauration documenté | Ingénieur DevOps | Écrire les étapes de restauration dans le runbook | Runbook mis à jour et révisé | Avant le déploiement en production |
| 8. Surveiller la production après le déploiement | Ingénieur de fiabilité des sites (ex. Tom Okafor) | Regarder les journaux, les métriques et les taux d'erreur pendant 24 heures | Aucun pic d'erreur nouveau | Immédiatement après le déploiement, puis hebdomadairement pendant le premier mois |

Responsabilité : chaque élément a un seul responsable nommé. La fréquence de révision est spécifiée pour garantir une exactitude continue.

## Pièges courants et comment les éviter

### Ne pas épingler les versions des images de base

Utiliser `FROM node:latest` ou `FROM ubuntu` conduit à des changements inattendus lorsque l'image de base est mise à jour. Épinglez toujours la version majeure et mineure :

```dockerfile
FROM node:18.17.1 AS builder
```

Ou utilisez le digest :

```dockerfile
FROM node:18@sha256:... AS builder
```

Cela rend les builds reproductibles.

### Copier des fichiers inutiles dans l'étape finale

Une erreur courante est `COPY --from=builder /app /app`, qui inclut le code source, les outils de build et les secrets. À la place, copiez uniquement les artefacts d'exécution :

```dockerfile
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./
RUN npm ci --only=production
```

Cela réduit la taille de l'image et la surface d'attaque.

### Ignorer l'invalidation du cache de build

Les builds multi-étapes peuvent mettre en cache les couches de manière incorrecte si l'ordre de `COPY` et `RUN` n'est pas optimal. Pour Node.js, copiez d'abord `package.json` et `package-lock.json`, puis exécutez `npm ci`, puis copiez le reste du code source. Ainsi, l'installation des dépendances est mise en cache à moins que ces fichiers ne changent.

Avant :

```dockerfile
COPY . .
RUN npm ci
```

Après :

```dockerfile
COPY package*.json ./
RUN npm ci
COPY . .
```

Maintenant, les changements dans les fichiers sources n'invalident pas le cache des dépendances.

### Sauter les tests de restauration

De nombreuses équipes testent la mise à niveau mais pas la restauration. Testez toujours le retour à la version d'image précédente.

```bash
docker run -d --name rollback-test -p 3000:3000 myapp:old-prod
curl http://localhost:3000/health
```

Si l'image de restauration fonctionne, vous avez un chemin sûr.

### Ne pas utiliser les secrets BuildKit

Si votre étape de build a besoin d'accéder à des paquets privés ou à des identifiants, évitez de passer les secrets comme arguments de build car ils persistent dans l'historique de l'image. Utilisez les secrets BuildKit :

```dockerfile
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci
```

Construisez avec :

```bash
docker build --secret id=npmrc,src=$HOME/.npmrc -t myapp:secure .
```

Cela garde les secrets hors de l'image finale.

## Conclusion

La mise à niveau et la migration des builds multi-étapes Docker est un processus contrôlé, pas une seule commande. En documentant votre état actuel, en apportant des changements ciblés, en vérifiant chaque étape et en planifiant la restauration, vous protégez votre pipeline de déploiement et l'exécution de l'application.

Points clés à retenir :

- Vérifiez toujours les versions de Docker et BuildKit avant de changer quoi que ce soit
- Mettez à jour le Dockerfile de manière incrémentale, étape par étape
- Testez la nouvelle image isolément avec un tag différent
- Comparez les tailles et contenus des images pour assurer une image finale plus légère
- Ayez un plan de restauration et testez-le
- Attribuez les responsabilités et révisez les listes de contrôle régulièrement

Utilisez ce guide comme modèle pour votre propre mise à niveau. Adaptez les commandes et les exemples à votre application et environnement spécifiques, et vous réduirez les risques et les temps d'arrêt.

Prochaine étape : choisissez un petit build multi-étapes de votre projet, exécutez les commandes d'inventaire et créez une base de référence. Ensuite, tentez une mise à niveau de version dans un environnement de test et vérifiez les résultats. Documentez ce qui a fonctionné et ce qui n'a pas fonctionné, et vous serez mieux préparé pour des migrations plus importantes.