================================================================================
[OK] DOCKER EN ÉQUIPE DE 3 DÉVELOPPEURS - APPLICATION FLASK (GUIDE ULTRA-DÉTAILLÉ)
================================================================================

Ce guide couvre un cas réel et concret: la même équipe de 3 développeurs Python
qui travaillent sur l'application Flask "TaskManager API" et utilisent Docker
pour conteneuriser, standardiser et déployer leur application collectivement.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
QU'EST-CE QUE DOCKER ET POURQUOI L'UTILISER EN ÉQUIPE?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  PROBLÈME SANS DOCKER (situation réelle de l'équipe avant):
  ──────────────────────────────────────────────────────────
  Alice  -> Python 3.11, PostgreSQL 15, macOS
  Bob    -> Python 3.10, PostgreSQL 14, Ubuntu 22.04
  Claire -> Python 3.9,  PostgreSQL 13, Windows 11

  Résultat:
  - "Ça marche chez moi mais pas chez toi"  <- problème classique
  - Bob passe 2h à reproduire un bug d'Alice qu'il n'arrive pas à voir
  - En production: Python 3.8 + PostgreSQL 12 -> comportements inattendus
  - Nouveau développeur = 1 journée d'installation avant de pouvoir coder

  SOLUTION AVEC DOCKER:
  ─────────────────────
  Alice, Bob et Claire ont TOUS:
  -> Python 3.11.8 (exactement)
  -> PostgreSQL 15.4 (exactement)
  -> Même OS (Alpine Linux dans le conteneur)
  -> Application lancée en 1 commande: docker-compose up

  CONCEPTS CLÉS (rappel):
  ───────────────────────
  Image Docker     -> Recette immuable (comme une classe en POO)
                     Ex: "Python 3.11 + nos dépendances + notre code"

  Conteneur Docker -> Instance d'une image (comme un objet instancié)
                     Ex: Notre appli Flask qui tourne réellement

  Dockerfile       -> Fichier texte qui décrit comment construire l'image
                     Ex: "Pars de Python 3.11, copie le code, lance Flask"

  docker-compose   -> Outil pour orchestrer plusieurs conteneurs ensemble
                     Ex: Flask + PostgreSQL + Redis qui démarrent ensemble

  Volume           -> Dossier partagé entre le conteneur et la machine hôte
                     Ex: La base de données survit même si le conteneur s'arrête

  Registry         -> Dépôt d'images Docker (comme GitHub mais pour les images)
                     Ex: Docker Hub, GitHub Container Registry

  Flux de travail Docker en équipe:
  ──────────────────────────────────
  ┌───────────────────────────────────────────────────────────────────────┐
  │                                                                       │
  │   Alice écrit le Dockerfile et docker-compose.yml                    │
  │         │                                                             │
  │         [BLACK_DOWN-POINTING_TRIANGLE]                                                             │
  │   git push -> Bob et Claire font git pull                             │
  │         │                                                             │
  │         [BLACK_DOWN-POINTING_TRIANGLE]                                                             │
  │   docker-compose up  <-  1 seule commande pour tout lancer            │
  │         │                                                             │
  │         [BLACK_DOWN-POINTING_TRIANGLE]                                                             │
  │   ┌─────────────┐   ┌──────────────┐   ┌──────────────┐            │
  │   │  Flask API  │   │  PostgreSQL  │   │    Redis     │            │
  │   │  :8000      │──[BLACK_RIGHT-POINTING_TRIANGLE]│  :5432       │   │  :6379       │            │
  │   │ (conteneur) │   │ (conteneur)  │   │ (conteneur)  │            │
  │   └─────────────┘   └──────────────┘   └──────────────┘            │
  │                                                                       │
  └───────────────────────────────────────────────────────────────────────┘

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PRÉSENTATION DE L'ÉQUIPE ET DU PROJET
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

L'équipe (identique aux guides SonarQube et ELK):
  - Alice  -> Lead développeuse (aussi responsable Docker/DevOps)
  - Bob    -> Développeur backend
  - Claire -> Développeuse backend + tests

Le projet:
  - Nom: "TaskManager API"
  - Stack: Flask + SQLAlchemy + PostgreSQL + Redis (cache)
  - Build: Docker + Docker Compose
  - Versionning: Git + GitHub
  - Registry: GitHub Container Registry (ghcr.io)

Structure du projet mise à jour:
  taskmanager/
  ├── app/
  │   ├── __init__.py
  │   ├── models.py
  │   ├── routes.py
  │   ├── utils.py
  │   └── logger.py
  ├── tests/
  │   ├── conftest.py
  │   ├── test_routes.py
  │   └── test_models.py
  ├── docker/                           <- NOUVEAU
  │   ├── Dockerfile                    <- NOUVEAU: image de l'application
  │   ├── Dockerfile.dev                <- NOUVEAU: image de développement
  │   └── Dockerfile.test               <- NOUVEAU: image pour les tests
  ├── nginx/                            <- NOUVEAU
  │   └── nginx.conf                    <- NOUVEAU: reverse proxy
  ├── scripts/                          <- NOUVEAU
  │   ├── entrypoint.sh                 <- NOUVEAU: script de démarrage
  │   ├── wait-for-it.sh                <- NOUVEAU: attendre la DB
  │   └── healthcheck.sh                <- NOUVEAU: vérification santé
  ├── docker-compose.yml                <- NOUVEAU: environnement de dev
  ├── docker-compose.prod.yml           <- NOUVEAU: environnement de prod
  ├── docker-compose.test.yml           <- NOUVEAU: environnement de test
  ├── .dockerignore                     <- NOUVEAU: fichiers exclus
  ├── requirements.txt
  ├── requirements-dev.txt
  ├── config.py
  ├── run.py
  ├── .env.example                      <- NOUVEAU: template des variables
  ├── .env                              <- (gitignore) variables locales
  └── sonar-project.properties


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PARTIE 1 - MISE EN PLACE PAR ALICE (LEAD DEV / RESPONSABLE DOCKER)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Alice est chargée de tout configurer Docker pour l'équipe.
Bob et Claire n'ont qu'à suivre la PARTIE 2 ensuite.

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.1 - Alice installe Docker sur son poste
────────────────────────────────────────────────────────────────────────────────

  INSTALLATION (Mac avec Homebrew):
  ─────────────────────────────────
  brew install --cask docker

  # Puis lancer Docker Desktop depuis les Applications

  INSTALLATION (Ubuntu/Debian):
  ──────────────────────────────
  # Méthode officielle recommandée:
  curl -fsSL https://get.docker.com -o get-docker.sh
  sudo sh get-docker.sh

  # Ajouter son utilisateur au groupe docker (évite sudo):
  sudo usermod -aG docker $USER
  newgrp docker

  INSTALLATION (Windows):
  ────────────────────────
  # Télécharger Docker Desktop depuis:
  # https://www.docker.com/products/docker-desktop/
  # Installer et redémarrer

  VÉRIFICATION (toutes plateformes):
  ────────────────────────────────────
  docker --version
  # Attendu: Docker version 25.x.x

  docker-compose --version
  # ou: docker compose version (nouvelle syntaxe)
  # Attendu: Docker Compose version v2.x.x

  # Test rapide: lancer le conteneur "hello-world":
  docker run hello-world

  SORTIE ATTENDUE:
  ────────────────
  Hello from Docker!
  This message shows that your installation appears to be working correctly.

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.2 - Alice met à jour requirements.txt pour PostgreSQL et Redis
────────────────────────────────────────────────────────────────────────────────

  COMMANDES:
  ──────────
  cd taskmanager/
  source venv/bin/activate

  # Ajouter les nouvelles dépendances:
  pip install psycopg2-binary redis flask-caching gunicorn

  # Regénérer requirements.txt:
  pip freeze > requirements.txt

  CONTENU FINAL DE requirements.txt:
  ────────────────────────────────────
  Flask==3.0.0
  Flask-SQLAlchemy==3.1.1
  psycopg2-binary==2.9.9       <- Driver PostgreSQL
  redis==5.0.1                 <- Client Redis
  Flask-Caching==2.1.0         <- Cache avec Redis
  gunicorn==21.2.0             <- Serveur WSGI production
  python-logstash==0.4.8
  python-json-logger==2.0.7
  SQLAlchemy==2.0.23

  CONTENU DE requirements-dev.txt:
  ──────────────────────────────────
  pytest==7.4.3
  pytest-cov==4.1.0
  pytest-flask==1.3.0
  coverage==7.3.2
  black==23.12.1               <- Formatteur de code
  flake8==6.1.0                <- Linter

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.3 - Alice crée le fichier .dockerignore
────────────────────────────────────────────────────────────────────────────────

Ce fichier dit à Docker quels fichiers NE PAS copier dans l'image.
C'est l'équivalent du .gitignore mais pour Docker.
Sans lui, l'image serait énorme et contiendrait des secrets!

  COMMANDE:
  ─────────
  nano .dockerignore

  CONTENU COMPLET:
  ────────────────

  # ── Environnements virtuels Python ──────────────────────────────────
  venv/
  .venv/
  env/
  .env/

  # ── Fichiers de secrets (JAMAIS dans une image Docker!) ─────────────
  .env
  .env.*
  !.env.example          <- Exception: le template (sans secrets) est OK

  # ── Cache Python ────────────────────────────────────────────────────
  __pycache__/
  *.py[cod]
  *$py.class
  *.pyc
  .pytest_cache/
  .coverage
  htmlcov/
  coverage.xml

  # ── Base de données locale ───────────────────────────────────────────
  *.db
  *.sqlite
  *.sqlite3
  instance/

  # ── Logs locaux ──────────────────────────────────────────────────────
  logs/
  *.log

  # ── Git ──────────────────────────────────────────────────────────────
  .git/
  .gitignore
  .gitattributes

  # ── Éditeurs ─────────────────────────────────────────────────────────
  .idea/
  .vscode/
  *.swp
  *.swo
  .DS_Store
  Thumbs.db

  # ── Documentation ─────────────────────────────────────────────────────
  docs/
  *.md
  README*

  # ── CI/CD et configs locales ──────────────────────────────────────────
  .github/
  sonar-project.properties
  docker/
  nginx/

  # ── SonarQube ────────────────────────────────────────────────────────
  .scannerwork/

  RÉSULTAT: L'image Docker ne contiendra que le code source et les
  fichiers nécessaires. Elle sera plus petite, plus rapide, plus sûre.

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.4 - Alice crée le Dockerfile principal (production)
────────────────────────────────────────────────────────────────────────────────

  COMMANDE:
  ─────────
  mkdir -p docker
  nano docker/Dockerfile

  CONTENU COMPLET ET COMMENTÉ:
  ─────────────────────────────

  # ════════════════════════════════════════════════════════════════════
  # DOCKERFILE - TaskManager API (Production)
  # Utilise un build multi-étapes pour minimiser la taille finale
  # ════════════════════════════════════════════════════════════════════

  # ────────────────────────────────────────────────────────────────────
  # ÉTAPE 1: "builder" - Installation des dépendances
  # Utilise une image plus grosse avec les outils de compilation
  # ────────────────────────────────────────────────────────────────────
  FROM python:3.11-slim AS builder

  # Métadonnées de l'image (bonnes pratiques)
  LABEL maintainer="alice@equipe.com"
  LABEL description="TaskManager API - Builder stage"
  LABEL version="1.0.0"

  # Définir le dossier de travail dans le conteneur
  WORKDIR /build

  # Variables d'environnement pour Python
  # PYTHONDONTWRITEBYTECODE=1 -> Pas de fichiers .pyc (inutiles en conteneur)
  # PYTHONUNBUFFERED=1        -> Logs en temps réel (pas de buffer)
  ENV PYTHONDONTWRITEBYTECODE=1 \
      PYTHONUNBUFFERED=1 \
      PIP_NO_CACHE_DIR=1 \
      PIP_DISABLE_PIP_VERSION_CHECK=1

  # Copier UNIQUEMENT les fichiers de dépendances d'abord
  # -> Docker met en cache cette couche si requirements.txt n'a pas changé
  # -> Gain de temps considérable lors des rebuilds!
  COPY requirements.txt .

  # Installer les dépendances dans un dossier séparé
  # --no-cache-dir  : Pas de cache pip (réduit la taille)
  # --user          : Installation dans ~/.local (pas en root)
  RUN pip install --no-cache-dir --user -r requirements.txt

  # ────────────────────────────────────────────────────────────────────
  # ÉTAPE 2: "production" - Image finale légère
  # Repart de zéro, copie seulement ce qui est nécessaire
  # ────────────────────────────────────────────────────────────────────
  FROM python:3.11-slim AS production

  LABEL maintainer="alice@equipe.com"
  LABEL description="TaskManager API - Production"

  # Créer un utilisateur non-root pour la sécurité
  # JAMAIS faire tourner une appli en root dans un conteneur!
  RUN groupadd --gid 1001 appgroup && \
      useradd --uid 1001 --gid appgroup --shell /bin/bash --create-home appuser

  # Dossier de travail
  WORKDIR /app

  # Variables d'environnement production
  ENV PYTHONDONTWRITEBYTECODE=1 \
      PYTHONUNBUFFERED=1 \
      PATH="/home/appuser/.local/bin:$PATH" \
      FLASK_ENV=production \
      FLASK_APP=run.py

  # Copier les dépendances installées depuis l'étape builder
  COPY --from=builder --chown=appuser:appgroup \
       /root/.local /home/appuser/.local

  # Copier les scripts utilitaires
  COPY --chown=appuser:appgroup scripts/entrypoint.sh /entrypoint.sh
  COPY --chown=appuser:appgroup scripts/healthcheck.sh /healthcheck.sh
  RUN chmod +x /entrypoint.sh /healthcheck.sh

  # Copier le code source de l'application
  # (fait en dernier = couche la plus souvent invalidée)
  COPY --chown=appuser:appgroup app/         ./app/
  COPY --chown=appuser:appgroup config.py    ./config.py
  COPY --chown=appuser:appground run.py       ./run.py

  # Passer à l'utilisateur non-root
  USER appuser

  # Exposer le port sur lequel Gunicorn écoute
  EXPOSE 8000

  # Health check: Docker vérifie si l'application est vivante
  # --interval: vérifie toutes les 30 secondes
  # --timeout:  considère comme mort après 10 secondes sans réponse
  # --retries:  3 échecs consécutifs = conteneur "unhealthy"
  # --start-period: attendre 40s au démarrage avant de commencer
  HEALTHCHECK --interval=30s --timeout=10s --retries=3 --start-period=40s \
    CMD /healthcheck.sh

  # Point d'entrée: script qui prépare l'environnement
  ENTRYPOINT ["/entrypoint.sh"]

  # Commande par défaut: Gunicorn (serveur de production)
  # -w 4        : 4 workers (processus parallèles)
  # -b 0.0.0.0:8000 : écouter sur toutes les interfaces, port 8000
  # --timeout 120   : timeout des requêtes à 120 secondes
  # --access-logfile - : logs d'accès vers stdout (Docker les capture)
  # --error-logfile -  : logs d'erreurs vers stderr
  CMD ["gunicorn", \
       "--workers", "4", \
       "--bind", "0.0.0.0:8000", \
       "--timeout", "120", \
       "--access-logfile", "-", \
       "--error-logfile", "-", \
       "run:app"]

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.5 - Alice crée le Dockerfile de développement
────────────────────────────────────────────────────────────────────────────────

  COMMANDE:
  ─────────
  nano docker/Dockerfile.dev

  CONTENU:
  ────────

  # ════════════════════════════════════════════════════════════════════
  # DOCKERFILE.DEV - TaskManager API (Développement)
  # Inclut les outils de dev: hot-reload, debugger, outils de test
  # ════════════════════════════════════════════════════════════════════

  FROM python:3.11-slim

  LABEL description="TaskManager API - Development"

  # Outils système utiles en développement
  RUN apt-get update && apt-get install -y \
      curl \
      git \
      vim \
      netcat-openbsd \    <- Pour wait-for-it.sh
      && rm -rf /var/lib/apt/lists/*

  WORKDIR /app

  ENV PYTHONDONTWRITEBYTECODE=1 \
      PYTHONUNBUFFERED=1 \
      FLASK_ENV=development \
      FLASK_DEBUG=1 \
      FLASK_APP=run.py

  # Installer les dépendances de production ET de développement
  COPY requirements.txt requirements-dev.txt ./
  RUN pip install --no-cache-dir -r requirements.txt -r requirements-dev.txt

  # Copier les scripts
  COPY scripts/entrypoint.sh /entrypoint.sh
  COPY scripts/wait-for-it.sh /wait-for-it.sh
  RUN chmod +x /entrypoint.sh /wait-for-it.sh

  # En développement, le code est monté via un volume (pas copié)
  # -> Les modifications de fichiers sont visibles immédiatement
  # -> Pas besoin de rebuild l'image à chaque changement de code

  EXPOSE 5000

  ENTRYPOINT ["/entrypoint.sh"]

  # Flask dev server avec hot-reload:
  CMD ["python", "-m", "flask", "run", "--host=0.0.0.0", "--port=5000", "--reload"]

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.6 - Alice crée le Dockerfile de test
────────────────────────────────────────────────────────────────────────────────

  COMMANDE:
  ─────────
  nano docker/Dockerfile.test

  CONTENU:
  ────────

  # ════════════════════════════════════════════════════════════════════
  # DOCKERFILE.TEST - TaskManager API (Tests CI/CD)
  # Image légère dédiée à l'exécution des tests automatisés
  # ════════════════════════════════════════════════════════════════════

  FROM python:3.11-slim

  LABEL description="TaskManager API - Tests"

  RUN apt-get update && apt-get install -y \
      curl \
      netcat-openbsd \
      && rm -rf /var/lib/apt/lists/*

  WORKDIR /app

  ENV PYTHONDONTWRITEBYTECODE=1 \
      PYTHONUNBUFFERED=1 \
      FLASK_ENV=testing \
      FLASK_APP=run.py

  COPY requirements.txt requirements-dev.txt ./
  RUN pip install --no-cache-dir -r requirements.txt -r requirements-dev.txt

  # Copier tout le code (source + tests)
  COPY app/     ./app/
  COPY tests/   ./tests/
  COPY config.py run.py ./

  # Commande par défaut: lancer tous les tests avec couverture
  CMD ["pytest", "--cov=app", "--cov-report=xml", "--cov-report=term-missing", "-v"]

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.7 - Alice crée les scripts utilitaires
────────────────────────────────────────────────────────────────────────────────

  COMMANDE:
  ─────────
  mkdir -p scripts

  ── scripts/entrypoint.sh ────────────────────────────────────────────────────

  #!/bin/bash
  # entrypoint.sh - Script exécuté au démarrage du conteneur
  # Vérifie que la base de données est prête AVANT de lancer Flask

  set -e  # Stopper le script si une commande échoue

  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
  echo "  TaskManager API - Démarrage du conteneur"
  echo "  Environnement: ${FLASK_ENV:-production}"
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"

  # ── Attendre que PostgreSQL soit prêt ─────────────────────────────────
  if [ -n "$DATABASE_HOST" ]; then
      echo "[HOURGLASS_WITH_FLOWING_SAND] Attente de PostgreSQL sur ${DATABASE_HOST}:${DATABASE_PORT:-5432}..."

      MAX_RETRIES=30
      RETRY_COUNT=0

      until pg_isready -h "$DATABASE_HOST" -p "${DATABASE_PORT:-5432}" \
            -U "${DATABASE_USER:-taskmanager}" > /dev/null 2>&1; do

          RETRY_COUNT=$((RETRY_COUNT + 1))

          if [ $RETRY_COUNT -ge $MAX_RETRIES ]; then
              echo "[X] PostgreSQL non disponible après ${MAX_RETRIES} tentatives. Abandon."
              exit 1
          fi

          echo "  Tentative ${RETRY_COUNT}/${MAX_RETRIES} - PostgreSQL pas encore prêt..."
          sleep 2
      done

      echo "[OK] PostgreSQL prêt!"
  fi

  # ── Attendre que Redis soit prêt ──────────────────────────────────────
  if [ -n "$REDIS_HOST" ]; then
      echo "[HOURGLASS_WITH_FLOWING_SAND] Attente de Redis sur ${REDIS_HOST}:${REDIS_PORT:-6379}..."

      until nc -z "$REDIS_HOST" "${REDIS_PORT:-6379}" > /dev/null 2>&1; do
          echo "  Redis pas encore prêt, nouvelle tentative dans 2s..."
          sleep 2
      done

      echo "[OK] Redis prêt!"
  fi

  # ── Migrations de base de données ────────────────────────────────────
  if [ "${RUN_MIGRATIONS:-false}" = "true" ]; then
      echo "[SYNC] Exécution des migrations de base de données..."
      python -c "
  from app import create_app, db
  app = create_app()
  with app.app_context():
      db.create_all()
      print('[OK] Tables créées/vérifiées')
  "
  fi

  echo "[RAPIDE] Lancement de l'application..."
  echo ""

  # Exécuter la commande passée à docker run ou CMD du Dockerfile
  exec "$@"

  ── scripts/healthcheck.sh ───────────────────────────────────────────────────

  #!/bin/bash
  # healthcheck.sh - Vérifie que l'application répond correctement
  # Utilisé par HEALTHCHECK dans le Dockerfile

  # Tenter de contacter l'endpoint /health de l'API
  # --fail : retourner code 1 si HTTP != 2xx
  # --silent : pas de sortie
  # --max-time 5 : timeout de 5 secondes
  curl --fail --silent --max-time 5 \
       "http://localhost:${APP_PORT:-8000}/health" > /dev/null 2>&1

  EXIT_CODE=$?

  if [ $EXIT_CODE -eq 0 ]; then
      echo "Health check: OK"
      exit 0
  else
      echo "Health check: FAILED (code: $EXIT_CODE)"
      exit 1
  fi

  COMMANDES:
  ──────────
  chmod +x scripts/entrypoint.sh scripts/healthcheck.sh

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.8 - Alice ajoute un endpoint /health dans Flask
────────────────────────────────────────────────────────────────────────────────

Alice ajoute dans app/routes.py:

  ── AJOUT DANS app/routes.py ────────────────────────────────────────────────

  import os
  from datetime import datetime

  @tasks_bp.route('/health', methods=['GET'])
  def health_check():
      """
      Endpoint de vérification de santé pour Docker HEALTHCHECK et load balancers.
      Vérifie: application Flask + connexion base de données + Redis (si dispo)
      """
      health_status = {
          "status":      "healthy",
          "timestamp":   datetime.utcnow().isoformat() + "Z",
          "version":     os.environ.get("APP_VERSION", "1.0.0"),
          "environment": os.environ.get("FLASK_ENV", "unknown"),
          "checks":      {}
      }

      # ── Vérifier la base de données ────────────────────────────────────
      try:
          # Exécuter une requête simple pour tester la connexion
          db.session.execute(db.text("SELECT 1"))
          health_status["checks"]["database"] = "ok"
      except Exception as e:
          health_status["status"] = "unhealthy"
          health_status["checks"]["database"] = f"error: {str(e)}"

      # ── Vérifier Redis (si configuré) ──────────────────────────────────
      redis_url = os.environ.get("REDIS_URL")
      if redis_url:
          try:
              import redis
              r = redis.from_url(redis_url, socket_connect_timeout=2)
              r.ping()
              health_status["checks"]["redis"] = "ok"
          except Exception as e:
              # Redis non critique: on reste "healthy" mais on le note
              health_status["checks"]["redis"] = f"warning: {str(e)}"

      # Retourner 200 si healthy, 503 si unhealthy
      http_status = 200 if health_status["status"] == "healthy" else 503
      return jsonify(health_status), http_status

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.9 - Alice crée le docker-compose.yml (développement)
────────────────────────────────────────────────────────────────────────────────

C'est LE fichier central: il décrit tous les services qui tournent ensemble.

  COMMANDE:
  ─────────
  nano docker-compose.yml

  CONTENU COMPLET ET COMMENTÉ:
  ─────────────────────────────

  # ════════════════════════════════════════════════════════════════════
  # DOCKER-COMPOSE.YML - Environnement de DÉVELOPPEMENT
  # Lance: Flask (hot-reload) + PostgreSQL + Redis + pgAdmin
  # ════════════════════════════════════════════════════════════════════

  version: '3.9'

  services:

    # ──────────────────────────────────────────────────────────────────
    # SERVICE 1: Application Flask (développement avec hot-reload)
    # ──────────────────────────────────────────────────────────────────
    api:
      # Construire depuis le Dockerfile de développement
      build:
        context: .               <- Dossier racine du projet (contexte de build)
        dockerfile: docker/Dockerfile.dev
      container_name: taskmanager-api-dev

      # Variables d'environnement (depuis le fichier .env)
      env_file:
        - .env

      # Variables additionnelles ou overrides
      environment:
        - FLASK_ENV=development
        - FLASK_DEBUG=1
        - DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}
        - DATABASE_HOST=postgres
        - DATABASE_PORT=5432
        - DATABASE_USER=${POSTGRES_USER}
        - REDIS_URL=redis://redis:6379/0
        - REDIS_HOST=redis
        - RUN_MIGRATIONS=true

      # Ports: machine_hôte:conteneur
      # -> http://localhost:5000 sur la machine de Bob/Claire
      ports:
        - "${API_PORT:-5000}:5000"

      # Volumes:
      # 1. Code source monté en live -> hot-reload sans rebuild
      # 2. Dossier logs persisté sur la machine hôte
      volumes:
        - .:/app                         <- Code source (lecture/écriture)
        - /app/venv                      <- Exclure le venv local (si existant)
        - ./logs:/app/logs               <- Logs persistés

      # Réseau interne entre les services
      networks:
        - taskmanager-network

      # Dépendances: démarrer APRÈS postgres et redis (et attendre qu'ils soient sains)
      depends_on:
        postgres:
          condition: service_healthy     <- Attendre le health check postgres
        redis:
          condition: service_healthy

      # Redémarrer automatiquement si crash (utile en dev)
      restart: unless-stopped

    # ──────────────────────────────────────────────────────────────────
    # SERVICE 2: PostgreSQL - Base de données
    # ──────────────────────────────────────────────────────────────────
    postgres:
      image: postgres:15-alpine          <- Alpine = image très légère
      container_name: taskmanager-postgres

      environment:
        POSTGRES_DB:       ${POSTGRES_DB:-taskmanager}
        POSTGRES_USER:     ${POSTGRES_USER:-taskmanager}
        POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-taskmanager_secret}
        # Optimisations PostgreSQL pour le développement
        POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --locale=fr_FR.UTF-8"

      ports:
        - "${POSTGRES_PORT:-5432}:5432"  <- Accès direct depuis pgAdmin/DBeaver local

      volumes:
        # Données persistées sur la machine hôte (ne disparaissent pas avec le conteneur)
        - postgres_data:/var/lib/postgresql/data
        # Scripts SQL d'initialisation (exécutés une seule fois à la création)
        - ./docker/init-scripts:/docker-entrypoint-initdb.d:ro

      networks:
        - taskmanager-network

      healthcheck:
        test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-taskmanager} -d ${POSTGRES_DB:-taskmanager}"]
        interval: 10s
        timeout: 5s
        retries: 5
        start_period: 30s

      restart: unless-stopped

    # ──────────────────────────────────────────────────────────────────
    # SERVICE 3: Redis - Cache et sessions
    # ──────────────────────────────────────────────────────────────────
    redis:
      image: redis:7-alpine
      container_name: taskmanager-redis

      # Activer la persistence Redis (données sauvegardées toutes les 60s)
      command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru

      ports:
        - "${REDIS_PORT:-6379}:6379"

      volumes:
        - redis_data:/data

      networks:
        - taskmanager-network

      healthcheck:
        test: ["CMD", "redis-cli", "ping"]
        interval: 10s
        timeout: 5s
        retries: 5

      restart: unless-stopped

    # ──────────────────────────────────────────────────────────────────
    # SERVICE 4: pgAdmin - Interface web pour PostgreSQL (dev seulement)
    # ──────────────────────────────────────────────────────────────────
    pgadmin:
      image: dpage/pgadmin4:latest
      container_name: taskmanager-pgadmin

      environment:
        PGADMIN_DEFAULT_EMAIL:    ${PGADMIN_EMAIL:-admin@equipe.com}
        PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_PASSWORD:-pgadmin_secret}
        PGADMIN_CONFIG_SERVER_MODE: "False"   <- Mode desktop (pas de login)

      ports:
        - "${PGADMIN_PORT:-5050}:80"          <- Interface sur http://localhost:5050

      volumes:
        - pgadmin_data:/var/lib/pgadmin

      networks:
        - taskmanager-network

      depends_on:
        postgres:
          condition: service_healthy

      # profiles: ["tools"] <- Décommenter pour que pgAdmin ne démarre pas par défaut
      restart: unless-stopped

  # ────────────────────────────────────────────────────────────────────
  # VOLUMES NOMMÉS: Données persistées entre les redémarrages
  # ────────────────────────────────────────────────────────────────────
  volumes:
    postgres_data:
      driver: local
    redis_data:
      driver: local
    pgadmin_data:
      driver: local

  # ────────────────────────────────────────────────────────────────────
  # RÉSEAUX: Communication interne entre les conteneurs
  # ────────────────────────────────────────────────────────────────────
  networks:
    taskmanager-network:
      driver: bridge
      name: taskmanager-network

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.10 - Alice crée le docker-compose.test.yml
────────────────────────────────────────────────────────────────────────────────

  COMMANDE:
  ─────────
  nano docker-compose.test.yml

  CONTENU:
  ────────

  # ════════════════════════════════════════════════════════════════════
  # DOCKER-COMPOSE.TEST.YML - Environnement de TEST
  # Lance les tests dans des conteneurs isolés (CI/CD)
  # ════════════════════════════════════════════════════════════════════

  version: '3.9'

  services:

    # Tests Flask (utilise SQLite en mémoire, pas PostgreSQL)
    test:
      build:
        context: .
        dockerfile: docker/Dockerfile.test
      container_name: taskmanager-tests

      environment:
        - FLASK_ENV=testing
        - TESTING=true
        - DATABASE_URL=sqlite:///:memory:
        # Logstash désactivé en test (comme dans le guide ELK)
        - LOGSTASH_HOST=127.0.0.1
        - LOGSTASH_PORT=1

      volumes:
        # Récupérer les rapports de couverture sur la machine hôte
        - ./coverage.xml:/app/coverage.xml
        - ./htmlcov:/app/htmlcov

      networks:
        - test-network

    # PostgreSQL de test (base séparée, isolée)
    postgres-test:
      image: postgres:15-alpine
      container_name: taskmanager-postgres-test

      environment:
        POSTGRES_DB:       taskmanager_test
        POSTGRES_USER:     test_user
        POSTGRES_PASSWORD: test_password

      # Pas de persistance pour les tests (repart de zéro à chaque run)
      tmpfs:
        - /var/lib/postgresql/data    <- Stockage en RAM (ultra-rapide pour les tests)

      networks:
        - test-network

      healthcheck:
        test: ["CMD-SHELL", "pg_isready -U test_user"]
        interval: 5s
        timeout: 3s
        retries: 10

  networks:
    test-network:
      driver: bridge

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.11 - Alice crée le docker-compose.prod.yml
────────────────────────────────────────────────────────────────────────────────

  COMMANDE:
  ─────────
  nano docker-compose.prod.yml

  CONTENU:
  ────────

  # ════════════════════════════════════════════════════════════════════
  # DOCKER-COMPOSE.PROD.YML - Environnement de PRODUCTION
  # Lance: Nginx + Gunicorn + PostgreSQL + Redis
  # Utilisation: docker-compose -f docker-compose.prod.yml up -d
  # ════════════════════════════════════════════════════════════════════

  version: '3.9'

  services:

    # ── Nginx: Reverse proxy et serveur de fichiers statiques ────────────
    nginx:
      image: nginx:1.25-alpine
      container_name: taskmanager-nginx

      ports:
        - "80:80"
        - "443:443"

      volumes:
        - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
        - ./nginx/ssl:/etc/nginx/ssl:ro              <- Certificats SSL
        - nginx_logs:/var/log/nginx

      networks:
        - taskmanager-network

      depends_on:
        api:
          condition: service_healthy

      restart: always     <- En prod: toujours redémarrer

    # ── Application Flask (Gunicorn, image de production) ────────────────
    api:
      build:
        context: .
        dockerfile: docker/Dockerfile
        target: production            <- Cibler l'étape "production" du multi-stage

      image: ghcr.io/equipe/taskmanager-api:${APP_VERSION:-latest}
      container_name: taskmanager-api

      env_file:
        - .env.prod                   <- Fichier de secrets de production

      environment:
        - FLASK_ENV=production
        - DATABASE_HOST=postgres
        - REDIS_HOST=redis
        - RUN_MIGRATIONS=true

      # En prod: PAS de port exposé directement (passe par Nginx)
      expose:
        - "8000"

      volumes:
        - app_logs:/app/logs

      networks:
        - taskmanager-network

      depends_on:
        postgres:
          condition: service_healthy
        redis:
          condition: service_healthy

      # Déploiement sans interruption (rolling update)
      deploy:
        replicas: 2                   <- 2 instances en parallèle
        update_config:
          parallelism: 1              <- Mettre à jour 1 instance à la fois
          delay: 10s
          failure_action: rollback    <- Rollback si la mise à jour échoue

      restart: always

    # ── PostgreSQL de production ──────────────────────────────────────────
    postgres:
      image: postgres:15-alpine
      container_name: taskmanager-postgres-prod

      env_file:
        - .env.prod

      volumes:
        - postgres_data:/var/lib/postgresql/data
        - ./backups:/backups           <- Dossier pour les sauvegardes

      networks:
        - taskmanager-network

      healthcheck:
        test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
        interval: 30s
        timeout: 10s
        retries: 5
        start_period: 60s

      restart: always

    # ── Redis de production ───────────────────────────────────────────────
    redis:
      image: redis:7-alpine
      container_name: taskmanager-redis-prod
      command: redis-server --requirepass ${REDIS_PASSWORD} --appendonly yes

      volumes:
        - redis_data:/data

      networks:
        - taskmanager-network

      healthcheck:
        test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
        interval: 30s
        timeout: 10s
        retries: 5

      restart: always

  volumes:
    postgres_data:
    redis_data:
    app_logs:
    nginx_logs:

  networks:
    taskmanager-network:
      driver: bridge

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.12 - Alice crée la configuration Nginx
────────────────────────────────────────────────────────────────────────────────

  COMMANDE:
  ─────────
  mkdir -p nginx
  nano nginx/nginx.conf

  CONTENU:
  ────────

  # ════════════════════════════════════════════════════════════════════
  # NGINX.CONF - Reverse proxy pour TaskManager API
  # ════════════════════════════════════════════════════════════════════

  worker_processes auto;

  events {
      worker_connections 1024;
  }

  http {
      # Logs au format JSON (intégration avec ELK)
      log_format json_combined escape=json
          '{"time":"$time_iso8601",'
          '"remote_addr":"$remote_addr",'
          '"method":"$request_method",'
          '"uri":"$request_uri",'
          '"status":$status,'
          '"duration_ms":$request_time,'
          '"bytes_sent":$bytes_sent,'
          '"user_agent":"$http_user_agent"}';

      access_log /var/log/nginx/access.log json_combined;
      error_log  /var/log/nginx/error.log warn;

      # Optimisations
      sendfile on;
      gzip on;
      gzip_types application/json text/plain;

      # Upstream: les instances de l'API Flask (load balancing)
      upstream taskmanager_api {
          least_conn;                        <- Moins de connexions actives
          server api:8000 max_fails=3 fail_timeout=30s;
          # En prod avec 2 replicas:
          # server api_1:8000;
          # server api_2:8000;
          keepalive 32;
      }

      server {
          listen 80;
          server_name taskmanager.equipe.local;

          # Rediriger HTTP vers HTTPS en production
          # return 301 https://$host$request_uri;

          # Headers de sécurité
          add_header X-Content-Type-Options nosniff;
          add_header X-Frame-Options DENY;
          add_header X-XSS-Protection "1; mode=block";

          # Timeout pour les requêtes longues
          proxy_read_timeout    120s;
          proxy_connect_timeout 10s;

          # Transmettre vers l'API Flask
          location /api/ {
              proxy_pass         http://taskmanager_api;
              proxy_set_header   Host             $host;
              proxy_set_header   X-Real-IP        $remote_addr;
              proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;
              proxy_set_header   X-Forwarded-Proto $scheme;
          }

          # Endpoint de santé Nginx
          location /nginx-health {
              return 200 "OK\n";
              add_header Content-Type text/plain;
          }
      }
  }

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.13 - Alice crée le fichier .env.example
────────────────────────────────────────────────────────────────────────────────

Ce fichier est dans Git. Il montre les variables nécessaires SANS les valeurs
sensibles. Bob et Claire le copient en .env et remplissent leurs valeurs.

  COMMANDE:
  ─────────
  nano .env.example

  CONTENU:
  ────────

  # ════════════════════════════════════════════════════════════════════
  # .ENV.EXAMPLE - Template de configuration TaskManager
  # Copier ce fichier: cp .env.example .env
  # Puis remplir les valeurs selon votre environnement
  # NE JAMAIS committer le fichier .env dans Git!
  # ════════════════════════════════════════════════════════════════════

  # ── Application Flask ────────────────────────────────────────────────
  FLASK_ENV=development
  FLASK_DEBUG=1
  FLASK_APP=run.py
  APP_PORT=5000
  APP_VERSION=1.0.0
  SECRET_KEY=changez-moi-en-production-utilisez-un-vrai-secret

  # ── PostgreSQL ───────────────────────────────────────────────────────
  POSTGRES_DB=taskmanager
  POSTGRES_USER=taskmanager
  POSTGRES_PASSWORD=changez-ce-mot-de-passe
  POSTGRES_HOST=postgres
  POSTGRES_PORT=5432
  DATABASE_URL=postgresql://taskmanager:changez-ce-mot-de-passe@postgres:5432/taskmanager

  # ── Redis ─────────────────────────────────────────────────────────────
  REDIS_HOST=redis
  REDIS_PORT=6379
  REDIS_URL=redis://redis:6379/0

  # ── ELK (optionnel, voir guide ELK) ───────────────────────────────────
  LOGSTASH_HOST=localhost
  LOGSTASH_PORT=5000
  APP_ENV=development

  # ── SonarQube (optionnel, voir guide SonarQube) ───────────────────────
  SONAR_TOKEN=votre-token-sonarqube
  SONAR_HOST_URL=http://localhost:9000

  # ── pgAdmin (interface web PostgreSQL) ────────────────────────────────
  PGADMIN_EMAIL=admin@equipe.com
  PGADMIN_PASSWORD=changez-ce-mot-de-passe
  PGADMIN_PORT=5050

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.14 - Alice fait le premier build et vérifie que tout fonctionne
────────────────────────────────────────────────────────────────────────────────

  COMMANDES TERMINAL:
  ───────────────────
  cd taskmanager/

  # Créer son propre .env depuis le template:
  cp .env.example .env
  nano .env    <- Remplir avec ses propres valeurs

  # Construire les images (première fois: ~5 minutes, ensuite <30s):
  docker-compose build

  SORTIE ATTENDUE:
  ────────────────
  [+] Building 45.3s (18/18) FINISHED
   => [api internal] load build context                              0.1s
   => [api builder 1/5] FROM python:3.11-slim                       12.3s
   => [api builder 2/5] WORKDIR /build                              0.1s
   => [api builder 3/5] COPY requirements.txt .                     0.2s
   => [api builder 4/5] RUN pip install --no-cache-dir...           28.4s
   => [api production 1/5] FROM python:3.11-slim                    0.0s
   => [api production 2/5] RUN groupadd...                          1.2s
   => [api production 3/5] WORKDIR /app                             0.1s
   => [api production 4/5] COPY --from=builder...                   0.8s
   => [api production 5/5] COPY app/ config.py run.py               0.2s
   => [api] exporting to image                                       1.8s
   [OK] Successfully built abc123def456

  # Lancer tous les services:
  docker-compose up -d

  SORTIE ATTENDUE:
  ────────────────
  [+] Running 4/4
   [OK] Container taskmanager-postgres  Started  3.2s
   [OK] Container taskmanager-redis     Started  3.1s
   [OK] Container taskmanager-api-dev   Started  5.8s
   [OK] Container taskmanager-pgadmin   Started  4.1s

  # Vérifier que tout tourne:
  docker-compose ps

  SORTIE ATTENDUE:
  ────────────────
  NAME                      STATUS          PORTS
  taskmanager-api-dev       Up (healthy)    0.0.0.0:5000->5000/tcp
  taskmanager-postgres      Up (healthy)    0.0.0.0:5432->5432/tcp
  taskmanager-redis         Up (healthy)    0.0.0.0:6379->6379/tcp
  taskmanager-pgadmin       Up              0.0.0.0:5050->80/tcp

  # Tester l'application:
  curl http://localhost:5000/health

  RÉPONSE ATTENDUE:
  ─────────────────
  {
    "status": "healthy",
    "timestamp": "2024-01-15T10:30:00Z",
    "version": "1.0.0",
    "environment": "development",
    "checks": {
      "database": "ok",
      "redis": "ok"
    }
  }

  curl http://localhost:5000/tasks
  # Réponse: []  (liste vide, base de données toute neuve)

  curl -X POST http://localhost:5000/tasks \
       -H "Content-Type: application/json" \
       -d '{"title": "Premier test Docker!", "description": "Ca marche!"}'
  # Réponse: {"id": 1, "title": "Premier test Docker!", ...}

  Alice commite tout et pousse sur GitHub:
  ─────────────────────────────────────────
  git add Dockerfile docker/ docker-compose.yml docker-compose.test.yml \
          docker-compose.prod.yml .dockerignore .env.example nginx/ scripts/
  git commit -m "feat: dockerize application - dev/test/prod environments

  - Multi-stage Dockerfile (builder + production)
  - Dockerfile.dev with hot-reload
  - Dockerfile.test for CI/CD
  - docker-compose.yml for development (Flask+PostgreSQL+Redis+pgAdmin)
  - docker-compose.test.yml for isolated tests
  - docker-compose.prod.yml with Nginx + Gunicorn
  - entrypoint.sh with DB health checks
  - nginx.conf with JSON logging
  - .env.example template"

  git push origin main


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PARTIE 2 - CONFIGURATION POSTE DE BOB ET CLAIRE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Bob et Claire reçoivent d'Alice:
  - Un message Slack: "J'ai dockerisé le projet! Lisez PARTIE 2 du guide."
  - L'URL du dépôt Git (déjà connu)
  - Leur fichier .env personnel (Alice leur envoie en privé)

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 2.1 - Bob installe Docker sur son poste Ubuntu
────────────────────────────────────────────────────────────────────────────────

BOB dans son terminal:

  COMMANDES:
  ──────────
  # Méthode officielle recommandée:
  curl -fsSL https://get.docker.com -o get-docker.sh
  sudo sh get-docker.sh

  # Ajouter son utilisateur au groupe docker:
  sudo usermod -aG docker $USER
  newgrp docker   <- Appliquer sans redémarrer

  # Vérification:
  docker run hello-world
  docker-compose --version

  SORTIE ATTENDUE:
  ────────────────
  Hello from Docker!
  Docker Compose version v2.23.0

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 2.2 - Bob récupère le projet et lance l'environnement
────────────────────────────────────────────────────────────────────────────────

BOB dans son terminal:

  COMMANDES:
  ──────────
  # Récupérer les dernières modifications (le Dockerfile d'Alice):
  cd taskmanager/
  git pull origin main

  # Créer son .env personnel depuis le template:
  cp .env.example .env
  nano .env
  # Bob remplace les valeurs par les siennes:
  # SECRET_KEY=ma-cle-secrete-bob-2024
  # POSTGRES_PASSWORD=mon-mot-de-passe-bob
  # etc.

  # Lancer tout en UNE SEULE COMMANDE:
  docker-compose up -d

  SORTIE (première fois - téléchargement des images):
  ────────────────────────────────────────────────────
  [+] Pulling 4/4
   [OK] postgres Pulled    45.2s
   [OK] redis Pulled       12.3s
   [OK] pgadmin Pulled     38.7s
  [+] Building 38.4s (18/18) FINISHED
  [+] Running 4/4
   [OK] Container taskmanager-postgres  Started
   [OK] Container taskmanager-redis     Started
   [OK] Container taskmanager-api-dev   Started
   [OK] Container taskmanager-pgadmin   Started

  # Vérifier:
  docker-compose ps
  curl http://localhost:5000/health

  BOB PEUT MAINTENANT CODER sans installer Python, PostgreSQL ni Redis!
  Tout est dans les conteneurs.

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 2.3 - Claire installe Docker Desktop sur Windows et lance le projet
────────────────────────────────────────────────────────────────────────────────

CLAIRE sur Windows:

  INSTALLATION:
  ─────────────
  1. Télécharger Docker Desktop: https://www.docker.com/products/docker-desktop/
  2. Lancer l'installeur .exe
  3. Cocher "Use WSL 2 instead of Hyper-V" (recommandé)
  4. Redémarrer Windows
  5. Lancer Docker Desktop depuis le menu Démarrer
  6. Attendre que l'icône Docker dans la barre des tâches soit verte

  DANS POWERSHELL ou Git Bash:
  ────────────────────────────
  docker --version
  docker-compose --version

  cd taskmanager/
  git pull origin main
  cp .env.example .env
  notepad .env   <- Éditer avec ses valeurs

  docker-compose up -d

  # Vérifier dans le navigateur:
  # http://localhost:5000/health  -> {"status":"healthy",...}
  # http://localhost:5050         -> pgAdmin (interface PostgreSQL)

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 2.4 - Commandes quotidiennes que Bob et Claire utilisent
────────────────────────────────────────────────────────────────────────────────

  # ── DÉMARRAGE ─────────────────────────────────────────────────────────
  docker-compose up -d
  # -> Lance tous les services en arrière-plan
  # -> La première fois: build automatique des images

  # ── ARRÊT (DONNÉES CONSERVÉES) ───────────────────────────────────────
  docker-compose down
  # -> Stoppe et supprime les conteneurs
  # -> Les données PostgreSQL et Redis sont conservées dans les volumes

  # ── ARRÊT COMPLET (DONNÉES SUPPRIMÉES) ───────────────────────────────
  docker-compose down -v
  # -> ATTENTION: supprime AUSSI les volumes (données perdues!)
  # -> Utile pour repartir de zéro

  # ── VOIR LES LOGS EN TEMPS RÉEL ──────────────────────────────────────
  docker-compose logs -f              # Tous les services
  docker-compose logs -f api          # Seulement Flask
  docker-compose logs -f postgres     # Seulement PostgreSQL
  docker-compose logs --tail=50 api   # 50 dernières lignes

  # ── EXÉCUTER UNE COMMANDE DANS UN CONTENEUR ───────────────────────────
  docker-compose exec api bash        # Ouvrir un shell dans le conteneur Flask
  docker-compose exec api python      # Lancer Python dans le conteneur
  docker-compose exec postgres psql -U taskmanager -d taskmanager  # Shell PostgreSQL

  # ── RECONSTRUIRE APRÈS MODIFICATIONS DU DOCKERFILE ────────────────────
  docker-compose build api            # Rebuild seulement Flask
  docker-compose up -d --build        # Rebuild ET relancer

  # ── VOIR L'ÉTAT DES CONTENEURS ───────────────────────────────────────
  docker-compose ps                   # Statut avec ports
  docker stats                        # CPU/RAM en temps réel


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PARTIE 3 - WORKFLOW QUOTIDIEN DE L'ÉQUIPE (CAS CONCRETS)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

════════════════════════════════════════════════════════════════════════════════
SCÉNARIO A - Bob ajoute une dépendance Python et met à jour l'image
════════════════════════════════════════════════════════════════════════════════

Bob doit ajouter la librairie "Pillow" pour redimensionner des images
attachées aux tâches (nouvelle feature).

──────────────────────────────────────────────────────────────────────────────
A.1 - Bob ajoute la dépendance et rebuild l'image
──────────────────────────────────────────────────────────────────────────────

BOB dans son terminal:

  # Toujours travailler dans un conteneur pour tester:
  docker-compose exec api pip install Pillow

  # Tester que ça marche dans le conteneur:
  docker-compose exec api python -c "from PIL import Image; print('Pillow OK!')"

  # Ajouter dans requirements.txt:
  echo "Pillow==10.1.0" >> requirements.txt

  # Pousser le requirements.txt mis à jour:
  git add requirements.txt
  git commit -m "feat: add Pillow for image processing"
  git push

  # Reconstruire l'image avec la nouvelle dépendance:
  docker-compose build api

  SORTIE (avec cache - très rapide):
  ───────────────────────────────────
  [+] Building 8.2s (18/18) FINISHED
   => CACHED [builder 1/5] FROM python:3.11-slim              0.0s
   => CACHED [builder 2/5] WORKDIR /build                     0.0s
   => CACHED [builder 3/5] COPY requirements.txt              0.0s  <- requirements.txt changé!
   => [builder 4/5] RUN pip install --no-cache-dir...         6.8s  <- Réinstallation
   => [production 5/5] COPY app/ config.py run.py             0.2s
   [OK] Successfully built

  # Relancer avec la nouvelle image:
  docker-compose up -d api

──────────────────────────────────────────────────────────────────────────────
A.2 - Alice et Claire récupèrent la mise à jour
──────────────────────────────────────────────────────────────────────────────

ALICE et CLAIRE dans leur terminal:

  COMMANDES:
  ──────────
  git pull origin main

  # Reconstruire l'image (Docker détecte que requirements.txt a changé):
  docker-compose build api

  # Relancer:
  docker-compose up -d api

  # Vérifier que Pillow est bien présent:
  docker-compose exec api python -c "import PIL; print(PIL.__version__)"
  # -> 10.1.0


════════════════════════════════════════════════════════════════════════════════
SCÉNARIO B - Claire débogue un problème qui n'existe que dans le conteneur
════════════════════════════════════════════════════════════════════════════════

Claire reçoit une erreur 500 sur son poste. Bob dit que ça marche chez lui.
Avec Docker, Claire peut inspecter l'état exact du conteneur.

──────────────────────────────────────────────────────────────────────────────
B.1 - Claire inspecte les logs du conteneur
──────────────────────────────────────────────────────────────────────────────

CLAIRE dans son terminal:

  # Voir les logs Flask en temps réel:
  docker-compose logs -f api

  SORTIE:
  ───────
  taskmanager-api-dev | [ERROR] Exception on /tasks [POST]
  taskmanager-api-dev | Traceback (most recent call last):
  taskmanager-api-dev |   File "/app/app/routes.py", line 45, in create_task
  taskmanager-api-dev |     task = Task(title=data['title'])
  taskmanager-api-dev | sqlalchemy.exc.OperationalError: (psycopg2.OperationalError)
  taskmanager-api-dev | FATAL: password authentication failed for user "taskmanager"

  Claire comprend immédiatement: son mot de passe PostgreSQL dans .env
  ne correspond pas à celui utilisé pour créer la base.

──────────────────────────────────────────────────────────────────────────────
B.2 - Claire entre dans le conteneur pour investiguer
──────────────────────────────────────────────────────────────────────────────

  COMMANDES:
  ──────────
  # Ouvrir un shell interactif dans le conteneur Flask:
  docker-compose exec api bash

  # Maintenant Claire est DANS le conteneur (comme un SSH):
  # (conteneur)$ 

  # Tester la connexion PostgreSQL directement depuis le conteneur:
  python -c "
  import psycopg2, os
  try:
      conn = psycopg2.connect(os.environ['DATABASE_URL'])
      print('Connexion OK!')
      conn.close()
  except Exception as e:
      print(f'Erreur: {e}')
  "
  # -> Erreur: password authentication failed

  # Voir les variables d'environnement du conteneur:
  env | grep POSTGRES
  # -> POSTGRES_PASSWORD=mon-vieux-mot-de-passe-claire
  # -> DATABASE_URL=postgresql://taskmanager:ancien-mdp@postgres:5432/taskmanager

  # Claire réalise que son .env a un ancien mot de passe
  exit   <- Sortir du conteneur

  # Vérifier l'état de PostgreSQL:
  docker-compose exec postgres psql -U taskmanager -c "\l"
  # -> Erreur: même problème de mot de passe

──────────────────────────────────────────────────────────────────────────────
B.3 - Claire corrige en recréant la base avec le bon mot de passe
──────────────────────────────────────────────────────────────────────────────

  COMMANDES:
  ──────────
  # Mettre à jour .env avec le bon mot de passe:
  nano .env
  # -> POSTGRES_PASSWORD=mon-nouveau-bon-mot-de-passe
  # -> DATABASE_URL=postgresql://taskmanager:mon-nouveau-bon-mot-de-passe@postgres:5432/taskmanager

  # Supprimer le volume PostgreSQL (repart de zéro):
  docker-compose down -v postgres

  # Recréer PostgreSQL avec le bon mot de passe:
  docker-compose up -d postgres

  # Attendre qu'il soit prêt:
  docker-compose logs -f postgres
  # -> "database system is ready to accept connections" <- bon signe

  # Redémarrer l'API:
  docker-compose restart api

  # Vérifier:
  curl http://localhost:5000/health
  # -> {"status": "healthy", "checks": {"database": "ok"}}  [OK]


════════════════════════════════════════════════════════════════════════════════
SCÉNARIO C - Alice prépare le build de production et le publie sur le registry
════════════════════════════════════════════════════════════════════════════════

L'équipe est prête à déployer la version 1.2.0 en production.
Alice construit l'image de production et la publie sur GitHub Container Registry.

──────────────────────────────────────────────────────────────────────────────
C.1 - Alice construit l'image de production
──────────────────────────────────────────────────────────────────────────────

ALICE dans son terminal:

  COMMANDES:
  ──────────
  # Se connecter au registry GitHub:
  echo $GITHUB_TOKEN | docker login ghcr.io -u alice --password-stdin

  # Construire l'image de PRODUCTION (multi-stage Dockerfile):
  docker build \
    --file docker/Dockerfile \
    --target production \
    --tag ghcr.io/equipe/taskmanager-api:1.2.0 \
    --tag ghcr.io/equipe/taskmanager-api:latest \
    --build-arg APP_VERSION=1.2.0 \
    .

  SORTIE:
  ───────
  [+] Building 42.1s (18/18) FINISHED
   => [builder 1/5] FROM python:3.11-slim                   0.0s (cache)
   => [builder 4/5] RUN pip install...                      0.0s (cache)
   => [production 5/5] COPY app/ config.py run.py           0.3s
   [OK] Successfully built f8a2c1d4e5b6
   [OK] Successfully tagged ghcr.io/equipe/taskmanager-api:1.2.0
   [OK] Successfully tagged ghcr.io/equipe/taskmanager-api:latest

  # Vérifier la taille de l'image (doit être ~200MB max):
  docker images ghcr.io/equipe/taskmanager-api

  SORTIE:
  ───────
  REPOSITORY                           TAG      SIZE
  ghcr.io/equipe/taskmanager-api       1.2.0    187MB    [OK] Bonne taille
  ghcr.io/equipe/taskmanager-api       latest   187MB

  # Tester l'image de production localement AVANT de publier:
  docker run --rm \
    --env-file .env \
    -e DATABASE_HOST=localhost \
    -e FLASK_ENV=production \
    -p 8000:8000 \
    ghcr.io/equipe/taskmanager-api:1.2.0

  # Dans un autre terminal:
  curl http://localhost:8000/health
  # -> {"status": "healthy", ...}  [OK]

──────────────────────────────────────────────────────────────────────────────
C.2 - Alice publie l'image sur GitHub Container Registry
──────────────────────────────────────────────────────────────────────────────

  COMMANDES:
  ──────────
  # Pousser les deux tags:
  docker push ghcr.io/equipe/taskmanager-api:1.2.0
  docker push ghcr.io/equipe/taskmanager-api:latest

  SORTIE:
  ───────
  The push refers to repository [ghcr.io/equipe/taskmanager-api]
  1a2b3c4d5e6f: Pushed
  f6e5d4c3b2a1: Pushed
  ...
  1.2.0: digest: sha256:abc123...  size: 1847
  latest: digest: sha256:abc123... size: 1847

INTERFACE WEB GitHub:

  1. Aller sur github.com/equipe/taskmanager
  2. Onglet "Packages" (ou "Containers")
  3. Alice voit son package:
     ┌────────────────────────────────────────────────────────────────┐
     │  [PACKAGE] taskmanager-api                                            │
     │                                                                │
     │  ghcr.io/equipe/taskmanager-api                               │
     │  Published: just now                                          │
     │                                                                │
     │  Tags:                                                         │
     │  [OK] 1.2.0    (187 MB)   Published just now                    │
     │  [OK] latest   (187 MB)   Published just now                    │
     │                                                                │
     │  [   Install   ]  docker pull ghcr.io/equipe/taskmanager-api  │
     └────────────────────────────────────────────────────────────────┘

──────────────────────────────────────────────────────────────────────────────
C.3 - Alice déploie en production avec docker-compose.prod.yml
──────────────────────────────────────────────────────────────────────────────

Sur le serveur de production (connexion SSH):

  COMMANDES SUR LE SERVEUR:
  ──────────────────────────
  # Connexion au registry:
  echo $GITHUB_TOKEN | docker login ghcr.io -u equipe --password-stdin

  # Télécharger la nouvelle image:
  docker pull ghcr.io/equipe/taskmanager-api:1.2.0

  # Mettre à jour l'image utilisée:
  export APP_VERSION=1.2.0

  # Déploiement sans interruption (rolling update):
  docker-compose -f docker-compose.prod.yml up -d --no-deps api

  SORTIE:
  ───────
  [+] Running 1/1
   [OK] Container taskmanager-api  Started  2.3s

  # Vérifier que la nouvelle version est en ligne:
  curl http://localhost/api/health
  # -> {"status":"healthy","version":"1.2.0",...}  [OK]

  # En cas de problème: rollback vers la version précédente en 5 secondes!
  docker-compose -f docker-compose.prod.yml up -d \
    --no-deps \
    -e APP_VERSION=1.1.0 \
    api


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PARTIE 4 - TESTS AVEC DOCKER
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Claire gère tous les tests. Elle s'assure que les tests tournent
de manière identique en local, sur le poste de Bob et en CI/CD.

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 4.1 - Lancer les tests dans Docker
────────────────────────────────────────────────────────────────────────────────

CLAIRE dans son terminal:

  MÉTHODE 1: Via docker-compose.test.yml (recommandée)
  ─────────────────────────────────────────────────────
  docker-compose -f docker-compose.test.yml run --rm test

  SORTIE ATTENDUE:
  ────────────────
  ============= test session starts ==============
  platform linux -- Python 3.11.8
  collected 14 items

  tests/test_routes.py::test_get_tasks_empty            PASSED [ 7%]
  tests/test_routes.py::test_create_task                PASSED [14%]
  tests/test_routes.py::test_create_task_missing_title  PASSED [21%]
  tests/test_routes.py::test_get_task                   PASSED [28%]
  tests/test_routes.py::test_get_task_not_found         PASSED [35%]
  tests/test_routes.py::test_update_task                PASSED [42%]
  tests/test_routes.py::test_delete_task                PASSED [50%]
  tests/test_routes.py::test_filter_tasks_done          PASSED [57%]
  tests/test_routes.py::test_filter_tasks_not_done      PASSED [64%]
  tests/test_routes.py::test_filter_tasks_no_param      PASSED [71%]
  tests/test_routes.py::test_search_tasks               PASSED [78%]
  tests/test_routes.py::test_health_check               PASSED [85%]
  tests/models/test_models.py::test_task_model          PASSED [92%]
  tests/models/test_models.py::test_task_to_dict        PASSED [100%]

  =============== 14 passed in 1.23s ===============

  ---------- coverage: platform linux, python 3.11.8 -----------
  Name              Stmts   Miss  Cover
  app/__init__.py      24      1    96%
  app/models.py        18      0   100%
  app/routes.py        68      4    94%
  app/utils.py         12      1    92%
  TOTAL               122      6    95%

  MÉTHODE 2: Tests dans le conteneur de développement (plus rapide)
  ──────────────────────────────────────────────────────────────────
  # Si l'environnement dev tourne déjà:
  docker-compose exec api pytest --cov=app --cov-report=term-missing -v

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 4.2 - Script de test unifié pour toute l'équipe
────────────────────────────────────────────────────────────────────────────────

Alice crée un script test.sh qui unifie la commande pour tout le monde:

  COMMANDE:
  ─────────
  nano test.sh

  CONTENU:
  ────────

  #!/bin/bash
  # test.sh - Lancer les tests dans Docker (identique pour Alice, Bob et Claire)

  set -e

  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
  echo "  TaskManager - Tests Docker"
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"

  # Construire l'image de test si nécessaire:
  echo "[OUTIL] Construction de l'image de test..."
  docker-compose -f docker-compose.test.yml build test

  # Lancer les tests:
  echo "[TEST] Lancement des tests..."
  docker-compose -f docker-compose.test.yml run --rm test

  EXIT_CODE=$?

  if [ $EXIT_CODE -eq 0 ]; then
      echo ""
      echo "[OK] Tous les tests sont passés!"
  else
      echo ""
      echo "[X] Des tests ont échoué (code: $EXIT_CODE)"
      exit $EXIT_CODE
  fi

  # Nettoyer les conteneurs de test:
  docker-compose -f docker-compose.test.yml down --remove-orphans

  COMMANDE:
  ─────────
  chmod +x test.sh

  # Utilisation par tous les membres de l'équipe:
  ./test.sh


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PARTIE 5 - INTÉGRATION CI/CD AVEC GITHUB ACTIONS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Alice configure la pipeline CI/CD complète qui automatise tout:
tests -> SonarQube -> build Docker -> push registry -> déploiement.

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 5.1 - Secrets GitHub nécessaires
────────────────────────────────────────────────────────────────────────────────

INTERFACE WEB GitHub (github.com/equipe/taskmanager):

  Settings > Secrets and variables > Actions > New repository secret:

  ┌──────────────────────────────────────────────────────────────────────┐
  │  Secrets configurés:                                                 │
  │                                                                      │
  │  SONAR_TOKEN          -> Token SonarQube (guide SonarQube)           │
  │  SONAR_HOST_URL       -> URL SonarQube                               │
  │  GHCR_TOKEN           -> Token GitHub pour publier les images Docker  │
  │  PROD_SERVER_HOST     -> IP du serveur de production                 │
  │  PROD_SERVER_USER     -> Utilisateur SSH de production               │
  │  PROD_SERVER_SSH_KEY  -> Clé privée SSH pour déployer               │
  │  POSTGRES_PASSWORD    -> Mot de passe PostgreSQL de production       │
  │  SECRET_KEY           -> Clé secrète Flask de production             │
  └──────────────────────────────────────────────────────────────────────┘

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 5.2 - Pipeline GitHub Actions complète
────────────────────────────────────────────────────────────────────────────────

  COMMANDE:
  ─────────
  mkdir -p .github/workflows
  nano .github/workflows/ci-cd.yml

  CONTENU COMPLET:
  ────────────────

  name: CI/CD - Tests + Docker + Déploiement

  on:
    push:
      branches: [main, develop, 'feature/**']
    pull_request:
      branches: [main]
    release:
      types: [published]    <- Déploiement en production lors d'une release GitHub

  env:
    REGISTRY: ghcr.io
    IMAGE_NAME: ${{ github.repository }}/taskmanager-api

  jobs:

    # ══════════════════════════════════════════════════════════════════
    # JOB 1: Tests automatisés dans Docker
    # ══════════════════════════════════════════════════════════════════
    tests:
      name: Tests Python dans Docker
      runs-on: ubuntu-latest

      steps:
        - name: Checkout code
          uses: actions/checkout@v4
          with:
            fetch-depth: 0

        - name: Créer le fichier .env de test
          run: |
            cat > .env << EOF
            FLASK_ENV=testing
            TESTING=true
            DATABASE_URL=sqlite:///:memory:
            LOGSTASH_HOST=127.0.0.1
            LOGSTASH_PORT=1
            APP_ENV=ci
            SECRET_KEY=ci-secret-key-not-for-production
            EOF

        - name: Build de l'image de test
          run: docker-compose -f docker-compose.test.yml build test

        - name: Lancement des tests
          run: docker-compose -f docker-compose.test.yml run --rm test

        - name: Copier le rapport de couverture
          run: |
            # Extraire coverage.xml du conteneur de test
            CONTAINER_ID=$(docker-compose -f docker-compose.test.yml \
              run -d test sleep 10)
            docker cp $CONTAINER_ID:/app/coverage.xml coverage.xml || true
            docker stop $CONTAINER_ID || true

        - name: Nettoyer les conteneurs de test
          if: always()
          run: docker-compose -f docker-compose.test.yml down --remove-orphans

        - name: Archiver le rapport de couverture
          uses: actions/upload-artifact@v3
          with:
            name: coverage-report
            path: coverage.xml

    # ══════════════════════════════════════════════════════════════════
    # JOB 2: Analyse SonarQube (dépend des tests)
    # ══════════════════════════════════════════════════════════════════
    sonarqube:
      name: Analyse SonarQube
      runs-on: ubuntu-latest
      needs: tests            <- S'exécute seulement si les tests passent

      steps:
        - uses: actions/checkout@v4
          with:
            fetch-depth: 0

        - name: Télécharger le rapport de couverture
          uses: actions/download-artifact@v3
          with:
            name: coverage-report

        - name: Cache SonarQube
          uses: actions/cache@v3
          with:
            path: ~/.sonar/cache
            key: ${{ runner.os }}-sonar

        - name: SonarQube Scan
          uses: sonarsource/sonarqube-scan-action@master
          env:
            SONAR_TOKEN:    ${{ secrets.SONAR_TOKEN }}
            SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}

        - name: SonarQube Quality Gate
          uses: sonarsource/sonarqube-quality-gate-action@master
          timeout-minutes: 5
          env:
            SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

    # ══════════════════════════════════════════════════════════════════
    # JOB 3: Build et publication de l'image Docker
    # (uniquement sur main/develop et releases)
    # ══════════════════════════════════════════════════════════════════
    build-and-push:
      name: Build et Push image Docker
      runs-on: ubuntu-latest
      needs: [tests, sonarqube]    <- Tests ET SonarQube doivent passer

      if: |
        github.event_name == 'push' && (
          github.ref == 'refs/heads/main' ||
          github.ref == 'refs/heads/develop'
        ) || github.event_name == 'release'

      permissions:
        contents: read
        packages: write    <- Permission pour publier sur ghcr.io

      outputs:
        image-tag: ${{ steps.meta.outputs.tags }}
        image-digest: ${{ steps.build.outputs.digest }}

      steps:
        - uses: actions/checkout@v4

        - name: Connexion au GitHub Container Registry
          uses: docker/login-action@v3
          with:
            registry: ${{ env.REGISTRY }}
            username: ${{ github.actor }}
            password: ${{ secrets.GITHUB_TOKEN }}    <- Automatique dans GitHub Actions

        - name: Extraire les métadonnées pour les tags Docker
          id: meta
          uses: docker/metadata-action@v5
          with:
            images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
            tags: |
              # Tag "latest" sur main
              type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
              # Tag "develop" sur develop
              type=raw,value=develop,enable=${{ github.ref == 'refs/heads/develop' }}
              # Tag avec le SHA du commit (ex: sha-abc1234)
              type=sha,prefix=sha-
              # Tag avec la version de la release (ex: 1.2.0)
              type=semver,pattern={{version}}
              # Tag avec le numéro de build (ex: build-42)
              type=raw,value=build-${{ github.run_number }}

        - name: Configurer le cache Docker BuildKit
          uses: docker/setup-buildx-action@v3

        - name: Build et Push l'image Docker
          id: build
          uses: docker/build-push-action@v5
          with:
            context: .
            file: docker/Dockerfile
            target: production
            push: true
            tags: ${{ steps.meta.outputs.tags }}
            labels: ${{ steps.meta.outputs.labels }}
            # Cache entre les builds pour accélérer
            cache-from: type=gha
            cache-to: type=gha,mode=max
            build-args: |
              APP_VERSION=${{ github.run_number }}
              BUILD_DATE=${{ github.event.head_commit.timestamp }}
              GIT_COMMIT=${{ github.sha }}

        - name: Résumé du build
          run: |
            echo "## [DOCKER] Image Docker publiée" >> $GITHUB_STEP_SUMMARY
            echo "" >> $GITHUB_STEP_SUMMARY
            echo "**Tags:**" >> $GITHUB_STEP_SUMMARY
            echo "${{ steps.meta.outputs.tags }}" | tr ',' '\n' | \
              sed 's/^/- /' >> $GITHUB_STEP_SUMMARY
            echo "" >> $GITHUB_STEP_SUMMARY
            echo "**Digest:** ${{ steps.build.outputs.digest }}" >> $GITHUB_STEP_SUMMARY

    # ══════════════════════════════════════════════════════════════════
    # JOB 4: Déploiement en production (uniquement lors d'une release)
    # ══════════════════════════════════════════════════════════════════
    deploy-production:
      name: Déploiement Production
      runs-on: ubuntu-latest
      needs: build-and-push
      if: github.event_name == 'release'

      environment:
        name: production
        url: https://taskmanager.equipe.com/health    <- URL visible dans GitHub

      steps:
        - name: Déployer via SSH
          uses: appleboy/ssh-action@master
          with:
            host:        ${{ secrets.PROD_SERVER_HOST }}
            username:    ${{ secrets.PROD_SERVER_USER }}
            key:         ${{ secrets.PROD_SERVER_SSH_KEY }}
            script: |
              # Aller dans le dossier du projet
              cd /opt/taskmanager

              # Récupérer la nouvelle image
              echo ${{ secrets.GITHUB_TOKEN }} | \
                docker login ghcr.io -u ${{ github.actor }} --password-stdin

              docker pull ghcr.io/equipe/taskmanager-api:${{ github.event.release.tag_name }}

              # Mettre à jour la version
              export APP_VERSION=${{ github.event.release.tag_name }}

              # Déploiement sans interruption
              docker-compose -f docker-compose.prod.yml up -d --no-deps api

              # Vérification post-déploiement
              sleep 10
              STATUS=$(curl -sf http://localhost/api/health | python3 -c \
                "import sys,json; print(json.load(sys.stdin)['status'])")

              if [ "$STATUS" = "healthy" ]; then
                echo "[OK] Déploiement réussi! Version: $APP_VERSION"
              else
                echo "[X] Déploiement échoué! Rollback..."
                docker-compose -f docker-compose.prod.yml up -d --no-deps \
                  -e APP_VERSION=previous api
                exit 1
              fi

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 5.3 - Visualiser la pipeline dans GitHub
────────────────────────────────────────────────────────────────────────────────

INTERFACE WEB GitHub:

  Onglet "Actions" du dépôt après un push sur main:

  ┌──────────────────────────────────────────────────────────────────────┐
  │  CI/CD - Tests + Docker + Déploiement            [OK] Success          │
  │  feat: add Pillow for image processing                               │
  │  main  ·  3 minutes ago  ·  2m 48s                                  │
  │                                                                      │
  │  Jobs:                                                               │
  │  [OK] Tests Python dans Docker        (45s)                           │
  │     -> 14 passed, coverage 95%                                        │
  │                                                                      │
  │  [OK] Analyse SonarQube               (38s)                           │
  │     -> Quality Gate PASSED                                            │
  │                                                                      │
  │  [OK] Build et Push image Docker      (1m 25s)                        │
  │     -> ghcr.io/equipe/taskmanager-api:latest                         │
  │     -> ghcr.io/equipe/taskmanager-api:sha-abc1234                    │
  │     -> ghcr.io/equipe/taskmanager-api:build-42                       │
  └──────────────────────────────────────────────────────────────────────┘


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PARTIE 6 - COMMANDES DOCKER AVANCÉES UTILES POUR L'ÉQUIPE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

────────────────────────────────────────────────────────────────────────────────
INSPECTER ET DÉBOGUER
────────────────────────────────────────────────────────────────────────────────

  # Voir les logs d'un conteneur (temps réel):
  docker-compose logs -f api
  docker-compose logs -f api --tail=100   <- 100 dernières lignes seulement

  # Entrer dans un conteneur en cours d'exécution:
  docker-compose exec api bash            <- Shell bash
  docker-compose exec api sh              <- Shell sh (si pas de bash)
  docker-compose exec postgres psql -U taskmanager  <- Shell PostgreSQL
  docker-compose exec redis redis-cli     <- Shell Redis

  # Lancer une commande ponctuelle sans rester dans le conteneur:
  docker-compose exec api python -c "from app import create_app; print('OK')"
  docker-compose exec api pip list        <- Lister les packages installés
  docker-compose exec api env             <- Variables d'environnement

  # Inspecter un conteneur (réseau, volumes, config):
  docker inspect taskmanager-api-dev      <- Informations détaillées en JSON
  docker inspect taskmanager-api-dev | grep -A5 '"Networks"'

  # Voir les ressources utilisées:
  docker stats                            <- CPU/RAM en temps réel (tous les conteneurs)
  docker stats taskmanager-api-dev        <- Seulement l'API

  # Voir les logs d'un conteneur qui a crashé (il n'est plus là):
  docker logs taskmanager-api-dev         <- Même si le conteneur est arrêté

────────────────────────────────────────────────────────────────────────────────
GÉRER LES IMAGES
────────────────────────────────────────────────────────────────────────────────

  # Lister les images locales:
  docker images
  docker images | grep taskmanager        <- Filtrer par nom

  # Supprimer une image spécifique:
  docker rmi ghcr.io/equipe/taskmanager-api:1.1.0

  # Supprimer toutes les images non utilisées (nettoyage):
  docker image prune

  # Supprimer TOUT ce qui n'est pas utilisé (dangereux!):
  docker system prune -a

  # Voir la taille occupée par Docker:
  docker system df

  EXEMPLE DE SORTIE:
  ──────────────────
  TYPE            TOTAL   ACTIVE  SIZE      RECLAIMABLE
  Images          12      4       2.34GB    1.87GB (80%)
  Containers      4       4       1.2MB     0B (0%)
  Local Volumes   3       3       456MB     0B (0%)
  Build Cache     24      0       312MB     312MB

────────────────────────────────────────────────────────────────────────────────
GÉRER LES VOLUMES (DONNÉES PERSISTÉES)
────────────────────────────────────────────────────────────────────────────────

  # Lister les volumes:
  docker volume ls
  docker volume ls | grep taskmanager

  # Inspecter un volume (voir où il est stocké sur la machine):
  docker volume inspect taskmanager_postgres_data

  EXEMPLE DE SORTIE:
  ──────────────────
  [
    {
      "Name": "taskmanager_postgres_data",
      "Driver": "local",
      "Mountpoint": "/var/lib/docker/volumes/taskmanager_postgres_data/_data",
      "Labels": null
    }
  ]

  # Sauvegarder une base de données PostgreSQL depuis le conteneur:
  docker-compose exec postgres pg_dump \
    -U taskmanager \
    -d taskmanager \
    > backup_$(date +%Y%m%d_%H%M%S).sql

  # Restaurer une sauvegarde:
  docker-compose exec -T postgres psql \
    -U taskmanager \
    -d taskmanager \
    < backup_20240115_143022.sql

  # Supprimer un volume (DONNÉES PERDUES DÉFINITIVEMENT!):
  docker volume rm taskmanager_postgres_data

────────────────────────────────────────────────────────────────────────────────
OPTIMISER LES BUILDS
────────────────────────────────────────────────────────────────────────────────

  # Voir l'historique des couches d'une image (debug du cache):
  docker history ghcr.io/equipe/taskmanager-api:latest

  EXEMPLE DE SORTIE:
  ──────────────────
  IMAGE          CREATED BY                                      SIZE
  f8a2c1d4e5b6   CMD ["gunicorn", "--workers", "4"...]          0B
  <missing>      COPY app/ config.py run.py                     45.2kB
  <missing>      COPY --from=builder /root/.local               87.3MB
  <missing>      USER appuser                                    0B
  <missing>      COPY scripts/entrypoint.sh /entrypoint.sh      1.2kB
  <missing>      ENV PYTHONDONTWRITEBYTECODE=1 ...              0B
  <missing>      WORKDIR /app                                    0B
  <missing>      RUN groupadd --gid 1001 appgroup...            3.4MB
  <missing>      FROM python:3.11-slim                          125.1MB

  # Mesurer le temps de build avec et sans cache:
  time docker-compose build --no-cache api   <- Sans cache (premier build)
  time docker-compose build api              <- Avec cache (builds suivants)

  # Forcer le rebuild d'une étape spécifique:
  docker-compose build --no-cache api       <- Rebuild complet
  docker-compose build api                  <- Utilise le cache

────────────────────────────────────────────────────────────────────────────────
RÉSEAU DOCKER (COMMUNICATION ENTRE SERVICES)
────────────────────────────────────────────────────────────────────────────────

  # Lister les réseaux Docker:
  docker network ls

  # Inspecter le réseau de l'application:
  docker network inspect taskmanager-network

  # Tester la connectivité depuis un conteneur vers un autre:
  docker-compose exec api ping postgres     <- Ping PostgreSQL par nom de service
  docker-compose exec api ping redis        <- Ping Redis par nom de service

  # Les conteneurs se parlent par leur NOM DE SERVICE (pas localhost!):
  # [OK] DATABASE_URL=postgresql://taskmanager:pass@postgres:5432/taskmanager
  # [X] DATABASE_URL=postgresql://taskmanager:pass@localhost:5432/taskmanager
  #                                                    ^ FAUX! Pas localhost!


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PARTIE 7 - INTÉGRATION DOCKER + SONARQUBE + ELK (STACK COMPLÈTE)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

L'équipe utilise maintenant SonarQube + ELK + Docker ensemble.
Voici comment les trois outils s'intègrent.

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 7.1 - Docker-compose qui lance TOUT (app + ELK + SonarQube)
────────────────────────────────────────────────────────────────────────────────

Alice crée un docker-compose.full.yml pour avoir tout en local:

  COMMANDE:
  ─────────
  nano docker-compose.full.yml

  CONTENU:
  ────────

  # ════════════════════════════════════════════════════════════════════
  # DOCKER-COMPOSE.FULL.YML - Stack complète pour développement
  # App Flask + PostgreSQL + Redis + ELK + SonarQube
  # Usage: docker-compose -f docker-compose.full.yml up -d
  # ════════════════════════════════════════════════════════════════════

  version: '3.9'

  services:

    # ── Application Flask ────────────────────────────────────────────
    api:
      build:
        context: .
        dockerfile: docker/Dockerfile.dev
      container_name: taskmanager-api
      env_file: .env
      environment:
        - FLASK_ENV=development
        - DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}
        - REDIS_URL=redis://redis:6379/0
        - LOGSTASH_HOST=logstash
        - LOGSTASH_PORT=5000
        - RUN_MIGRATIONS=true
      ports:
        - "5000:5000"
      volumes:
        - .:/app
        - ./logs:/app/logs
      depends_on:
        postgres:
          condition: service_healthy
        redis:
          condition: service_healthy
        logstash:
          condition: service_started
      networks:
        - taskmanager-network
      restart: unless-stopped

    # ── PostgreSQL ───────────────────────────────────────────────────
    postgres:
      image: postgres:15-alpine
      container_name: taskmanager-postgres
      environment:
        POSTGRES_DB:       ${POSTGRES_DB:-taskmanager}
        POSTGRES_USER:     ${POSTGRES_USER:-taskmanager}
        POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-secret}
      volumes:
        - postgres_data:/var/lib/postgresql/data
      ports:
        - "5432:5432"
      networks:
        - taskmanager-network
      healthcheck:
        test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-taskmanager}"]
        interval: 10s
        timeout: 5s
        retries: 5
      restart: unless-stopped

    # ── Redis ────────────────────────────────────────────────────────
    redis:
      image: redis:7-alpine
      container_name: taskmanager-redis
      ports:
        - "6379:6379"
      volumes:
        - redis_data:/data
      networks:
        - taskmanager-network
      healthcheck:
        test: ["CMD", "redis-cli", "ping"]
        interval: 10s
        timeout: 5s
        retries: 5
      restart: unless-stopped

    # ── Elasticsearch (pour ELK) ─────────────────────────────────────
    elasticsearch:
      image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0
      container_name: taskmanager-elasticsearch
      environment:
        - discovery.type=single-node
        - xpack.security.enabled=false
        - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
      volumes:
        - elasticsearch_data:/usr/share/elasticsearch/data
      ports:
        - "9200:9200"
      networks:
        - taskmanager-network
      healthcheck:
        test: ["CMD-SHELL", "curl -sf http://localhost:9200/_cluster/health"]
        interval: 30s
        timeout: 10s
        retries: 5
        start_period: 60s
      restart: unless-stopped

    # ── Logstash ─────────────────────────────────────────────────────
    logstash:
      image: docker.elastic.co/logstash/logstash:8.11.0
      container_name: taskmanager-logstash
      volumes:
        - ./docker/logstash/pipeline:/usr/share/logstash/pipeline:ro
        - ./logs:/logs:ro
      ports:
        - "5001:5000"     <- Port 5001 sur l'hôte (5000 est pris par Flask)
      environment:
        LS_JAVA_OPTS: "-Xmx256m -Xms256m"
      networks:
        - taskmanager-network
      depends_on:
        elasticsearch:
          condition: service_healthy
      restart: unless-stopped

    # ── Kibana ───────────────────────────────────────────────────────
    kibana:
      image: docker.elastic.co/kibana/kibana:8.11.0
      container_name: taskmanager-kibana
      ports:
        - "5601:5601"
      environment:
        ELASTICSEARCH_HOSTS: '["http://elasticsearch:9200"]'
      networks:
        - taskmanager-network
      depends_on:
        elasticsearch:
          condition: service_healthy
      restart: unless-stopped

    # ── SonarQube ────────────────────────────────────────────────────
    sonarqube:
      image: sonarqube:lts-community
      container_name: taskmanager-sonarqube
      environment:
        - SONAR_JDBC_URL=jdbc:postgresql://postgres:5432/sonarqube
        - SONAR_JDBC_USERNAME=sonarqube
        - SONAR_JDBC_PASSWORD=sonarqube_secret
      volumes:
        - sonarqube_data:/opt/sonarqube/data
        - sonarqube_logs:/opt/sonarqube/logs
        - sonarqube_extensions:/opt/sonarqube/extensions
      ports:
        - "9000:9000"
      networks:
        - taskmanager-network
      depends_on:
        postgres:
          condition: service_healthy
      restart: unless-stopped

  volumes:
    postgres_data:
    redis_data:
    elasticsearch_data:
    sonarqube_data:
    sonarqube_logs:
    sonarqube_extensions:

  networks:
    taskmanager-network:
      driver: bridge

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 7.2 - Script de démarrage de la stack complète
────────────────────────────────────────────────────────────────────────────────

  COMMANDE:
  ─────────
  nano start-full-stack.sh

  CONTENU:
  ────────

  #!/bin/bash
  # start-full-stack.sh - Lance TOUTE la stack (App + ELK + SonarQube)

  set -e

  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
  echo "  TaskManager - Stack Complète (App + ELK + SonarQube)"
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"

  # Vérifier que .env existe
  if [ ! -f .env ]; then
      echo "[X] Fichier .env manquant. Copier .env.example et le remplir."
      echo "   cp .env.example .env && nano .env"
      exit 1
  fi

  echo "[OUTIL] Construction des images..."
  docker-compose -f docker-compose.full.yml build api

  echo "[RAPIDE] Démarrage de tous les services..."
  docker-compose -f docker-compose.full.yml up -d

  echo ""
  echo "[HOURGLASS_WITH_FLOWING_SAND] Attente que tous les services soient prêts..."
  sleep 30

  # Vérifications
  echo ""
  echo "[RECHERCHE] Vérification des services..."

  check_service() {
      local name=$1
      local url=$2
      if curl -sf "$url" > /dev/null 2>&1; then
          echo "  [OK] $name"
      else
          echo "  [ATTENTION]  $name (pas encore prêt, attendre 1-2 minutes)"
      fi
  }

  check_service "Flask API"      "http://localhost:5000/health"
  check_service "Elasticsearch"  "http://localhost:9200/_cluster/health"
  check_service "Kibana"         "http://localhost:5601"
  check_service "SonarQube"      "http://localhost:9000/api/system/status"
  check_service "pgAdmin"        "http://localhost:5050"

  echo ""
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
  echo "  [LISTE] URLS DE L'ÉQUIPE"
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
  echo "  [WEB] API Flask:      http://localhost:5000"
  echo "  [HEAVY_BLACK_HEART]  Health Check:  http://localhost:5000/health"
  echo "  [GRAPHIQUE] Kibana:         http://localhost:5601"
  echo "  [RECHERCHE] SonarQube:      http://localhost:9000  (admin/admin)"
  echo "  [POSTGRES] pgAdmin:        http://localhost:5050"
  echo "  - Elasticsearch:  http://localhost:9200"
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"

  COMMANDE:
  ─────────
  chmod +x start-full-stack.sh

  # Utilisation par l'équipe le matin:
  ./start-full-stack.sh


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PARTIE 8 - BONNES PRATIQUES DE L'ÉQUIPE (RÈGLES D'OR DOCKER)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

────────────────────────────────────────────────────────────────────────────────
RÈGLE 1 - Jamais de secrets dans une image Docker
────────────────────────────────────────────────────────────────────────────────

  # [X] JAMAIS FAIRE: hardcoder un secret dans le Dockerfile
  ENV DATABASE_PASSWORD=mon-mot-de-passe-secret   <- VISIBLE DANS L'IMAGE!
  COPY .env /app/.env                             <- .env dans l'image = DANGER!

  # [OK] TOUJOURS FAIRE: passer les secrets via variables d'environnement
  # Dans docker-compose.yml:
  env_file:
    - .env                  <- Lu depuis la machine hôte, jamais dans l'image
  environment:
    - DATABASE_PASSWORD=${DATABASE_PASSWORD}  <- Depuis l'environnement hôte

  # En production: utiliser Docker Secrets ou HashiCorp Vault
  # (jamais de .env en production!)

────────────────────────────────────────────────────────────────────────────────
RÈGLE 2 - Utiliser des tags précis, jamais "latest" en production
────────────────────────────────────────────────────────────────────────────────

  # [X] En production: latest est imprévisible
  image: postgres:latest           <- Quelle version? Peut changer à tout moment!
  image: python:latest             <- Pareil!

  # [OK] En production: version fixe
  image: postgres:15.4-alpine      <- Version précise et reproductible
  image: python:3.11.8-slim        <- Exactement cette version

  # [OK] Pour les images de l'équipe: tag avec le numéro de release
  image: ghcr.io/equipe/taskmanager-api:1.2.0    <- Déployable, rollback possible

────────────────────────────────────────────────────────────────────────────────
RÈGLE 3 - Toujours avoir un .dockerignore complet
────────────────────────────────────────────────────────────────────────────────

  # Sans .dockerignore:
  # - L'image contient le dossier .git/ (plusieurs centaines de MB!)
  # - L'image contient le venv/ (inutile, les dépendances sont réinstallées)
  # - L'image contient .env avec les mots de passe (FUITE DE SECRETS!)
  # - L'image est beaucoup plus grosse -> build + push + pull plus lents

  # Vérifier ce qui est inclus dans le contexte de build:
  docker build --no-cache -t test . 2>&1 | head -5
  # -> "Sending build context to Docker daemon  X.XXkB"
  # Si X.XX > 10MB: revoir le .dockerignore!

────────────────────────────────────────────────────────────────────────────────
RÈGLE 4 - Utiliser le build multi-stage pour minimiser les images de production
────────────────────────────────────────────────────────────────────────────────

  # Image SANS multi-stage (mauvaise pratique):
  # -> Contient: gcc, make, headers système, cache pip, tests...
  # -> Taille typique: 800MB - 1GB

  # Image AVEC multi-stage (notre Dockerfile):
  # -> Contient UNIQUEMENT: Python, les dépendances, le code
  # -> Taille typique: 150-200MB

  # Comparer:
  docker images | grep taskmanager
  # taskmanager-api  production  187MB  [OK]
  # taskmanager-api  dev         412MB  (normal, a les outils de dev)

────────────────────────────────────────────────────────────────────────────────
RÈGLE 5 - Ne jamais tourner en root dans un conteneur de production
────────────────────────────────────────────────────────────────────────────────

  # [X] DANGEREUX: si l'app est compromise, l'attaquant est root dans le conteneur
  FROM python:3.11-slim
  # ... aucun changement d'utilisateur
  CMD ["gunicorn", "run:app"]   <- Tourne en root!

  # [OK] SÉCURISÉ: créer et utiliser un utilisateur non-root
  RUN groupadd --gid 1001 appgroup && \
      useradd --uid 1001 --gid appgroup appuser
  USER appuser                  <- Tout ce qui suit tourne en non-root
  CMD ["gunicorn", "run:app"]   <- Tourne en appuser (uid 1001)

  # Vérifier l'utilisateur actif:
  docker-compose exec api whoami
  # -> appuser  [OK]  (et non root)

────────────────────────────────────────────────────────────────────────────────
RÈGLE 6 - Toujours avoir un health check sur les services critiques
────────────────────────────────────────────────────────────────────────────────

  # Sans health check: Docker ne sait pas si le service est vraiment prêt
  # -> "Up" ne signifie pas "fonctionnel"!

  # [OK] Avec health check:
  healthcheck:
    test: ["CMD-SHELL", "pg_isready -U taskmanager"]
    interval: 10s     <- Vérifier toutes les 10 secondes
    timeout: 5s       <- Timeout de 5 secondes
    retries: 5        <- 5 échecs = "unhealthy"
    start_period: 30s <- Ignorer les échecs pendant 30s au démarrage

  # depends_on avec condition: attendre que le health check passe
  depends_on:
    postgres:
      condition: service_healthy   <- Meilleur que juste "service_started"!


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PARTIE 9 - RÉSUMÉ VISUEL - QUI FAIT QUOI ET QUAND (DOCKER)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  ALICE (Lead / Responsable Docker)
  ───────────────────────────────────
  [OK] Crée et maintient les Dockerfiles (dev/test/prod) - une fois
  [OK] Crée et maintient les docker-compose.yml - une fois, évolue
  [OK] Configure la pipeline CI/CD avec build et push automatiques
  [OK] Gère le GitHub Container Registry (images publiées)
  [OK] Effectue les déploiements en production (via pipeline ou SSH)
  [OK] Met à jour les images de base (python:3.11, postgres:15) régulièrement
  [OK] Décide de la stratégie de versionnage des images (semver)

  BOB et CLAIRE (Développeurs)
  ─────────────────────────────
  [OK] Installent Docker une seule fois
  [OK] Lancent docker-compose up -d le matin, down le soir
  [OK] Ajoutent leurs dépendances dans requirements.txt (puis rebuild)
  [OK] Utilisent docker-compose exec api pour déboguer
  [OK] Lancent ./test.sh avant chaque push
  [OK] Ne créent JAMAIS d'images ou de Dockerfile sans en parler à Alice


  WORKFLOW COMPLET D'UNE FEATURE AVEC DOCKER:
  ─────────────────────────────────────────────

  Matin              Dev en cours                Fin de feature
  ──────             ────────────                ──────────────
  docker-compose     [écrire code]               ./test.sh
  up -d              [modifier routes.py]         v
  v                  (hot-reload détecte)        docker-compose
  Tous les           [voir changement sur]       exec api pytest
  services prêts     localhost:5000              v
  en 30 secondes!    instantanément              git push
                                                 v
                                                 GitHub Actions:
                                                 tests + sonar +
                                                 build Docker +
                                                 push registry [OK]


  ARCHITECTURE COMPLÈTE DE L'ÉQUIPE:
  ────────────────────────────────────

  Poste Dev (Alice/Bob/Claire)         Serveur partagé / CI
  ────────────────────────────         ────────────────────
  docker-compose up
    ┌─────────────────────┐            GitHub Actions:
    │  Flask :5000        │            - Tests Docker
    │  (hot-reload dev)   │            - SonarQube scan
    └──────────┬──────────┘            - Build image prod
               │                       - Push ghcr.io
    ┌──────────[BLACK_DOWN-POINTING_TRIANGLE]──────────┐            - Deploy prod
    │  PostgreSQL :5432   │
    └─────────────────────┘            Production:
    ┌─────────────────────┐              Nginx
    │  Redis :6379        │               v
    └─────────────────────┘            Flask (Gunicorn)
    ┌─────────────────────┐               v
    │  Elasticsearch :9200│            PostgreSQL
    │  Logstash :5001     │            Redis
    │  Kibana :5601       │            (tous dans Docker)
    └─────────────────────┘
    ┌─────────────────────┐
    │  SonarQube :9000    │
    └─────────────────────┘


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PARTIE 10 - CHEATSHEET DOCKER (RÉFÉRENCE RAPIDE ÉQUIPE)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  COMMANDES QUOTIDIENNES:
  ───────────────────────

  docker-compose up -d               # Démarrer tous les services
  docker-compose down                # Arrêter (données conservées)
  docker-compose down -v             # Arrêter ET supprimer les données
  docker-compose ps                  # État des services
  docker-compose logs -f api         # Logs Flask en temps réel
  docker-compose exec api bash       # Shell dans le conteneur Flask
  docker-compose restart api         # Redémarrer seulement Flask
  docker-compose build api           # Rebuild l'image Flask
  docker-compose up -d --build       # Rebuild ET relancer

  COMMANDES DE BUILD ET REGISTRY:
  ─────────────────────────────────

  # Build image de production:
  docker build -f docker/Dockerfile --target production -t taskmanager:1.2.0 .

  # Voir la taille de l'image:
  docker images taskmanager

  # Publier sur GitHub Registry:
  docker login ghcr.io -u alice
  docker push ghcr.io/equipe/taskmanager-api:1.2.0

  # Télécharger une image:
  docker pull ghcr.io/equipe/taskmanager-api:1.2.0

  COMMANDES DE DÉBOGAGE:
  ──────────────────────

  docker stats                       # CPU/RAM en temps réel
  docker system df                   # Espace disque Docker
  docker inspect taskmanager-api-dev # Info détaillée du conteneur
  docker inspect taskmanager-network # Info détaillée du réseau
  docker volume inspect postgres_data # Où sont stockées les données

  COMMANDES DE NETTOYAGE:
  ────────────────────────

  docker image prune                 # Supprimer les images non utilisées
  docker container prune             # Supprimer les conteneurs arrêtés
  docker volume prune                # Supprimer les volumes non utilisés
  docker system prune                # Tout nettoyer (images+conteneurs)
  docker system prune -a             # Tout nettoyer y compris images utiles
                                     # [ATTENTION] ATTENTION: à utiliser avec précaution!

  SAUVEGARDE / RESTAURATION DB:
  ──────────────────────────────

  # Sauvegarder PostgreSQL:
  docker-compose exec postgres pg_dump -U taskmanager taskmanager > backup.sql

  # Restaurer PostgreSQL:
  docker-compose exec -T postgres psql -U taskmanager taskmanager < backup.sql

  # Sauvegarder un volume entier:
  docker run --rm -v taskmanager_postgres_data:/data -v $(pwd):/backup \
    alpine tar czf /backup/postgres_backup.tar.gz /data

  ALIAS PRATIQUES (à ajouter dans ~/.bashrc ou ~/.zshrc):
  ────────────────────────────────────────────────────────

  alias dc='docker-compose'
  alias dcu='docker-compose up -d'
  alias dcd='docker-compose down'
  alias dcl='docker-compose logs -f'
  alias dcb='docker-compose build'
  alias dce='docker-compose exec'
  alias dcp='docker-compose ps'

  # Utilisation:
  dcu              # docker-compose up -d
  dcl api          # docker-compose logs -f api
  dce api bash     # docker-compose exec api bash
  dcp              # docker-compose ps

  URLS EN DÉVELOPPEMENT LOCAL:
  ─────────────────────────────

  Flask API:       http://localhost:5000
  Health check:    http://localhost:5000/health
  pgAdmin:         http://localhost:5050
  Kibana:          http://localhost:5601
  SonarQube:       http://localhost:9000  (admin/admin au premier lancement)
  Elasticsearch:   http://localhost:9200
  PostgreSQL:      localhost:5432  (via DBeaver, pgAdmin, etc.)
  Redis:           localhost:6379  (via redis-cli, RedisInsight, etc.)

  ERREURS FRÉQUENTES ET SOLUTIONS:
  ──────────────────────────────────

  "Port already in use":
  -> Quelque chose utilise déjà ce port sur votre machine
  -> Solution: lsof -i :5000 | kill $(lsof -t -i:5000)
  -> Ou changer le port dans .env: API_PORT=5001

  "Cannot connect to the Docker daemon":
  -> Docker Desktop n'est pas lancé (Mac/Windows)
  -> Solution: lancer Docker Desktop

  "standard_init_linux.go: exec user process caused: no such file or directory":
  -> Fichier .sh avec fins de ligne Windows (CRLF au lieu de LF)
  -> Solution: dos2unix scripts/entrypoint.sh

  "image operating system linux does not match host platform windows/amd64":
  -> Problème de plateforme sur Windows avec puce ARM
  -> Solution: ajouter dans docker-compose.yml:
     platform: linux/amd64

================================================================================
FIN DE LA SECTION - DOCKER EN ÉQUIPE DE 3 DÉVELOPPEURS (FLASK/PYTHON)
================================================================================