Docker Compose — Orchestrer plusieurs conteneurs#
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.
Analogie — La recette de cuisine
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 :
# 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 Docker à utiliser |
|
|
Construire depuis un Dockerfile |
|
|
Publier des ports |
|
|
Monter volumes ou répertoires |
|
|
Variables d’environnement |
|
|
Charger depuis un fichier |
|
|
Ordre de démarrage / healthcheck |
voir ci-dessous |
|
Vérifier si le service est prêt |
voir ci-dessous |
|
Politique de redémarrage |
|
|
Grouper des services optionnels |
|
|
Ressources et réplicas (Swarm/Compose) |
|
|
Rattacher à des réseaux |
|
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 :
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
Les trois conditions de depends_on
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érationnelservice_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 :
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 :
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#
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:
Quand utiliser quoi ?
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 :
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]
# 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#
# 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#
/tmp/ipykernel_22743/4212682099.py:112: UserWarning: Glyph 127760 (\N{GLOBE WITH MERIDIANS}) missing from font(s) DejaVu Sans.
plt.tight_layout()
/tmp/ipykernel_22743/4212682099.py:112: UserWarning: Glyph 128013 (\N{SNAKE}) missing from font(s) DejaVu Sans.
plt.tight_layout()
/tmp/ipykernel_22743/4212682099.py:112: UserWarning: Glyph 128452 (\N{FILE CABINET}) missing from font(s) DejaVu Sans.
plt.tight_layout()
/tmp/ipykernel_22743/4212682099.py:113: UserWarning: Glyph 127760 (\N{GLOBE WITH MERIDIANS}) missing from font(s) DejaVu Sans.
plt.savefig("compose_architecture.png", dpi=120, bbox_inches="tight")
/tmp/ipykernel_22743/4212682099.py:113: UserWarning: Glyph 128013 (\N{SNAKE}) missing from font(s) DejaVu Sans.
plt.savefig("compose_architecture.png", dpi=120, bbox_inches="tight")
/tmp/ipykernel_22743/4212682099.py:113: UserWarning: Glyph 128452 (\N{FILE CABINET}) missing from font(s) DejaVu Sans.
plt.savefig("compose_architecture.png", dpi=120, bbox_inches="tight")
/home/loc/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 127760 (\N{GLOBE WITH MERIDIANS}) missing from font(s) DejaVu Sans.
fig.canvas.print_figure(bytes_io, **kw)
/home/loc/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 128013 (\N{SNAKE}) missing from font(s) DejaVu Sans.
fig.canvas.print_figure(bytes_io, **kw)
/home/loc/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 128452 (\N{FILE CABINET}) missing from font(s) DejaVu Sans.
fig.canvas.print_figure(bytes_io, **kw)
Visualisation : ordre de démarrage et dépendances#
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 :
# 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
La variable ${DB_PASSWORD}
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) :
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}")
=======================================================
Projet : webapp
Services : 5
Réseaux : 2
=======================================================
📊 Graphe de dépendances :
nginx ← app [service_healthy]
app ← db [service_healthy], redis [service_started], migration [service_completed_successfully]
migration ← db [service_healthy]
db ← (aucune dépendance)
redis ← (aucune dépendance)
✅ Ordre de démarrage :
1. db
2. redis
3. migration
4. app
5. nginx
⚠️ Avertissements :
⚠️ 'nginx' attend 'app' (service_healthy) mais 'app' n'a pas de healthcheck défini !
ℹ️ 'nginx' : restart policy est 'no' — le service ne redémarrera pas automatiquement
ℹ️ 'app' : restart policy est 'no' — le service ne redémarrera pas automatiquement
ℹ️ 'db' : restart policy est 'no' — le service ne redémarrera pas automatiquement
ℹ️ 'redis' : restart policy est 'no' — le service ne redémarrera pas automatiquement
# 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()
/tmp/ipykernel_22743/3066436115.py:86: UserWarning: Glyph 128274 (\N{LOCK}) missing from font(s) DejaVu Sans.
plt.tight_layout()
/home/loc/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 128274 (\N{LOCK}) missing from font(s) DejaVu Sans.
fig.canvas.print_figure(bytes_io, **kw)
Points clés à retenir#
Résumé du chapitre
Docker Compose en une phrase : un fichier YAML qui décrit toute votre stack, une commande qui la lance.
Les concepts essentiels :
compose.ymldécrit services, réseaux et volumesdepends_on+condition: service_healthygarantit l’ordre de démarrageRéseaux nommés isolent les services entre eux (le backend n’est pas accessible depuis Internet)
Volumes nommés pour les données persistantes, bind mounts pour le développement
Profiles pour activer des services optionnels (
--profile dev)Variables d’environnement (
.env) pour les secrets — jamais en dur dans le YAMLdocker compose up -dpour démarrer,docker compose downpour tout arrêter proprement