Chapitre 1214 minIntermédiaire
Docker dans la CI/CD : construire, publier et déployer une image
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- décrire le circuit d'une image, du commit à la production ;
- construire pour la bonne architecture et publier dans un registre ;
- installer un registre privé protégé par HTTPS et mot de passe ;
- écrire un script de déploiement qui revient en arrière en cas d'échec.
4 exercices corrigés
Utiliser Docker dans la CI/CD, c'est confier au pipeline la construction de l'image à chaque modification du code, son test, son analyse de sécurité et sa publication dans un registre, puis déployer sur le serveur exactement l'image qui a été testée. Ce chapitre présente le circuit complet de sorbier-frais, de GitHub au serveur, installe un registre privé pour les outils internes, et écrit le script de déploiement de srv-gestion. Le détail des workflows est dans le cours Intégration continue.
Le circuit d'une image, du commit au serveur
| Étape | Où | Ce qui se passe |
|---|---|---|
| 1. Commit et pull request | GitHub | Le code est relu ; la CI construit l'image et lance les tests, sans rien publier |
2. Fusion sur main ou tag v2.5.0 | GitHub Actions | La CI construit l'image, lance les tests, l'analyse avec un scanner, puis la pousse dans ghcr.io avec ses étiquettes |
| 3. Recette | Serveur de recette | On y déploie l'image sha-… ou 2.5.0 et on vérifie avec les utilisateurs |
| 4. Production | srv-gestion | On déploie la même image, sans la reconstruire |
Le workflow GitHub Actions, en bref
Le workflow de sorbier-frais utilise les actions officielles de Docker :
- uses: docker/setup-buildx-action@v4
- uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: meta
uses: docker/metadata-action@v6
with:
images: ghcr.io/sorbier/sorbier-frais
tags: |
type=sha
type=semver,pattern={{version}}
- uses: docker/build-push-action@v7
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
Le jeton GITHUB_TOKEN du workflow suffit pour pousser dans ghcr.io, à condition d'accorder la permission packages: write au job. Le workflow complet (déclencheurs, cache, permissions, test de fumée, équivalent GitLab CI) est expliqué pas à pas dans le chapitre Construire et publier une image Docker dans la CI.
Entre la construction et la publication, on ajoute l'analyse de l'image. Pour ne pas dépendre d'une action tierce, on peut lancer le scanner lui-même, en conteneur, à une version précise :
- name: Analyser l'image
run: |
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
aquasec/trivy:${TRIVY_VERSION}@${TRIVY_DIGEST} image \
--severity CRITICAL --ignore-unfixed --exit-code 1 \
sorbier-frais:test
Ici, monter le socket est acceptable : le runner de la CI est une machine jetable, détruite à la fin du job, et Trivy a besoin de lire l'image construite localement. La version et l'empreinte (sha256:…) de l'image Trivy sont fixées dans le workflow (variables TRIVY_VERSION et TRIVY_DIGEST), à une version vérifiée, jamais latest : une étiquette peut être réécrite, comme en mars 2026, une empreinte non. L'image testée est d'abord construite localement (docker build -t sorbier-frais:test .), puis poussée seulement si l'analyse passe (détail).
Construire pour la bonne architecture
Une image construite sur un Mac à processeur Apple Silicon est en arm64 ; srv-gestion est en amd64. Lancée sur le serveur, elle échoue avec exec format error. Trois solutions :
- construire dans la CI, dont les runners sont en
amd64; - préciser la plateforme :
docker build --platform linux/amd64 -t … .; - construire une image multi-architecture avec Buildx :
docker buildx build --platform linux/amd64,linux/arm64 -t … --push ..
Un registre privé pour les outils internes
Les applications de Sorbier sont dans ghcr.io, privées. Pour les petits outils internes (scripts de sauvegarde, images de dépannage) et pour garder une copie des images critiques dans l'entreprise, le service informatique installe un registre privé sur srv-gestion, derrière le même proxy Caddy, à l'adresse registre.ad.sorbier.example.
Le registre libre de Docker (registry:3) ne fournit aucune interface et peu de sécurité : on lui ajoute une authentification par fichier htpasswd (mots de passe hachés avec bcrypt) et on le place derrière le proxy HTTPS. Le fichier de mots de passe se génère avec l'outil htpasswd de l'image Apache, l'image du registre ne le contenant plus :
sudo mkdir -p /srv/gestion/registre
docker run --rm --entrypoint htpasswd httpd:2.4 -Bbn ci-sorbier 'Un-Mot-De-Passe-Long-2026' \
| sudo tee /srv/gestion/registre/htpasswd > /dev/null
Le service, ajouté au compose.yaml de srv-gestion :
registre:
image: registry:3
environment:
REGISTRY_AUTH: htpasswd
REGISTRY_AUTH_HTPASSWD_REALM: "Registre Sorbier"
REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd
volumes:
- ./registre/htpasswd:/auth/htpasswd:ro
- registre-donnees:/var/lib/registry
networks:
- frontal
restart: unless-stopped
(avec registre-donnees: sous la clé volumes: du bas), et dans le Caddyfile :
registre.ad.sorbier.example {
tls /certs/registre.crt /certs/registre.key
reverse_proxy registre:5000
}
puis lance le registre et recharge Caddy, qui ne relit pas seul son fichier :
sudo docker compose up -d
sudo docker compose exec -w /etc/caddy proxy caddy reload
Depuis le poste d'administration :
docker login registre.ad.sorbier.example
docker tag outil-sauvegarde:1.2 registre.ad.sorbier.example/outils/sauvegarde:1.2
docker push registre.ad.sorbier.example/outils/sauvegarde:1.2
Le script de déploiement de srv-gestion
Le déploiement en production se fait par un script, versionné avec compose.yaml, qui fait toujours les mêmes gestes dans le même ordre : exporter la base, changer la version, récupérer les images, relancer, vérifier, et revenir en arrière si la vérification échoue.
#!/usr/bin/env bash
# deployer.sh : déploie une version de sorbier-frais sur srv-gestion
# Usage : sudo ./deployer.sh 2.5.0
set -euo pipefail
VERSION="${1:?Usage : $0 <version>}"
DOSSIER=/srv/gestion
URL_SANTE=https://gestion.ad.sorbier.example/api/health
cd "$DOSSIER"
ANCIENNE=$(grep '^VERSION=' .env | cut -d= -f2)
echo "Version actuelle : $ANCIENNE, version demandée : $VERSION"
# 1. Export de la base avant toute modification
mkdir -p /srv/sauvegardes
docker compose exec -T bdd sh -c 'exec mysqldump --single-transaction --databases frais -uroot -p"$(cat /run/secrets/db_root_password)"' \
> "/srv/sauvegardes/frais-avant-$VERSION-$(date +%F-%H%M).sql"
# 2. Nouvelle version et images
sed -i "s/^VERSION=.*/VERSION=$VERSION/" .env
docker compose pull api front
docker compose up -d
# 3. Vérification : l'API doit répondre dans les 60 secondes
for essai in $(seq 1 12); do
if curl --fail --silent --output /dev/null "$URL_SANTE"; then
echo "Version $VERSION en service."
exit 0
fi
sleep 5
done
# 4. Échec : retour à la version précédente
echo "Échec de la vérification, retour à la version $ANCIENNE." >&2
sed -i "s/^VERSION=.*/VERSION=$ANCIENNE/" .env
docker compose up -d
exit 1
| Partie | Rôle |
|---|---|
set -euo pipefail | Le script s'arrête à la première erreur, sur une variable non définie, ou si une commande d'un tube échoue |
${1:?…} | Refuse de démarrer sans numéro de version |
exec -T bdd … mysqldump | Export logique de la base dans le conteneur ; -T désactive le terminal, indispensable pour rediriger la sortie vers un fichier |
$(cat /run/secrets/db_root_password) | Entre apostrophes simples, la substitution est faite dans le conteneur, qui lit son secret : le mot de passe n'apparaît pas sur l'hôte |
La boucle for | Douze essais espacés de 5 secondes ; curl --fail renvoie une erreur sur un code HTTP 4xx ou 5xx |
| Le retour arrière | Remet l'ancienne version ; la base, elle, n'est pas restaurée automatiquement : c'est une décision humaine, avec l'export à disposition |
Le script ne restaure pas la base de lui-même : si la nouvelle version a déjà modifié la structure des tables, l'administrateur décide de restaurer l'export après avoir analysé la situation. L'écriture de scripts Bash robustes est détaillée dans le cours Scripts PowerShell et Bash. À plus grande échelle, ces gestes sont automatisés par un outil comme Ansible, ou par la CI elle-même (déploiement continu).
Exercices
Exercice 1 sur 4 Promouvoir la même image Réussi À revoir
La recette de sorbier-frais 2.5.0 est validée. Un développeur propose de relancer docker build sur la branche main pour produire l'image de production « pour être sûr d'avoir la dernière version ». Explique pourquoi c'est une mauvaise idée et ce qu'il faut faire.
Voir la réponseMasquer la réponse de l'exercice 1
Une nouvelle construction produit une autre image : l'image de base node:24-slim a pu changer entre-temps, une dépendance aussi, et main a peut-être reçu d'autres commits. On déploierait en production une image qui n'a jamais été testée en recette.
Il faut déployer la même image que celle de la recette : son étiquette fixe (2.5.0) ou, mieux, son empreinte @sha256:…. C'est le principe « construire une fois, déployer partout ».
Tu avais trouvé ?
Exercice 2 sur 4 exec format error Réussi À revoir
Un développeur construit l'image sur son Mac M3, la pousse dans le registre, et sur srv-gestion le conteneur s'arrête aussitôt avec exec /usr/local/bin/docker-entrypoint.sh: exec format error. Explique et donne deux solutions.
Voir la réponseMasquer la réponse de l'exercice 2
L'image a été construite pour l'architecture du Mac, arm64 ; srv-gestion est en amd64 (x86-64). Le processeur du serveur ne sait pas exécuter les binaires de l'image.
Solutions : construire l'image dans la CI (runners amd64), ce qui est de toute façon la bonne pratique ; ou construire en précisant la plateforme, docker build --platform linux/amd64 … ; ou publier une image multi-architecture avec docker buildx build --platform linux/amd64,linux/arm64 … --push.
Tu avais trouvé ?
Exercice 3 sur 4 Relire le script de déploiement Réussi À revoir
Que se passe-t-il, avec le script deployer.sh de ce chapitre, dans chacun de ces cas ?
- L'administrateur tape
sudo ./deployer.shsans argument. - La nouvelle image n'existe pas dans le registre (faute de frappe dans la version).
- La nouvelle version démarre, mais
/healthrenvoie une erreur 500 pendant deux minutes.
Voir la réponseMasquer la réponse de l'exercice 3
${1:?…}arrête le script tout de suite avec le message d'usage : rien n'est modifié.- L'export de la base est fait,
.envest modifié, puisdocker compose pulléchoue ; avecset -e, le script s'arrête sans relancer les conteneurs. Les anciens conteneurs tournent toujours, mais.envcontient la mauvaise version : le prochaindocker compose up -déchouerait. Il faut corriger.envà la main. (Amélioration possible : faire lepullavec la nouvelle version avant de modifier.env, par exempleVERSION=$VERSION docker compose pull api front, puisqu'une variable d'environnement du shell est prioritaire sur le fichier.env.) - Les douze essais échouent (60 secondes), le script remet l'ancienne version dans
.env, relancedocker compose up -det se termine avec le code 1. Le service est rétabli sur l'ancienne version ; l'export de la base est disponible si la nouvelle version a modifié des données.
Tu avais trouvé ?
Exercice 4 sur 4 Le registre sans HTTPS Réussi À revoir
Un tutoriel propose de lancer registry:3 sur le port 5000 en HTTP et d'ajouter "insecure-registries": ["192.168.10.179:5000"] dans le daemon.json de chaque serveur. Donne deux risques et l'alternative.
Voir la réponseMasquer la réponse de l'exercice 4
Risques :
- le mot de passe de
docker logincircule en clair sur le réseau : quiconque capture le trafic peut le réutiliser et pousser ses propres images ; - les images peuvent être modifiées en route (interception) : un serveur pourrait exécuter une image piégée en la croyant authentique ;
- sans authentification du tout, n'importe quel poste du réseau peut lire ou remplacer les images.
Alternative : le registre derrière le reverse proxy en HTTPS, avec un certificat de l'autorité interne (que les serveurs reconnaissent déjà) et une authentification (htpasswd, ou Harbor avec des comptes nominatifs et des comptes robots pour la CI).
Tu avais trouvé ?