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 :
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 :
# 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 buildercrée un alias utilisé dansCOPY --from=builder. npm ciest utilisé au lieu denpm installpour des installations reproductibles basées sur le fichier de verrouillage.- L'étape finale copie
package*.jsonet exécutenpm ci --only=production, puis ne copie que le répertoire compilédist. Cela maintient unnode_modulesléger et exclut les dépendances de développement. node:18-alpineest utilisé dans les deux étapes, mais l'étape de production pourrait passer à une image d'exécution encore plus petite commenode:18-alpine(déjà minimale) oudistrolesssi votre application le permet.
Étape 2 : Tester localement
Construisez l'image localement pour vous assurer que le Dockerfile fonctionne avant de toucher à la CI :
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 :
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 :
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 :
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 :
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 :
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 :
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 :
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 :
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 :
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.
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 :
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 inspectou 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
latestuniquement 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.