---
jupytext:
  text_representation:
    extension: .md
    format_name: myst
    format_version: 0.13
    jupytext_version: 1.16.0
kernelspec:
  name: python3
  display_name: Python 3
---

# Docker Compose — Orchestrer plusieurs conteneurs

```{code-cell} python
:tags: [hide-input]
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import matplotlib.patheffects as pe
from matplotlib.patches import FancyArrowPatch, FancyBboxPatch, Arc
import numpy as np
import pandas as pd
import seaborn as sns
import json
import re

sns.set_theme(style="whitegrid", palette="muted")
plt.rcParams.update({
    "figure.dpi": 120,
    "font.family": "sans-serif",
    "axes.spines.top": False,
    "axes.spines.right": False,
})
```

## Pourquoi Docker Compose ?

Imaginez que vous construisez une application web moderne. Vous avez besoin :

- d'un **serveur web** (nginx) qui reçoit les requêtes HTTP
- d'une **application** (Flask, Django, Node.js…) qui traite la logique métier
- d'une **base de données** (PostgreSQL) qui stocke les données
- d'un **cache** (Redis) qui accélère les lectures fréquentes

Sans Docker Compose, vous devriez lancer chaque conteneur à la main, créer les réseaux manuellement, gérer les variables d'environnement, vous souvenir des dizaines d'options `docker run`… C'est fastidieux et source d'erreurs.

**Docker Compose** résout ce problème : vous décrivez toute votre stack dans un fichier `compose.yml`, et une seule commande lance tout l'ensemble.

```{admonition} Analogie — La recette de cuisine
:class: tip
Docker Compose, c'est comme une recette de cuisine pour un repas complet. Plutôt que d'avoir des instructions séparées pour l'entrée, le plat et le dessert, la recette décrit les ingrédients de chaque plat, l'ordre de préparation et comment les servir ensemble. Le fichier `compose.yml` est votre recette ; `docker compose up` est le moment où vous mettez tout au four.
```

## Le fichier compose.yml

Le fichier `compose.yml` (anciennement `docker-compose.yml`) utilise le format YAML. Voici sa structure générale :

```yaml
# compose.yml — Structure générale annotée
name: mon-projet          # Nom du projet (préfixe des ressources créées)

services:                 # Les conteneurs de votre application
  web:                    # Nom du service
    image: nginx:alpine   # Image à utiliser
    ports:
      - "80:80"           # hôte:conteneur

  app:
    build: .              # Construire depuis le Dockerfile local
    environment:
      DATABASE_URL: postgres://db/myapp

  db:
    image: postgres:16
    volumes:
      - pgdata:/var/lib/postgresql/data  # Volume nommé

volumes:                  # Volumes nommés déclarés ici
  pgdata:

networks:                 # Réseaux personnalisés (optionnel)
  frontend:
  backend:
```

### Les instructions clés des services

Voici les instructions les plus importantes que vous rencontrerez dans un `compose.yml` :

| Instruction | Rôle | Exemple |
|---|---|---|
| `image` | Image Docker à utiliser | `postgres:16-alpine` |
| `build` | Construire depuis un Dockerfile | `build: ./app` |
| `ports` | Publier des ports `hôte:conteneur` | `"8080:80"` |
| `volumes` | Monter volumes ou répertoires | `./data:/app/data` |
| `environment` | Variables d'environnement | `DEBUG: "true"` |
| `env_file` | Charger depuis un fichier `.env` | `env_file: .env` |
| `depends_on` | Ordre de démarrage / healthcheck | voir ci-dessous |
| `healthcheck` | Vérifier si le service est prêt | voir ci-dessous |
| `restart` | Politique de redémarrage | `unless-stopped` |
| `profiles` | Grouper des services optionnels | `profiles: [debug]` |
| `deploy` | Ressources et réplicas (Swarm/Compose) | `replicas: 3` |
| `networks` | Rattacher à des réseaux | `[frontend, backend]` |

### depends_on avec condition service_healthy

Un problème classique : votre application Flask essaie de se connecter à PostgreSQL avant que PostgreSQL ne soit prêt. `depends_on` avec `condition: service_healthy` résout ça proprement :

```yaml
services:
  app:
    image: myapp:latest
    depends_on:
      db:
        condition: service_healthy   # Attend que db soit "healthy"
      redis:
        condition: service_started   # Attend juste que redis soit démarré

  db:
    image: postgres:16
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s      # Vérifier toutes les 5 secondes
      timeout: 5s       # Timeout par vérification
      retries: 5        # Nombre de tentatives avant "unhealthy"
      start_period: 10s # Délai avant la première vérification
```

```{admonition} Les trois conditions de depends_on
:class: note
- **`service_started`** : le conteneur a été lancé (mais peut ne pas être prêt)
- **`service_healthy`** : le healthcheck retourne succès — le service est vraiment opérationnel
- **`service_completed_successfully`** : pour les tâches (migrations, seeds) qui doivent finir avant d'autres services
```

## Réseaux dans Compose

### Le réseau par défaut

Docker Compose crée automatiquement un réseau pour votre projet. Tous les services qui n'ont pas de configuration réseau explicite y sont attachés et peuvent se contacter **par leur nom de service** :

```yaml
services:
  app:
    image: myapp
    # app peut joindre "db" via http://db:5432

  db:
    image: postgres:16
```

À l'intérieur du conteneur `app`, l'hôte `db` résout automatiquement vers le conteneur PostgreSQL. C'est la magie du DNS interne de Docker.

### Réseaux nommés et isolation

Pour des architectures plus complexes, vous pouvez créer plusieurs réseaux et contrôler quels services peuvent se voir :

```yaml
services:
  nginx:
    image: nginx:alpine
    networks:
      - frontend        # nginx peut parler à "app"
      - public          # nginx est accessible de l'extérieur

  app:
    image: myapp
    networks:
      - frontend        # app peut parler à nginx et db
      - backend

  db:
    image: postgres:16
    networks:
      - backend         # db n'est accessible QUE depuis app — pas depuis nginx

networks:
  frontend:
  backend:
  public:
```

## Volumes dans Compose

### Volume nommé vs bind mount

```yaml
services:
  db:
    image: postgres:16
    volumes:
      # Volume nommé : Docker gère l'emplacement (recommandé pour les données)
      - pgdata:/var/lib/postgresql/data

  app:
    image: myapp
    volumes:
      # Bind mount : vous liez un dossier de l'hôte (recommandé pour le dev)
      - ./src:/app/src
      # Volume nommé en lecture seule (sécurité)
      - config:/etc/app:ro

volumes:
  pgdata:        # Docker choisit /var/lib/docker/volumes/pgdata/
  config:
```

```{admonition} Quand utiliser quoi ?
:class: tip
**Volumes nommés** → données persistantes (bases de données, uploads). Docker les gère, ils survivent aux `docker compose down`.

**Bind mounts** → développement local : vos modifications de code sont immédiatement visibles dans le conteneur sans rebuild.

**`docker compose down -v`** supprime aussi les volumes nommés — attention aux données !
```

## Profiles : services optionnels

Les profiles permettent de démarrer seulement certains services selon le contexte :

```yaml
services:
  app:
    image: myapp         # Pas de profile = toujours démarré

  db:
    image: postgres:16   # Pas de profile = toujours démarré

  adminer:               # Interface web pour la DB
    image: adminer
    profiles: [tools]    # Démarré seulement avec --profile tools

  mailhog:               # Capture des emails en dev
    image: mailhog/mailhog
    profiles: [dev]

  prometheus:
    image: prom/prometheus
    profiles: [monitoring]
```

```bash
# Démarrer uniquement app + db
docker compose up

# Démarrer avec les outils de dev
docker compose --profile dev up

# Démarrer avec monitoring
docker compose --profile monitoring up

# Tout démarrer (tous les profiles)
docker compose --profile dev --profile tools --profile monitoring up
```

## Les commandes Compose essentielles

```bash
# Démarrer tous les services (en arrière-plan avec -d)
docker compose up -d

# Démarrer en reconstruisant les images
docker compose up -d --build

# Arrêter et supprimer les conteneurs (garder les volumes)
docker compose down

# Arrêter, supprimer conteneurs ET volumes nommés
docker compose down -v

# Voir l'état des services
docker compose ps

# Suivre les logs de tous les services
docker compose logs -f

# Logs d'un service spécifique
docker compose logs -f app

# Exécuter une commande dans un service en cours
docker compose exec app bash
docker compose exec db psql -U postgres

# Construire (ou reconstruire) les images
docker compose build

# Télécharger les images sans démarrer
docker compose pull

# Redémarrer un service
docker compose restart app

# Scaler un service (plusieurs réplicas)
docker compose up -d --scale app=3

# Voir la configuration fusionnée (après substitution des variables)
docker compose config
```

## Visualisation : architecture multi-services

```{code-cell} python
:tags: [hide-input]
fig, ax = plt.subplots(1, 1, figsize=(14, 9))
ax.set_xlim(0, 14)
ax.set_ylim(0, 9)
ax.axis("off")
ax.set_facecolor("#f8f9fa")
fig.patch.set_facecolor("#f8f9fa")

# Titre
ax.text(7, 8.5, "Architecture Docker Compose — Stack web complète",
        ha="center", va="center", fontsize=14, fontweight="bold", color="#2c3e50")

# === Réseau frontend ===
frontend_box = FancyBboxPatch((0.3, 4.2), 6.2, 3.8, boxstyle="round,pad=0.15",
                               facecolor="#e8f4f8", edgecolor="#3498db", linewidth=2, linestyle="--")
ax.add_patch(frontend_box)
ax.text(3.4, 7.85, "réseau : frontend", ha="center", va="center",
        fontsize=9, color="#2980b9", style="italic")

# === Réseau backend ===
backend_box = FancyBboxPatch((7.3, 4.2), 6.2, 3.8, boxstyle="round,pad=0.15",
                              facecolor="#fef9e7", edgecolor="#f39c12", linewidth=2, linestyle="--")
ax.add_patch(backend_box)
ax.text(10.4, 7.85, "réseau : backend", ha="center", va="center",
        fontsize=9, color="#e67e22", style="italic")

# === Réseau commun (app est dans les deux) ===
common_box = FancyBboxPatch((5.8, 4.7), 2.5, 2.8, boxstyle="round,pad=0.1",
                             facecolor="#e8f8e8", edgecolor="#27ae60", linewidth=2, linestyle=":")
ax.add_patch(common_box)
ax.text(7.05, 7.35, "app\n(les deux)", ha="center", va="center",
        fontsize=7.5, color="#1e8449", style="italic")

def draw_service(ax, x, y, w, h, name, image, color, icon=""):
    box = FancyBboxPatch((x, y), w, h, boxstyle="round,pad=0.1",
                          facecolor=color, edgecolor="#555", linewidth=1.5)
    ax.add_patch(box)
    ax.text(x + w/2, y + h*0.68, icon + " " + name,
            ha="center", va="center", fontsize=11, fontweight="bold", color="#2c3e50")
    ax.text(x + w/2, y + h*0.3, image,
            ha="center", va="center", fontsize=8.5, color="#555",
            bbox=dict(boxstyle="round,pad=0.2", facecolor="white", edgecolor="#ccc", alpha=0.8))

# Nginx
draw_service(ax, 0.6, 5.0, 2.4, 2.2, "nginx", "nginx:alpine", "#d6eaf8", "🌐")
# App Flask
draw_service(ax, 5.9, 5.0, 2.3, 2.2, "app", "flask:3.0", "#d5f5e3", "🐍")
# PostgreSQL
draw_service(ax, 8.0, 5.0, 2.5, 2.2, "db", "postgres:16", "#fdebd0", "🗄️")
# Redis
draw_service(ax, 10.8, 5.0, 2.3, 2.2, "redis", "redis:7-alpine", "#fadbd8", "⚡")

# Healthcheck badge
for xb, yb, label in [(8.0, 4.7, "✓ healthcheck"), (10.8, 4.7, "✓ healthcheck")]:
    ax.text(xb + 1.25, yb + 0.05, label, ha="center", va="center",
            fontsize=7.5, color="white",
            bbox=dict(boxstyle="round,pad=0.2", facecolor="#27ae60", edgecolor="none"))

# Flèches de dépendances
arrow_style = dict(arrowstyle="-|>", color="#555", lw=1.8,
                   connectionstyle="arc3,rad=0.0")

# nginx -> app
ax.annotate("", xy=(5.9, 6.1), xytext=(3.0, 6.1),
            arrowprops=dict(arrowstyle="-|>", color="#3498db", lw=2))
ax.text(4.45, 6.3, "depends_on", ha="center", fontsize=8, color="#3498db")

# app -> db
ax.annotate("", xy=(8.0, 6.1), xytext=(8.2, 6.1),
            arrowprops=dict(arrowstyle="-|>", color="#f39c12", lw=2))
ax.annotate("", xy=(8.0, 6.1), xytext=(8.25, 6.1),
            arrowprops=dict(arrowstyle="-|>", color="#f39c12", lw=2))
ax.annotate("", xy=(10.8, 6.1), xytext=(8.2, 6.1),
            arrowprops=dict(arrowstyle="-|>", color="#f39c12", lw=2))
ax.text(9.5, 6.3, "depends_on", ha="center", fontsize=8, color="#f39c12")

# app -> redis
ax.annotate("", xy=(10.8, 5.5), xytext=(8.2, 5.5),
            arrowprops=dict(arrowstyle="-|>", color="#e74c3c", lw=2, connectionstyle="arc3,rad=-0.3"))
ax.text(9.4, 5.1, "depends_on", ha="center", fontsize=8, color="#e74c3c")

# Internet -> nginx
ax.annotate("", xy=(0.6, 6.1), xytext=(-0.1 + 0.7, 6.1),
            arrowprops=dict(arrowstyle="-|>", color="#8e44ad", lw=2.5))
ax.text(0.55, 6.4, "Internet\n:80", ha="center", fontsize=8, color="#8e44ad",
        bbox=dict(boxstyle="round,pad=0.2", facecolor="#f9ebff", edgecolor="#8e44ad"))

# Volumes (bas)
ax.text(7, 3.9, "Volumes persistants", ha="center", va="center",
        fontsize=11, fontweight="bold", color="#2c3e50")

for x_v, name_v, color_v in [(3.5, "pgdata\n(postgres data)", "#fdebd0"),
                               (7.0, "redisdata\n(redis AOF/RDB)", "#fadbd8"),
                               (10.5, "uploads\n(fichiers app)", "#d5f5e3")]:
    vol = FancyBboxPatch((x_v - 1.2, 2.7), 2.4, 1.0, boxstyle="round,pad=0.1",
                          facecolor=color_v, edgecolor="#555", linewidth=1.3, linestyle="-")
    ax.add_patch(vol)
    ax.text(x_v, 3.2, name_v, ha="center", va="center", fontsize=8.5, color="#2c3e50")

# Flèches vers volumes
for sx, sy, ex in [(9.25, 5.0, 3.5), (11.95, 5.0, 7.0), (7.05, 5.0, 10.5)]:
    ax.annotate("", xy=(ex, 3.7), xytext=(sx, sy),
                arrowprops=dict(arrowstyle="-", color="#999", lw=1.2, linestyle="dotted"))

# Légende réseau
legend_items = [
    mpatches.Patch(facecolor="#e8f4f8", edgecolor="#3498db", linestyle="--", label="réseau frontend"),
    mpatches.Patch(facecolor="#fef9e7", edgecolor="#f39c12", linestyle="--", label="réseau backend"),
    mpatches.Patch(facecolor="#e8f8e8", edgecolor="#27ae60", linestyle=":", label="app (les deux réseaux)"),
]
ax.legend(handles=legend_items, loc="lower left", fontsize=8.5, framealpha=0.9)

plt.tight_layout()
plt.savefig("compose_architecture.png", dpi=120, bbox_inches="tight")
plt.show()
```

## Visualisation : ordre de démarrage et dépendances

```{code-cell} python
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# --- Graphe de dépendances ---
ax1 = axes[0]
ax1.set_xlim(0, 10)
ax1.set_ylim(0, 7)
ax1.axis("off")
ax1.set_title("Graphe de dépendances des services", fontsize=12, fontweight="bold", pad=10)
ax1.set_facecolor("#f8f9fa")

services_pos = {
    "nginx": (5, 5.5),
    "app": (5, 3.5),
    "db": (2.5, 1.5),
    "redis": (7.5, 1.5),
    "migration": (2.5, 3.5),
}
services_color = {
    "nginx": "#aed6f1",
    "app": "#a9dfbf",
    "db": "#f9e79f",
    "redis": "#f1948a",
    "migration": "#d7bde2",
}

for name, (x, y) in services_pos.items():
    circle = plt.Circle((x, y), 0.7, color=services_color[name], ec="#555", lw=1.5, zorder=3)
    ax1.add_patch(circle)
    ax1.text(x, y, name, ha="center", va="center", fontsize=9.5, fontweight="bold", zorder=4)

# Dépendances : (from, to, condition)
deps = [
    ("nginx", "app", "started"),
    ("app", "db", "healthy"),
    ("app", "redis", "started"),
    ("app", "migration", "completed"),
    ("migration", "db", "healthy"),
]
cond_colors = {"started": "#3498db", "healthy": "#27ae60", "completed": "#8e44ad"}

for frm, to, cond in deps:
    x1, y1 = services_pos[frm]
    x2, y2 = services_pos[to]
    dx, dy = x2 - x1, y2 - y1
    dist = (dx**2 + dy**2)**0.5
    ux, uy = dx/dist, dy/dist
    ax1.annotate("",
        xy=(x2 - ux*0.72, y2 - uy*0.72),
        xytext=(x1 + ux*0.72, y1 + uy*0.72),
        arrowprops=dict(arrowstyle="-|>", color=cond_colors[cond], lw=2.0))
    mx, my = (x1+x2)/2 + uy*0.25, (y1+y2)/2 - ux*0.25
    ax1.text(mx, my, cond, ha="center", va="center", fontsize=7.5,
             color=cond_colors[cond], fontweight="bold",
             bbox=dict(boxstyle="round,pad=0.15", facecolor="white", edgecolor=cond_colors[cond], alpha=0.85))

legend_dep = [mpatches.Patch(color=c, label=f"condition: service_{l}")
              for l, c in cond_colors.items()]
ax1.legend(handles=legend_dep, loc="upper left", fontsize=8)

# --- Ordre de démarrage ---
ax2 = axes[1]
ax2.set_facecolor("#f8f9fa")
ax2.set_title("Ordre de démarrage (timeline)", fontsize=12, fontweight="bold", pad=10)
ax2.set_xlabel("Temps (secondes)", fontsize=10)
ax2.set_xlim(-0.5, 30)
ax2.set_ylim(-0.5, 5)
ax2.set_yticks([])
ax2.spines["left"].set_visible(False)

timeline_services = ["db", "redis", "migration", "app", "nginx"]
colors_tl = ["#f9e79f", "#f1948a", "#d7bde2", "#a9dfbf", "#aed6f1"]
# (start, healthcheck_end, ready)
timings = [
    (0, 8, 12),     # db
    (0, 2, 3),      # redis
    (12, 14, 15),   # migration (attend db healthy)
    (15, 17, 18),   # app (attend db healthy + migration completed)
    (18, 19, 20),   # nginx (attend app started)
]

for i, (svc, col, (start, hc_end, ready)) in enumerate(zip(timeline_services, colors_tl, timings)):
    y = i + 0.1
    # Démarrage
    ax2.barh(y, hc_end - start, left=start, height=0.7, color=col, edgecolor="#555", lw=1.2, label=svc)
    # Prêt (après healthcheck)
    ax2.barh(y, ready - hc_end, left=hc_end, height=0.7, color=col, edgecolor="#27ae60", lw=2, alpha=0.6, hatch="///")
    ax2.text(start + (ready - start)/2, y + 0.35, svc,
             ha="center", va="center", fontsize=9.5, fontweight="bold", color="#2c3e50")
    # Marqueur "prêt"
    ax2.axvline(x=ready, ymin=(y-0.1)/5, ymax=(y+0.8)/5, color="#27ae60", lw=1.2, ls="--", alpha=0.5)

ax2.axvspan(0, 0.1, alpha=0, label="démarrage")
ax2.text(29, 4.6, "■ démarrage\n⁄ healthcheck OK", ha="right", va="top", fontsize=8, color="#555")
ax2.set_xlabel("Temps (secondes)", fontsize=10)
ax2.spines["bottom"].set_visible(True)

plt.tight_layout()
plt.show()
```

## Exemple complet commenté : stack web

Voici un exemple de stack complète avec nginx, Flask, PostgreSQL et Redis. C'est le type de fichier que vous utiliserez en production :

```yaml
# compose.yml — Stack web production-ready
name: webapp

services:

  # ── Proxy inverse ────────────────────────────────────────────
  nginx:
    image: nginx:1.25-alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro   # Config en lecture seule
      - ./nginx/ssl:/etc/nginx/ssl:ro
      - static_files:/srv/static:ro                   # Fichiers statiques de Flask
    depends_on:
      app:
        condition: service_healthy
    restart: unless-stopped
    networks:
      - frontend

  # ── Application Flask ─────────────────────────────────────────
  app:
    build:
      context: ./app
      dockerfile: Dockerfile
      target: production          # Multi-stage : stage "production"
    env_file:
      - .env                      # Secrets hors du compose.yml
    environment:
      DATABASE_URL: postgresql://appuser:${DB_PASSWORD}@db:5432/appdb
      REDIS_URL: redis://redis:6379/0
      FLASK_ENV: production
    volumes:
      - static_files:/app/static  # Partager les statics avec nginx
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
      migration:
        condition: service_completed_successfully
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 20s
    restart: unless-stopped
    deploy:
      resources:
        limits:
          cpus: "1.0"
          memory: 512M
    networks:
      - frontend
      - backend

  # ── Migrations de base de données ─────────────────────────────
  migration:
    build:
      context: ./app
      target: production
    command: flask db upgrade     # Alembic/Flask-Migrate
    env_file: .env
    environment:
      DATABASE_URL: postgresql://appuser:${DB_PASSWORD}@db:5432/appdb
    depends_on:
      db:
        condition: service_healthy
    restart: "no"                 # Ne redémarre pas : c'est une tâche unique
    networks:
      - backend

  # ── Base de données PostgreSQL ────────────────────────────────
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: appdb
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U appuser -d appdb"]
      interval: 5s
      timeout: 5s
      retries: 10
      start_period: 15s
    restart: unless-stopped
    deploy:
      resources:
        limits:
          memory: 256M
    networks:
      - backend

  # ── Cache Redis ───────────────────────────────────────────────
  redis:
    image: redis:7-alpine
    command: redis-server --appendonly yes --maxmemory 128mb --maxmemory-policy allkeys-lru
    volumes:
      - redisdata:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5
    restart: unless-stopped
    networks:
      - backend

  # ── Outils de développement (profile "dev") ───────────────────
  adminer:
    image: adminer:latest
    ports:
      - "8080:8080"
    profiles: [dev]
    depends_on:
      - db
    networks:
      - backend

  redis-commander:
    image: rediscommander/redis-commander:latest
    environment:
      REDIS_HOSTS: "local:redis:6379"
    ports:
      - "8081:8081"
    profiles: [dev]
    depends_on:
      - redis
    networks:
      - backend

volumes:
  pgdata:
  redisdata:
  static_files:

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge
    internal: true          # Pas d'accès Internet direct depuis le backend
```

```{admonition} La variable ${DB_PASSWORD}
:class: note
Les `${VARIABLE}` dans le compose.yml sont remplacées par les variables d'environnement de votre shell ou du fichier `.env` dans le même répertoire. Ne mettez jamais de mots de passe en dur dans le `compose.yml` — utilisez des variables d'environnement ou les `secrets` Docker Compose.
```

## Code Python : parseur et validateur de compose.yml

Le code suivant simule la lecture et la validation d'un fichier `compose.yml` avec PyYAML, et vérifie qu'il n'y a pas de cycles dans les dépendances (un problème qui bloquerait le démarrage) :

```{code-cell} python
import yaml
import json
from collections import defaultdict, deque

# Simulation d'un compose.yml en mémoire (sans fichier sur disque)
COMPOSE_YAML = """
name: webapp
services:
  nginx:
    image: nginx:1.25-alpine
    depends_on:
      app:
        condition: service_healthy
    networks: [frontend]

  app:
    build: ./app
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
      migration:
        condition: service_completed_successfully
    networks: [frontend, backend]

  migration:
    build: ./app
    depends_on:
      db:
        condition: service_healthy
    networks: [backend]

  db:
    image: postgres:16-alpine
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 10
    networks: [backend]

  redis:
    image: redis:7-alpine
    networks: [backend]

networks:
  frontend:
  backend:
    internal: true
"""

def parse_compose(yaml_str: str) -> dict:
    """Parse un compose.yml et retourne la structure Python."""
    return yaml.safe_load(yaml_str)

def extract_dependencies(compose: dict) -> dict[str, list[str]]:
    """Extrait le graphe de dépendances {service: [dépendances]}."""
    services = compose.get("services", {})
    graph = {}
    for name, cfg in services.items():
        deps = cfg.get("depends_on", {})
        if isinstance(deps, list):
            # Format court : depends_on: [db, redis]
            graph[name] = deps
        elif isinstance(deps, dict):
            # Format long avec conditions
            graph[name] = list(deps.keys())
        else:
            graph[name] = []
    return graph

def topological_sort(graph: dict[str, list[str]]) -> tuple[list[str], bool]:
    """
    Tri topologique (algorithme de Kahn).
    Retourne (ordre_démarrage, cycle_détecté).
    """
    in_degree = {node: 0 for node in graph}
    for node, deps in graph.items():
        for dep in deps:
            if dep in in_degree:
                in_degree[node] = in_degree.get(node, 0)
            # dep → node : dep doit démarrer AVANT node
    # Recalcul correct
    in_degree = defaultdict(int)
    for node in graph:
        in_degree[node] += 0
    # Pour chaque service, ses dépendances doivent le précéder
    reverse_graph = defaultdict(list)
    for node, deps in graph.items():
        for dep in deps:
            reverse_graph[dep].append(node)
            in_degree[node] += 1  # node dépend de dep → in_degree de node augmente

    # Correction : reset et calcul propre
    in_degree = {node: 0 for node in graph}
    for node, deps in graph.items():
        for dep in deps:
            in_degree[node] = in_degree.get(node, 0) + 1

    queue = deque([n for n, d in in_degree.items() if d == 0])
    order = []
    visited = set()

    while queue:
        node = queue.popleft()
        if node in visited:
            continue
        visited.add(node)
        order.append(node)
        # Chercher les services qui dépendent de ce nœud
        for n, deps in graph.items():
            if node in deps and n not in visited:
                in_degree[n] -= 1
                if in_degree[n] == 0:
                    queue.append(n)

    has_cycle = len(order) < len(graph)
    return order, has_cycle

def validate_compose(compose: dict) -> list[str]:
    """Valide un compose et retourne les avertissements."""
    warnings = []
    services = compose.get("services", {})

    for name, cfg in services.items():
        # Vérification healthcheck sur les services avec depends_on healthy
        deps = cfg.get("depends_on", {})
        if isinstance(deps, dict):
            for dep_name, dep_cfg in deps.items():
                if dep_cfg.get("condition") == "service_healthy":
                    dep_service = services.get(dep_name, {})
                    if "healthcheck" not in dep_service:
                        warnings.append(
                            f"⚠️  '{name}' attend '{dep_name}' (service_healthy) "
                            f"mais '{dep_name}' n'a pas de healthcheck défini !"
                        )

        # Vérification restart policy
        restart = cfg.get("restart", "no")
        if restart == "no" and name not in ["migration"]:
            warnings.append(
                f"ℹ️  '{name}' : restart policy est 'no' — "
                f"le service ne redémarrera pas automatiquement"
            )

        # Image ou build requis
        if "image" not in cfg and "build" not in cfg:
            warnings.append(f"❌ '{name}' : ni 'image' ni 'build' défini !")

    return warnings

# === Exécution ===
compose = parse_compose(COMPOSE_YAML)
graph = extract_dependencies(compose)
order, has_cycle = topological_sort(graph)
warnings = validate_compose(compose)

print("=" * 55)
print(f"  Projet : {compose.get('name', 'sans nom')}")
print(f"  Services : {len(compose.get('services', {}))}")
print(f"  Réseaux  : {len(compose.get('networks', {}))}")
print("=" * 55)

print("\n📊 Graphe de dépendances :")
for svc, deps in graph.items():
    conditions = {}
    raw_deps = compose["services"][svc].get("depends_on", {})
    if isinstance(raw_deps, dict):
        conditions = {k: v.get("condition", "?") for k, v in raw_deps.items()}
    if deps:
        dep_str = ", ".join(f"{d} [{conditions.get(d, 'started')}]" for d in deps)
        print(f"  {svc:12} ← {dep_str}")
    else:
        print(f"  {svc:12} ← (aucune dépendance)")

print(f"\n{'✅' if not has_cycle else '❌'} Ordre de démarrage :")
for i, svc in enumerate(order, 1):
    print(f"  {i}. {svc}")

if has_cycle:
    print("  ⚠️  CYCLE DÉTECTÉ — Compose ne pourra pas démarrer !")

print(f"\n{'⚠️  Avertissements' if warnings else '✅ Aucun avertissement'} :")
for w in warnings:
    print(f"  {w}")
```

```{code-cell} python
# Visualisation des réseaux et de l'isolation
fig, axes = plt.subplots(1, 2, figsize=(13, 5))

# --- Résumé des services par réseau ---
ax1 = axes[0]
services_data = compose.get("services", {})
network_members = defaultdict(list)
for svc_name, svc_cfg in services_data.items():
    nets = svc_cfg.get("networks", [])
    if isinstance(nets, list):
        for net in nets:
            network_members[net].append(svc_name)
    elif isinstance(nets, dict):
        for net in nets:
            network_members[net].append(svc_name)

networks_info = compose.get("networks", {})
net_names = list(network_members.keys())
svc_names = list(services_data.keys())

# Matrice d'appartenance
matrix = np.zeros((len(svc_names), len(net_names)))
for j, net in enumerate(net_names):
    for i, svc in enumerate(svc_names):
        if svc in network_members[net]:
            matrix[i, j] = 1

im = ax1.imshow(matrix, cmap="YlGn", aspect="auto", vmin=0, vmax=1)
ax1.set_xticks(range(len(net_names)))
ax1.set_yticks(range(len(svc_names)))
ax1.set_xticklabels(net_names, fontsize=11, fontweight="bold")
ax1.set_yticklabels(svc_names, fontsize=11)
ax1.set_title("Appartenance aux réseaux", fontsize=12, fontweight="bold", pad=10)

for i in range(len(svc_names)):
    for j in range(len(net_names)):
        txt = "✓" if matrix[i, j] else "✗"
        color = "white" if matrix[i, j] else "#ccc"
        ax1.text(j, i, txt, ha="center", va="center", fontsize=14, color=color)

# Annotations "internal"
for j, net in enumerate(net_names):
    if networks_info.get(net, {}) and networks_info[net] and networks_info[net].get("internal"):
        ax1.text(j, len(svc_names) - 0.1, "🔒 internal",
                 ha="center", va="bottom", fontsize=8, color="#c0392b", style="italic")

# --- Statistiques des services ---
ax2 = axes[1]
ax2.set_facecolor("#f8f9fa")
ax2.axis("off")
ax2.set_title("Résumé de la configuration", fontsize=12, fontweight="bold", pad=10)

rows = []
for svc_name, svc_cfg in services_data.items():
    rows.append({
        "Service": svc_name,
        "Source": "build" if "build" in svc_cfg else svc_cfg.get("image", "?")[:20],
        "Healthcheck": "✓" if "healthcheck" in svc_cfg else "—",
        "Restart": svc_cfg.get("restart", "no"),
        "Réseaux": len(svc_cfg.get("networks", [])),
    })

df = pd.DataFrame(rows)
col_labels = df.columns.tolist()
cell_text = df.values.tolist()

tbl = ax2.table(
    cellText=cell_text,
    colLabels=col_labels,
    loc="center",
    cellLoc="center",
)
tbl.auto_set_font_size(False)
tbl.set_fontsize(9)
tbl.scale(1.2, 1.6)

for j in range(len(col_labels)):
    tbl[(0, j)].set_facecolor("#2c3e50")
    tbl[(0, j)].set_text_props(color="white", fontweight="bold")

for i in range(1, len(rows) + 1):
    bg = "#f2f3f4" if i % 2 == 0 else "white"
    for j in range(len(col_labels)):
        tbl[(i, j)].set_facecolor(bg)

plt.tight_layout()
plt.show()
```

## Points clés à retenir

```{admonition} Résumé du chapitre
:class: important
**Docker Compose en une phrase** : un fichier YAML qui décrit toute votre stack, une commande qui la lance.

Les concepts essentiels :
1. **`compose.yml`** décrit services, réseaux et volumes
2. **`depends_on` + `condition: service_healthy`** garantit l'ordre de démarrage
3. **Réseaux nommés** isolent les services entre eux (le backend n'est pas accessible depuis Internet)
4. **Volumes nommés** pour les données persistantes, **bind mounts** pour le développement
5. **Profiles** pour activer des services optionnels (`--profile dev`)
6. **Variables d'environnement** (`.env`) pour les secrets — jamais en dur dans le YAML
7. **`docker compose up -d`** pour démarrer, **`docker compose down`** pour tout arrêter proprement
```
