TP11 : Docker en production — Healthchecks, Secrets & Résilience
Objectif du TP
- Distinguer un conteneur « qui tourne » d'un conteneur « qui fonctionne » grâce aux healthchecks.
- Configurer des stratégies de déploiement avancées avec rollback automatique en Swarm.
- Gérer les secrets de manière sécurisée avec Docker Secrets.
- Sauvegarder et restaurer des volumes Docker.
- Générer une SBOM et intégrer les scans de sécurité dans un pipeline CI/CD.
Rendu attendu
- Notes dans votre carnet répondant aux questions posées.
- Stack Swarm avec healthcheck,
update_configet rollback configurés. - Commandes de backup/restore d'un volume documentées.
- Une SBOM générée depuis votre image (fichier
sbom.json).
Questions d'ouverture
- Qu'est-ce qui différencie un conteneur « Running » d'un conteneur « Healthy » ?
- Où stocker un mot de passe de base de données dans une architecture Docker en production ?
- Que faut-il sauvegarder dans une architecture Docker pour pouvoir la reconstruire entièrement ?
Focus : La résilience en production
Un conteneur Running n'est pas forcément sain. Il peut tourner mais renvoyer des erreurs 500, avoir une connexion DB cassée ou être bloqué dans une boucle infinie. Les healthchecks permettent à Docker de distinguer « qui tourne » de « qui fonctionne », et de déclencher des actions automatiques en conséquence (restart, rollback).
Mise en pratique
1. Healthchecks
Un healthcheck est une commande que Docker exécute périodiquement à l'intérieur du conteneur pour vérifier qu'il est opérationnel. Si la commande échoue trop souvent, le conteneur passe à l'état unhealthy.
1.1. Dans un Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --only=production
COPY . .
HEALTHCHECK --interval=10s --timeout=3s --retries=3 --start-period=15s \
CMD wget -qO- http://localhost:3000/ || exit 1
CMD ["node", "app.js"]
| Paramètre | Description |
|---|---|
--interval |
Délai entre deux vérifications (défaut : 30s) |
--timeout |
Durée max d'exécution de la commande avant échec |
--retries |
Nombre d'échecs consécutifs avant de passer en unhealthy |
--start-period |
Délai de grâce au démarrage (le conteneur ne compte pas les échecs pendant ce temps) |
Vérifier le statut d'un conteneur :
# La colonne STATUS affiche "(healthy)" ou "(unhealthy)"
docker ps
# Voir le détail du healthcheck
docker inspect --format='{{json .State.Health}}' <container> | jq
1.2. Dans docker-compose.yml
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: secret
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 3s
retries: 5
app:
build: .
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000/"]
interval: 10s
timeout: 3s
retries: 3
start_period: 15s
depends_on:
db:
condition: service_healthy # Attend réellement que db soit opérationnelle
Le
condition: service_healthyrésout le problème évoqué dans le TP06 : on attend maintenant que le service soit réellement prêt, pas juste démarré.Question : Pourquoi
start_periodest-il important pour une application Node.js qui doit se connecter à une base de données avant de démarrer ?
1.3. Dans Docker Swarm
En Swarm, le healthcheck est utilisé par le scheduler pour valider qu'un réplica est sain avant de continuer le rolling update. Associé à failure_action: rollback, il garantit qu'une version cassée n'est jamais mise en production.
services:
web:
image: registry.local/my-app:v2
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000/"]
interval: 10s
timeout: 3s
retries: 3
start_period: 15s
deploy:
replicas: 3
update_config:
parallelism: 1 # Met à jour 1 réplica à la fois
delay: 15s # Délai entre chaque réplica
order: start-first # Démarre le nouveau avant de stopper l'ancien
failure_action: rollback # Rollback automatique si unhealthy
rollback_config:
parallelism: 1
order: stop-first
Question : Quelle différence entre
order: start-firstetorder: stop-firsten termes de disponibilité et de consommation de ressources ?
2. Rollback et stratégies de déploiement
2.1. Rolling update avec rollback automatique
Reprenez la stack du TP08. Vous allez simuler un déploiement raté.
- Construire une image volontairement cassée :
FROM node:20-alpine
WORKDIR /app
# Simule une application qui crash au démarrage
CMD ["sh", "-c", "echo 'Démarrage...' && sleep 3 && exit 1"]
- Déployer la version cassée avec failure_action rollback :
docker service update \
--image <REGISTRY_URL>/mon-image:broken \
--update-parallelism 1 \
--update-delay 10s \
--update-failure-action rollback \
tp8stack_web
- Observer le comportement :
# Regarder les tâches en temps réel
watch docker service ps tp8stack_web
# Voir les logs du service
docker service logs tp8stack_web
Vous devriez voir les tâches passer par Starting → Running → Failed, puis le rollback s'enclencher automatiquement.
- Rollback manuel (si vous n'avez pas configuré
failure_action: rollback) :
Question : Quelle différence entre une image qui « build correctement » et une image réellement « déployable » ? Comment le healthcheck et le rollback répondent-ils à cette question ?
2.2. Stratégie Blue-Green
La stratégie blue-green consiste à avoir deux environnements identiques tournant en parallèle. On bascule le trafic vers la nouvelle version (green) uniquement quand elle est validée. En cas de problème, retour immédiat vers l'ancienne (blue).
Exemple avec Docker Compose :
services:
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf
blue:
image: registry.local/my-app:v1
green:
image: registry.local/my-app:v2
# nginx.conf - pointer vers "green" pour basculer
upstream backend {
server blue:3000;
}
server {
listen 80;
location / {
proxy_pass http://backend;
}
}
Procédure de bascule :
# 1. Tester green sans l'exposer
docker compose exec green wget -qO- http://localhost:3000
# 2. Modifier nginx.conf : remplacer "blue:3000" par "green:3000"
# 3. Recharger Nginx sans coupure
docker compose exec nginx nginx -s reload
# 4. En cas de problème, revenir à blue dans nginx.conf et recharger
| Stratégie | Temps de bascule | Ressources | Risque |
|---|---|---|---|
| Rolling update | Progressif | Normal | Faible |
| Blue-Green | Instantané | x2 | Très faible |
| Canary | Progressif (% trafic) | Normal | Faible |
Question : Dans quel cas préféreriez-vous une stratégie blue-green plutôt qu'un rolling update ? Donnez un exemple concret.
3. Docker Secrets en Swarm
Les variables d'environnement et les fichiers .env présentent un risque : leur valeur est visible via docker inspect, dans les logs, et parfois dans l'historique du shell. Docker Secrets résout ce problème en Swarm en chiffrant les secrets dans le Raft store du cluster.
3.1. Créer et gérer des secrets
# Créer depuis stdin (aucune trace dans l'historique)
echo "super_password_secret" | docker secret create db_password -
# Créer depuis un fichier
docker secret create tls_cert ./certs/server.crt
# Lister les secrets
docker secret ls
# Inspecter (la valeur n'est JAMAIS affichée)
docker secret inspect db_password
# Supprimer
docker secret rm db_password
Les secrets sont chiffrés dans le Raft store du Swarm. Impossible de lire la valeur après création, même en tant qu'admin.
3.2. Utiliser dans une stack Swarm
# docker-compose.yml (mode Swarm)
services:
app:
image: registry.local/my-app:v1
secrets:
- db_password
- api_key
environment:
# Convention : pointer vers le fichier du secret
- DB_PASSWORD_FILE=/run/secrets/db_password
secrets:
db_password:
external: true # Le secret existe déjà dans Swarm
api_key:
external: true
Les secrets sont montés en tant que fichiers dans /run/secrets/ à l'intérieur du conteneur. Dans l'application Node.js :
const fs = require('fs');
// Lire le secret depuis le fichier monté par Swarm
const dbPassword = fs.readFileSync('/run/secrets/db_password', 'utf8').trim();
// Le secret n'est jamais exposé dans les logs, les variables d'env ou docker inspect
3.3. Comparaison des méthodes
| Méthode | Visible via | Swarm | Recommandé |
|---|---|---|---|
| Valeur en dur dans le code | git log |
❌ | ❌ Jamais |
| Variable d'environnement | docker inspect |
✅ | ⚠️ Config non-sensible |
Fichier .env |
Système de fichiers | ❌ | ⚠️ Dev uniquement |
| Docker Secret | Chiffré (Raft) | ✅ Swarm uniquement | ✅ |
| Vault / Secrets Manager | Chiffré (externe) | ✅ | ✅✅ Entreprise |
Question : Pourquoi
docker inspectpeut-il révéler des variables d'environnement sensibles ? Testez-le avec une variable d'environnement normale et observez la sortie.
4. Sauvegarde et restauration de volumes
Les images Docker peuvent être reconstruites ou repullées depuis un registry. Ce qui est irremplaçable, ce sont les données dans les volumes (base de données, uploads utilisateurs, certificats...).
4.1. Qu'est-ce qu'il faut sauvegarder ?
| Élément | Perte si non sauvegardé | Solution |
|---|---|---|
| Volumes (DB, uploads...) | ✅ Données définitivement perdues | Backup régulier |
docker-compose.yml |
✅ Configuration perdue | Git |
.env / secrets |
✅ Configuration sensible perdue | Coffre-fort / vault |
| Images | ❌ Rebuildables ou repullables | Registry |
| Conteneurs | ❌ Éphémères par nature | Pas nécessaire |
4.2. Backup d'un volume
# Créer le dossier de backup
mkdir -p backups
# Sauvegarder le volume "mysql_data"
docker run --rm \
-v mysql_data:/data \
-v $(pwd)/backups:/backup \
alpine \
tar czf /backup/mysql_data_$(date +%Y%m%d_%H%M%S).tar.gz -C /data .
Décomposé :
- --rm : supprime le conteneur temporaire après exécution
- -v mysql_data:/data : monte le volume source en lecture
- -v $(pwd)/backups:/backup : monte le dossier local de destination
- alpine tar czf ... : crée une archive compressée horodatée
4.3. Restauration d'un volume
# Créer un nouveau volume vide
docker volume create mysql_data_restore
# Restaurer l'archive dans le nouveau volume
docker run --rm \
-v mysql_data_restore:/data \
-v $(pwd)/backups:/backup \
alpine \
sh -c "tar xzf /backup/mysql_data_20240101_120000.tar.gz -C /data"
4.4. Exercice complet : backup et disaster recovery
- Lancez une stack avec PostgreSQL et insérez quelques données :
docker compose up -d
docker compose exec db psql -U postgres -c "CREATE TABLE test (id serial, msg text);"
docker compose exec db psql -U postgres -c "INSERT INTO test (msg) VALUES ('Donnée importante');"
-
Sauvegardez le volume.
-
Simulez un incident :
-
Restaurez le volume et relancez la stack.
-
Vérifiez que les données sont présentes :
Question : Pourquoi un
docker commit(qui transforme un conteneur en image) n'est-il pas une bonne stratégie de sauvegarde pour les données d'une base de données ?
5. SBOM et pipeline CI/CD
5.1. Qu'est-ce qu'une SBOM ?
Une SBOM (Software Bill of Materials) est l'inventaire exhaustif de tous les composants d'un logiciel : dépendances, bibliothèques, versions exactes, licences. En cas de vulnérabilité découverte (CVE), vous savez immédiatement quelles images sont affectées sans avoir à les rescanner.
5.2. Générer une SBOM avec Trivy
# Scanner et afficher en tableau (vu en TP09)
trivy image my-app:v1
# Générer une SBOM au format CycloneDX (standard industriel)
trivy image --format cyclonedx --output sbom.json my-app:v1
# Scanner le code source du projet
trivy fs .
# Filtrer uniquement les CVE critiques et hautes
trivy image --severity HIGH,CRITICAL my-app:v1
# Faire échouer le build si des CVE critiques sont trouvées (utile en CI)
trivy image --exit-code 1 --severity CRITICAL my-app:v1
Questions :
- Qu'est-ce qu'une CVE ? Cherchez un exemple récent.
- Pourquoi
:latestest-il un mauvais tag en production ? - Quelle différence entre scanner le code source (
trivy fs .) et scanner l'image finale (trivy image) ? - Quelle différence entre un tag
:latest,:1.2.3et:git-abc1234?
5.3. Pipeline GitHub Actions
# .github/workflows/docker.yml
name: Docker CI
on:
push:
pull_request:
jobs:
build-test-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t my-app:${{ github.sha }} .
- name: Run tests
run: docker compose run --rm test
- name: Scan image (Trivy)
uses: aquasecurity/trivy-action@master
with:
image-ref: my-app:${{ github.sha }}
format: table
exit-code: '1'
severity: HIGH,CRITICAL
- name: Generate SBOM
run: trivy image --format cyclonedx --output sbom.json my-app:${{ github.sha }}
- name: Push to registry
if: github.ref == 'refs/heads/main'
run: |
docker tag my-app:${{ github.sha }} registry/my-app:${{ github.sha }}
docker tag my-app:${{ github.sha }} registry/my-app:latest
docker push registry/my-app:${{ github.sha }}
docker push registry/my-app:latest
Ce pipeline garantit :
- Les tests passent avant toute publication.
- Aucune image avec une CVE critique n'est publiée.
- Chaque image est taguée avec son SHA Git (traçabilité).
- Une SBOM est générée à chaque build.
Question : Quel est l'
exit-coderetourné par Docker (et utilisé par la CI) quand une commande échoue ? Comment cela permet-il à GitHub Actions d'arrêter le pipeline ?
Conclusion
- Un healthcheck transforme un conteneur « qui tourne » en un conteneur « qui fonctionne » : Docker peut alors prendre des décisions automatiques (restart, rollback).
- Le
failure_action: rollbacken Swarm, couplé aux healthchecks, garantit qu'une version cassée ne remplace jamais une version stable. - Docker Secrets chiffre les informations sensibles dans le Raft store du Swarm : elles ne sont jamais visibles via
docker inspectou les logs. - Les données dans les volumes sont le seul élément véritablement irremplaçable d'une architecture Docker : sauvegardes régulières obligatoires.
- Une SBOM permet d'inventorier les composants d'une image et d'être réactif en cas de nouvelle CVE publiée.