Skip to content

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_config et 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_healthy ré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_period est-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-first et order: stop-first en 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é.

  1. 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"]
docker build -t <REGISTRY_URL>/mon-image:broken .
docker push <REGISTRY_URL>/mon-image:broken
  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
  1. 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 StartingRunningFailed, puis le rollback s'enclencher automatiquement.

  1. Rollback manuel (si vous n'avez pas configuré failure_action: rollback) :
docker service rollback tp8stack_web

# Vérifier
docker service ps tp8stack_web

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).

    Utilisateurs
         |
   [Nginx proxy]
    /           \
[blue:v1]    [green:v2]  ← testée mais pas encore exposée

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 inspect peut-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

  1. 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');"
  1. Sauvegardez le volume.

  2. Simulez un incident :

docker compose down -v  # Supprime les conteneurs ET les volumes
  1. Restaurez le volume et relancez la stack.

  2. Vérifiez que les données sont présentes :

docker compose exec db psql -U postgres -c "SELECT * FROM test;"

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 :latest est-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.3 et :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 :

  1. Les tests passent avant toute publication.
  2. Aucune image avec une CVE critique n'est publiée.
  3. Chaque image est taguée avec son SHA Git (traçabilité).
  4. Une SBOM est générée à chaque build.

Question : Quel est l'exit-code retourné 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: rollback en 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 inspect ou 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.