Polar Code 🎭

Command Palette

Search for a command to run...

05
Pièce N°05

Module 5 : Docker en Production - La Ville Sous Tension

La nuit de l'ouverture. Le système doit tenir. Pas de panne, pas de fuite, pas d'erreur. En production, il n'y a pas de seconde chance. Les conteneurs doivent être solides, surveillés, prêts à tout.

Introduction : La Différence Entre Jouer et Travailler

En développement, un conteneur qui plante, tu le redémarres.
En production, c'est des milliers d'utilisateurs en colère.
Des pertes d'argent. Des réputations qui s'effondrent.

Ce module, c'est le passage à l'âge adulte.
Des jouets aux outils.
Du garage à l'usine.


5.1 Sécurité - Les Verrous de la Ville

Le Conteneur n'est pas une VM

Mythe : "Les conteneurs sont isolés comme des VMs."
Réalité : Ils partagent le noyau. Une faille dans le noyau = tous les conteneurs.

Règles de base :

  1. Jamais root dans le conteneur
  2. Images signées et vérifiées
  3. Réseaux séparés
  4. Limites de ressources strictes

Hardening des Conteneurs

Dockerfile sécurisé :

# Utiliser des images minimales
FROM alpine:3.18  # 5 Mo vs 200 Mo pour Ubuntu

# Utilisateur non-root
RUN addgroup -g 1000 -S appgroup && \
    adduser -u 1000 -S appuser -G appgroup
USER appuser

# Copier avec les bonnes permissions
COPY --chown=appuser:appgroup app /app

# Nettoyer les permissions sensibles
RUN find / -perm /6000 -type f -exec chmod a-s {} \; 2>/dev/null || true

# Signature de l'image (avec Docker Content Trust)
# export DOCKER_CONTENT_TRUST=1

docker-compose.prod.yml :

services:
  api:
    security_opt:
      - no-new-privileges:true  # Empêche l'élévation de privilèges
      - seccomp:unconfined  # Ou un profil custom
    cap_drop:
      - ALL  # Enlève toutes les capabilities
    cap_add:
      - NET_BIND_SERVICE  # Ajoute seulement celle nécessaire
    read_only: true  # Système de fichiers en lecture seule
    tmpfs:
      - /tmp  # Sauf /tmp en RAM

Gestion des Secrets - Les Codes dans le Coffre

Méthodes (du pire au meilleur) :

  1. À éviter absolument :
# Fichier .env commité (NON !)
# Variables dans le Dockerfile (NON !)
# Secrets dans docker-compose.yml (NON !)
  1. Acceptable pour débuter :
# .env non-commité + docker-compose
environment:
  DB_PASSWORD: ${DB_PASSWORD}
# .env.production (GITIGNORE !)
DB_PASSWORD=SuperSecret123!
  1. Professionnel : Docker Secrets (Swarm)
version: '3.8'
services:
  db:
    image: postgres
    secrets:
      - db_password
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password

secrets:
  db_password:
    external: true  # Créé avec docker secret create
  1. Cloud Native : Vault, AWS Secrets Manager, Azure Key Vault
# Container init qui récupère les secrets avant le démarrage
ENTRYPOINT ["./init.sh"]
# init.sh :
#!/bin/sh
# Récupère les secrets depuis Vault
# Les écrit dans des fichiers
# Passe la main à l'application

Scanner les Images - Le Détecteur de Bombes

# Trivy (gratuit, open-source)
trivy image mon_image:latest

# Output :
# ✗ HIGH: Vulnerable package found
#   - CVE-2023-12345 in libssl
#   - Fix version: 1.1.1k

# Dans le CI/CD
trivy image --exit-code 1 --severity HIGH,CRITICAL mon_image:latest
# Si vulnérabilité critique, le build échoue

# Docker Scout (intégré à Docker)
docker scout quickview mon_image:latest
docker scout cves mon_image:latest

# Snyk, Aqua Security, Clair...

5.2 Monitoring et Logging - Les Yeux et les Oreilles

Métriques à Surveiller

Au niveau conteneur :

  • CPU usage (%)
  • Memory usage (absolu et %)
  • Network I/O
  • Disk I/O
  • Restart count

Au niveau application :

  • Latence (p95, p99)
  • Error rate
  • Throughput (req/s)
  • Business metrics (commandes/s, etc.)

Stack de Monitoring Prometheus/Grafana

docker-compose.monitoring.yml :

version: '3.8'

services:
  # Collecteur de métriques
  prometheus:
    image: prom/prometheus:latest
    container_name: prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.retention.time=30d'
      - '--web.enable-lifecycle'
    volumes:
      - ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - prometheus_data:/prometheus
    networks:
      - monitoring
    ports:
      - "9090:9090"
    restart: unless-stopped

  # Visualisation
  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    environment:
      GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD:-ChangeMe!}
      GF_INSTALL_PLUGINS: grafana-piechart-panel
    volumes:
      - grafana_data:/var/lib/grafana
      - ./monitoring/grafana/dashboards:/etc/grafana/provisioning/dashboards:ro
      - ./monitoring/grafana/datasources:/etc/grafana/provisioning/datasources:ro
    networks:
      - monitoring
    ports:
      - "3000:3000"
    restart: unless-stopped
    depends_on:
      - prometheus

  # Node Exporter (métriques système hôte)
  node-exporter:
    image: prom/node-exporter:latest
    container_name: node-exporter
    volumes:
      - /proc:/host/proc:ro
      - /sys:/host/sys:ro
      - /:/rootfs:ro
    command:
      - '--path.procfs=/host/proc'
      - '--path.sysfs=/host/sys'
      - '--path.rootfs=/rootfs'
    networks:
      - monitoring
    ports:
      - "9100:9100"
    restart: unless-stopped
    pid: "host"  # Accès au PID namespace hôte

  # cAdvisor (métriques conteneurs)
  cadvisor:
    image: gcr.io/cadvisor/cadvisor:latest
    container_name: cadvisor
    volumes:
      - /:/rootfs:ro
      - /var/run:/var/run:ro
      - /sys:/sys:ro
      - /var/lib/docker/:/var/lib/docker:ro
      - /dev/disk/:/dev/disk:ro
    devices:
      - /dev/kmsg
    networks:
      - monitoring
    ports:
      - "8080:8080"
    restart: unless-stopped
    privileged: true  # Nécessaire pour accéder aux métriques Docker

  # Alert Manager
  alertmanager:
    image: prom/alertmanager:latest
    container_name: alertmanager
    volumes:
      - ./monitoring/alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro
      - alertmanager_data:/alertmanager
    networks:
      - monitoring
    ports:
      - "9093:9093"
    restart: unless-stopped

  # Loki (logs)
  loki:
    image: grafana/loki:latest
    container_name: loki
    command: -config.file=/etc/loki/local-config.yaml
    volumes:
      - loki_data:/loki
    networks:
      - monitoring
    ports:
      - "3100:3100"
    restart: unless-stopped

  # Promtail (collecte logs)
  promtail:
    image: grafana/promtail:latest
    container_name: promtail
    volumes:
      - /var/log:/var/log:ro
      - /var/lib/docker/containers:/var/lib/docker/containers:ro
      - ./monitoring/promtail.yml:/etc/promtail/config.yml:ro
    networks:
      - monitoring
    restart: unless-stopped
    command: -config.file=/etc/promtail/config.yml

networks:
  monitoring:
    driver: bridge

volumes:
  prometheus_data:
    driver: local
  grafana_data:
    driver: local
  alertmanager_data:
    driver: local
  loki_data:
    driver: local

Configuration Prometheus :

# monitoring/prometheus.yml
global:
  scrape_interval: 15s
  evaluation_interval: 15s

alerting:
  alertmanagers:
    - static_configs:
        - targets:
          - alertmanager:9093

rule_files:
  - "alerts.yml"

scrape_configs:
  - job_name: 'prometheus'
    static_configs:
      - targets: ['localhost:9090']

  - job_name: 'node'
    static_configs:
      - targets: ['node-exporter:9100']

  - job_name: 'cadvisor'
    static_configs:
      - targets: ['cadvisor:8080']
    metrics_path: /metrics

  - job_name: 'docker'
    static_configs:
      - targets: ['cadvisor:8080']
    metrics_path: /metrics/docker

  - job_name: 'app'
    static_configs:
      - targets: ['api:3000']
    metrics_path: /metrics

Alertes :

# monitoring/alerts.yml
groups:
  - name: infrastructure
    rules:
      - alert: HighMemoryUsage
        expr: (1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100 > 85
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "High memory usage on {{ $labels.instance }}"
          description: "Memory usage is at {{ $value }}%"

      - alert: ContainerRestarted
        expr: increases(container_restarts_total[5m]) > 0
        labels:
          severity: critical
        annotations:
          summary: "Container {{ $labels.name }} restarted"
          
      - alert: HighCPUUsage
        expr: rate(container_cpu_usage_seconds_total[5m]) * 100 > 80
        for: 5m
        labels:
          severity: warning

Centralisation des Logs

Configuration Docker pour les logs :

// /etc/docker/daemon.json
{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  },
  "labels": ["environment=production"]
}

Ou mieux, directement vers un agrégateur :

{
  "log-driver": "loki",
  "log-opts": {
    "loki-url": "http://loki:3100/api/prom/push",
    "loki-external-labels": "job=docker"
  }
}

Dans docker-compose :

services:
  api:
    logging:
      driver: "loki"
      options:
        loki-url: "http://loki:3100/api/prom/push"
        loki-external-labels: "service=api"

5.3 Orchestration - Quand Docker Compose ne Suffit Plus

Les Limites de Docker Compose

Docker Compose, c'est pour :

  • Une seule machine
  • Développement
  • Petits déploiements

Dès que vous avez besoin de :

  • Haute disponibilité
  • Scaling horizontal
  • Rolling updates
  • Multi-nœuds
  • Auto-healing

... il faut un orchestrateur.

Docker Swarm vs Kubernetes

Docker Swarm : Le petit frère intégré.

# Initialiser le swarm
docker swarm init --advertise-addr <IP>

# Joindre un nœud
docker swarm join --token <TOKEN> <IP>:2377

# Déployer un stack
docker stack deploy -c docker-compose.prod.yml mon_app

# Voir les services
docker service ls

Avantages Swarm :

  • Simple, intégré à Docker
  • Courbe d'apprentissage douce
  • Bon pour les petites/moyennes infrastructures

Inconvénients :

  • Communauté plus petite
  • Moins de fonctionnalités
  • Pas le standard du marché

Kubernetes : Le géant.

# kubectl apply -f deployment.yml
# kubectl scale deployment api --replicas=5
# kubectl rolling-update...

Avantages Kubernetes :

  • Standard de facto
  • Énorme écosystème
  • Très puissant
  • Cloud-agnostic

Inconvénients :

  • Complexe
  • Nécessite une équipe dédiée (ou managed service)
  • Overkill pour les petites apps

Exemple de Stack Swarm pour Production

docker-compose.swarm.yml :

version: '3.8'

services:
  # Application avec réplicas
  api:
    image: registry.monentreprise.com/api:${TAG:-latest}
    deploy:
      mode: replicated
      replicas: 3
      update_config:
        parallelism: 1
        delay: 10s
        order: start-first
      rollback_config:
        parallelism: 0
        order: stop-first
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 3
        window: 120s
      placement:
        constraints:
          - node.role == worker
      resources:
        limits:
          cpus: '0.5'
          memory: 512M
        reservations:
          cpus: '0.25'
          memory: 256M
    networks:
      - backend
    secrets:
      - db_password
    configs:
      - source: app_config
        target: /app/config.yaml

  # Load balancer
  traefik:
    image: traefik:v2.10
    deploy:
      mode: global
      placement:
        constraints:
          - node.role == manager
    ports:
      - "80:80"
      - "443:443"
      - "8080:8080"  # Dashboard
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./traefik:/etc/traefik
    networks:
      - backend
      - frontend
    command:
      - "--api.insecure=true"
      - "--providers.docker=true"
      - "--providers.docker.swarmmode=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.web.address=:80"

secrets:
  db_password:
    external: true

configs:
  app_config:
    file: ./config/prod.yaml

networks:
  backend:
    driver: overlay
    attachable: true
  frontend:
    driver: overlay
    attachable: true

5.4 CI/CD avec Docker - L'Usine à Conteneurs

Pipeline GitLab CI

.gitlab-ci.yml :

stages:
  - test
  - build
  - security
  - deploy

variables:
  DOCKER_REGISTRY: registry.gitlab.com
  IMAGE_NAME: ${CI_REGISTRY_IMAGE}
  DOCKER_HOST: tcp://docker:2375
  DOCKER_TLS_CERTDIR: ""

# Services nécessaires
services:
  - docker:dind

before_script:
  - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY

# 1. Tests unitaires
unit-tests:
  stage: test
  image: node:18-alpine
  script:
    - cd backend
    - npm ci
    - npm test
  artifacts:
    reports:
      junit: backend/junit.xml
    paths:
      - backend/coverage/

# 2. Build de l'image
build-image:
  stage: build
  image: docker:latest
  script:
    - |
      docker build \
        --build-arg VERSION=${CI_COMMIT_SHA} \
        --build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
        -t ${IMAGE_NAME}:${CI_COMMIT_SHA} \
        -t ${IMAGE_NAME}:latest \
        .
    - docker push ${IMAGE_NAME}:${CI_COMMIT_SHA}
    - docker push ${IMAGE_NAME}:latest
  only:
    - main
    - develop

# 3. Scan de sécurité
security-scan:
  stage: security
  image: aquasec/trivy:latest
  script:
    - trivy image --exit-code 1 --severity HIGH,CRITICAL ${IMAGE_NAME}:${CI_COMMIT_SHA}
  allow_failure: false
  only:
    - main

# 4. Tests d'intégration
integration-tests:
  stage: test
  image: docker:latest
  services:
    - docker:dind
  script:
    - docker-compose -f docker-compose.test.yml up -d
    - sleep 10  # Attendre que les services démarrent
    - docker-compose -f docker-compose.test.yml exec api npm run test:integration
    - docker-compose -f docker-compose.test.yml down
  only:
    - main
    - develop

# 5. Déploiement staging
deploy-staging:
  stage: deploy
  image: docker:latest
  script:
    - docker stack deploy -c docker-compose.staging.yml --with-registry-auth mon_app_staging
  environment:
    name: staging
    url: https://staging.monapp.com
  only:
    - develop

# 6. Déploiement production
deploy-production:
  stage: deploy
  image: docker:latest
  script:
    - |
      docker service update \
        --image ${IMAGE_NAME}:${CI_COMMIT_SHA} \
        mon_app_production_api
  environment:
    name: production
    url: https://monapp.com
  when: manual  # Déclenchement manuel
  only:
    - main

GitHub Actions

.github/workflows/docker.yml :

name: Docker Build and Deploy

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2
      
      - name: Log in to Container Registry
        uses: docker/login-action@v2
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      
      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v4
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
      
      - name: Build and push
        uses: docker/build-push-action@v4
        with:
          context: .
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
          
      - name: Scan with Trivy
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest'
          format: 'sarif'
          output: 'trivy-results.sarif'
          
      - name: Upload Trivy scan results
        uses: github/codeql-action/upload-sarif@v2
        if: always()
        with:
          sarif_file: 'trivy-results.sarif'

5.5 Bonnes Pratiques Production - Le Code de Survie

1. Tagging Stratégique

# MAUVAIS
docker build -t monapp .

# BON
docker build \
  -t registry.com/monapp:1.2.3 \
  -t registry.com/monapp:1.2 \
  -t registry.com/monapp:1 \
  -t registry.com/monapp:latest \
  .

# MEILLEUR (avec Git commit SHA)
docker build \
  -t registry.com/monapp:$(git rev-parse --short HEAD) \
  -t registry.com/monapp:latest \
  .

2. Healthchecks Intelligents

# Dans le Dockerfile
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:8080/health || exit 1

# Endpoint /health qui vérifie :
# - Connexion DB
# - Connexion Redis
# - Espace disque
# - Mémoire disponible

3. Graceful Shutdown

# Capturer SIGTERM
STOPSIGNAL SIGTERM

# Script d'arrêt
COPY docker-entrypoint.sh /
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["app"]

docker-entrypoint.sh :

#!/bin/sh
set -e

# Fonction de cleanup
cleanup() {
    echo "Received SIGTERM, shutting down..."
    # Fermer les connexions DB, sauvegarder l'état, etc.
    kill -TERM "$PID" 2>/dev/null
    wait "$PID"
    exit 0
}

# Capturer les signaux
trap cleanup TERM INT

# Démarrer l'application en arrière-plan
exec "$@" &
PID=$!

# Attendre
wait $PID

4. Configuration Externalisée

# docker-compose.prod.yml
services:
  api:
    configs:
      - source: app_config_${ENVIRONMENT}
        target: /app/config.yaml

configs:
  app_config_production:
    external: true
  app_config_staging:
    external: true

5. Backup des Volumes

Script de backup :

#!/bin/bash
# backup-volumes.sh

BACKUP_DIR="/backups"
DATE=$(date +%Y%m%d_%H%M%S)

# Backup PostgreSQL
docker run --rm \
  -v postgres_data:/volume \
  -v $BACKUP_DIR:/backup \
  alpine \
  tar czf /backup/postgres_${DATE}.tar.gz -C /volume ./

# Backup Redis
docker run --rm \
  -v redis_data:/volume \
  -v $BACKUP_DIR:/backup \
  alpine \
  tar czf /backup/redis_${DATE}.tar.gz -C /volume ./

# Rotation (garder 7 jours)
find $BACKUP_DIR -name "*.tar.gz" -mtime +7 -delete

6. Mise à Jour sans Downtime

Blue-Green Deployment :

# Version A (current)
docker service update --image monapp:1.2.3 api-green

# Tester
curl https://api-green.monapp.com/health

# Basculer le routeur
docker service update \
  --update-delay 10s \
  --update-parallelism 1 \
  --update-failure-action rollback \
  --image monapp:1.2.3 api-blue

5.6 Checklist de Mise en Production

Avant le Déploiement

  • Images scannées pour vulnérabilités
  • Secrets gérés proprement (pas dans les images)
  • Healthchecks configurés
  • Logging centralisé configuré
  • Monitoring en place
  • Alertes configurées
  • Backup des volumes testé
  • Plan de rollback préparé
  • Load testing effectué
  • Documentation à jour

Après le Déploiement

  • Vérifier les métriques (CPU, mémoire, erreurs)
  • Vérifier les logs (pas d'erreurs au démarrage)
  • Tester les endpoints critiques
  • Vérifier les connexions aux services externes
  • Confirmer que le monitoring collecte bien
  • Documenter les incidents/actions

En Cas d'Incident

  1. Ne pas paniquer
  2. Consulter les logs centralisés
  3. Vérifier les métriques (saturation ?)
  4. Rollback si nécessaire
  5. Documenter l'incident
  6. Corriger et redéployer

Scène Finale : La Nuit de Garde

# 22h00 - Tout est calme
watch -n 5 'docker service ls && echo "---" && docker stats --no-stream'

# 23h30 - Pic de charge
# Les métriques montent
# AlertManager envoie une alerte Slack

# Investigation
docker service ps api --no-trunc  # Voir où tournent les tâches
docker logs api.1.qwerty123 --tail 100  # Logs d'un réplica spécifique
docker exec api.1.qwerty123 top  # Processus dans le conteneur

# Action
docker service scale api=8  # Scaling horizontal
# Ou
docker service update --limit-cpu 1.0 api  # Donner plus de CPU

# 00h30 - Retour à la normale
# Les métriques redescendent
# Ticket d'incident créé
# Post-mortem planifié pour demain

Conclusion du Module

La production, c'est où les théories rencontrent la réalité.
Où les "ça devrait marcher" deviennent des "ça marche, ou ça casse".

Les règles de la production :

  1. Tout est surveillé - Si tu ne le mesures pas, tu ne peux pas le gérer.
  2. Tout est loggé - Les logs sont ton seul témoin quand tout plante.
  3. Tout est automatique - Les déploiements manuels sont des erreurs en puissance.
  4. Tout est reproductible - Un environnement, un script, un résultat.
  5. Tout est prévu - Plan de rollback, backup, alertes.

Docker en production, ce n'est pas la fin du voyage.
C'est le début de la vraie aventure.
La ville est en ligne.
Elle respire.
Elle sert des milliers d'utilisateurs.

Et toi, tu es son gardien.