Chapitre 14 — Docker Compose en production#
Ce chapitre approfondit Docker Compose au-delà des bases — services, réseaux, volumes, healthchecks, depends_on — déjà couverts dans le chapitre Docker. L’objectif est de maîtriser les fonctionnalités avancées qui rendent Compose utilisable sur des projets complexes : réduction de la duplication YAML, gestion fine du cycle de vie des conteneurs, intégration CI/CD et transition vers Kubernetes.
YAML anchors et merges pour un Compose DRY#
Les fichiers compose.yml sur des projets réels deviennent rapidement répétitifs : chaque service partage des variables d’environnement, des labels de logging, des réseaux. Les YAML anchors permettent de factoriser cette configuration.
# compose.yml — configuration commune factorisée avec anchors
x-service-base: &service-base
restart: unless-stopped
networks:
- backend
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
labels:
- "prometheus.io/scrape=true"
x-env-common: &env-common
TZ: Europe/Paris
LOG_LEVEL: info
OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4317
services:
api:
<<: *service-base
image: myapp/api:${VERSION:-latest}
ports:
- "8080:8080"
environment:
<<: *env-common
DATABASE_URL: postgresql://db:5432/app
worker:
<<: *service-base
image: myapp/worker:${VERSION:-latest}
environment:
<<: *env-common
QUEUE_URL: redis://redis:6379/0
deploy:
replicas: 3
scheduler:
<<: *service-base
image: myapp/scheduler:${VERSION:-latest}
environment:
<<: *env-common
&service-base déclare l’ancre, *service-base la référence, <<: fusionne le contenu dans la map courante. Les clés définies localement écrasent celles de l’ancre.
Limites des anchors YAML
Les anchors YAML sont une fonctionnalité du parseur YAML, pas de Docker Compose. Ils ne fonctionnent qu’au sein d’un même fichier. Pour partager la configuration entre fichiers, utiliser extends: (voir section suivante). De plus, docker compose config résout les anchors et affiche le YAML aplati — utile pour déboguer.
extends: entre fichiers Compose#
Le mot-clé extends: permet d’hériter la définition d’un service défini dans un autre fichier, sans le copier. C’est le mécanisme DRY inter-fichiers de Compose.
# compose.base.yml — définitions partagées entre tous les environnements
services:
api:
image: myapp/api:${VERSION:-latest}
environment:
LOG_LEVEL: info
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 10s
timeout: 5s
retries: 3
# compose.prod.yml — surcharge pour la production
services:
api:
extends:
file: compose.base.yml
service: api
environment:
LOG_LEVEL: warn
DATABASE_URL: "${PROD_DATABASE_URL}"
deploy:
replicas: 4
resources:
limits:
cpus: "1.0"
memory: 512M
# Lancer avec le fichier production
docker compose -f compose.prod.yml up -d
Section deploy: — ressources et politiques#
La section deploy: est activée nativement par Docker Compose (depuis Compose v2) pour configurer le comportement de déploiement.
services:
api:
image: myapp/api:latest
deploy:
replicas: 3
resources:
limits:
cpus: "0.5"
memory: 256M
reservations:
cpus: "0.1"
memory: 64M
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
window: 120s
update_config:
parallelism: 1 # Mettre à jour 1 replica à la fois
delay: 10s # Délai entre chaque replica mis à jour
failure_action: rollback
monitor: 30s # Durée d'observation avant de valider la mise à jour
max_failure_ratio: 0.1
rollback_config:
parallelism: 0 # Rollback de tous les replicas simultanément
delay: 0s
failure_action: pause
monitor: 30s
deploy: avec Docker Compose standalone
Certaines options deploy: (comme replicas) ne sont pleinement honorées qu’avec Docker Swarm. Avec docker compose up, les resources (limits/reservations) sont appliquées, mais replicas crée plusieurs conteneurs préfixés par le nom du projet. Ce comportement est suffisant pour le développement local et les tests d’intégration CI.
Compose Watch — synchronisation en développement#
watch: (stable depuis Compose 2.22) remplace les volumes bind-mount pour le rechargement à chaud. Il surveille les fichiers locaux et synchronise les changements dans le conteneur, ou reconstruit l’image selon les règles définies.
services:
frontend:
build: ./frontend
ports:
- "3000:3000"
develop:
watch:
- action: sync
path: ./frontend/src
target: /app/src
ignore:
- node_modules/
- action: rebuild
path: ./frontend/package.json
- action: rebuild
path: ./frontend/Dockerfile
backend:
build: ./backend
develop:
watch:
- action: sync+restart
path: ./backend/src
target: /app/src
# Lancer avec le mode watch actif
docker compose watch
# Ou via up (depuis Compose 2.26)
docker compose up --watch
Les trois actions disponibles :
sync: copie les fichiers modifiés dans le conteneur sans redémarrer (rechargement à chaud via le process applicatif)sync+restart: synchronise puis redémarre le conteneurrebuild: reconstruit l’image et recrée le conteneur (pour les changements de dépendances)
Compose et tests d’intégration CI#
Compose est particulièrement utile en CI pour démarrer un environnement complet et exécuter des tests d’intégration. Les options dédiées permettent d’automatiser proprement ce workflow.
# compose.test.yml
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: testdb
POSTGRES_USER: test
POSTGRES_PASSWORD: test
healthcheck:
test: ["CMD-SHELL", "pg_isready -U test -d testdb"]
interval: 5s
timeout: 3s
retries: 10
redis:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
tests:
build: .
command: pytest tests/integration/ -v --tb=short
environment:
DATABASE_URL: postgresql://test:test@db:5432/testdb
REDIS_URL: redis://redis:6379/0
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
# En CI (GitHub Actions, GitLab CI, etc.)
# Démarrer tous les services et attendre qu'ils soient healthy
docker compose -f compose.test.yml up --wait
# Lancer les tests et récupérer le code de sortie du service "tests"
docker compose -f compose.test.yml run --exit-code-from tests tests
# Alternative : up avec exit-code-from (démarre + attend + retourne le code)
docker compose -f compose.test.yml up --abort-on-container-exit --exit-code-from tests
# Cleanup systématique (important en CI pour libérer les ressources)
docker compose -f compose.test.yml down --volumes --remove-orphans
--exit-code-from SERVICE fait retourner à compose up le code de sortie du service spécifié — essentiel pour que le CI détecte les échecs de tests.
Secrets Compose vs secrets Kubernetes#
# Secrets dans compose.yml — deux sources possibles
services:
api:
image: myapp/api
secrets:
- db_password
- api_key
secrets:
db_password:
# Source 1 : fichier local (développement)
file: ./secrets/db_password.txt
api_key:
# Source 2 : variable d'environnement (CI/CD)
environment: API_KEY_SECRET
Les secrets Compose sont montés en lecture seule dans /run/secrets/<secret_name> dans le conteneur. C’est plus sécurisé que les variables d’environnement (non visibles dans docker inspect), mais limité comparé aux secrets Kubernetes.
Comparaison avec les secrets Kubernetes :
Kubernetes Secrets : chiffrés au repos (avec KMS), RBAC granulaire, rotation sans redéploiement via external-secrets-operator
Compose secrets : simples et portables, mais pas de chiffrement natif, pas de rotation automatique
Pour la production sérieuse, utiliser Vault, AWS Secrets Manager ou Kubernetes External Secrets même avec Compose
Override files et multi-environnements#
Le mécanisme d’override files permet de maintenir une base commune et de surcharger uniquement ce qui diffère par environnement.
# compose.yml — base commune
services:
api:
image: myapp/api:${VERSION}
environment:
LOG_LEVEL: info
# compose.override.yml — chargé AUTOMATIQUEMENT en développement local
# (Docker Compose fusionne automatiquement ce fichier si présent)
services:
api:
build: . # Construire localement au lieu de tirer l'image
volumes:
- ./src:/app/src # Code source monté pour le rechargement à chaud
environment:
LOG_LEVEL: debug
ports:
- "8080:8080"
# compose.staging.yml — staging (chargé explicitement)
services:
api:
environment:
LOG_LEVEL: warn
DATABASE_URL: "${STAGING_DATABASE_URL}"
deploy:
replicas: 2
# Développement local : compose.yml + compose.override.yml (automatique)
docker compose up
# Staging : compose.yml + compose.staging.yml (explicite)
docker compose -f compose.yml -f compose.staging.yml up -d
# Production : compose.yml + compose.prod.yml
docker compose -f compose.yml -f compose.prod.yml up -d
Multi-projet et namespaces Compose#
COMPOSE_PROJECT_NAME isole les ressources (conteneurs, réseaux, volumes) de différents projets Compose sur un même hôte.
# Définir le nom de projet explicitement
COMPOSE_PROJECT_NAME=myapp-prod docker compose up -d
COMPOSE_PROJECT_NAME=myapp-staging docker compose up -d
# Ou via le flag --project-name
docker compose --project-name myapp-prod up -d
# Ou dans le fichier .env
echo "COMPOSE_PROJECT_NAME=myapp" >> .env
Les conteneurs, réseaux et volumes sont préfixés par le nom de projet (myapp-prod_api_1, myapp-staging_api_1), ce qui permet de faire coexister plusieurs instances du même compose.yml sans collision.
Isolation des réseaux par projet
Deux projets Compose distincts créent des réseaux Docker séparés. Les services d’un projet ne peuvent pas communiquer directement avec ceux d’un autre projet sans configuration explicite de réseaux partagés (external: true). C’est une bonne garantie d’isolation pour les environnements de test en parallèle.
Limites de Compose vs Kubernetes#
Compose est excellent pour le développement local et les déploiements simples sur un seul hôte. Ses limites apparaissent dès qu’on vise une infrastructure de production robuste.
Critère |
Docker Compose |
Kubernetes |
|---|---|---|
Multi-hôte natif |
Non (Swarm pour ça) |
Oui |
Autoscaling |
Manuel |
HPA, KEDA |
Self-healing avancé |
Basique (restart_policy) |
Liveness/readiness probes, PodDisruptionBudget |
Rolling update sans downtime |
Limité |
Natif |
Gestion des secrets |
Basique |
External Secrets, Vault |
Service discovery |
DNS Docker |
kube-dns + Service |
Observabilité |
Via intégrations manuelles |
Prometheus Operator, Grafana |
Migrer vers Kubernetes quand :
Le service doit tourner sur plusieurs hôtes pour la haute disponibilité
L’autoscaling automatique est requis (trafic variable)
L’équipe dépasse 5-10 développeurs avec des déploiements fréquents
Des exigences de compliance nécessitent une gestion des secrets avancée
Kompose — migration automatique
kompose convert traduit automatiquement un compose.yml en manifestes Kubernetes (Deployments, Services, PVCs). Le résultat nécessite des ajustements manuels mais accélère la migration initiale.
Graphe de dépendances entre services#
import networkx as nx
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import seaborn as sns
sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
# Graphe de dépendances d'un Compose complexe (e-commerce)
G = nx.DiGraph()
services = {
"nginx": {"color": "#4C72B0", "layer": 0},
"api": {"color": "#DD8452", "layer": 1},
"worker": {"color": "#DD8452", "layer": 1},
"scheduler": {"color": "#DD8452", "layer": 1},
"postgres": {"color": "#55A868", "layer": 2},
"redis": {"color": "#55A868", "layer": 2},
"minio": {"color": "#55A868", "layer": 2},
"mailhog": {"color": "#C44E52", "layer": 2},
"prometheus": {"color": "#8172B2", "layer": 3},
"grafana": {"color": "#8172B2", "layer": 3},
}
# depends_on (condition: service_healthy)
edges = [
("nginx", "api"),
("api", "postgres"),
("api", "redis"),
("api", "minio"),
("api", "mailhog"),
("worker", "postgres"),
("worker", "redis"),
("scheduler", "postgres"),
("scheduler", "redis"),
("prometheus", "api"),
("prometheus", "postgres"),
("grafana", "prometheus"),
]
G.add_nodes_from(services.keys())
G.add_edges_from(edges)
# Disposition hiérarchique par couche
pos = {}
layer_nodes = {}
for svc, attrs in services.items():
layer = attrs["layer"]
layer_nodes.setdefault(layer, []).append(svc)
for layer, nodes in layer_nodes.items():
n = len(nodes)
for i, node in enumerate(nodes):
pos[node] = (i - (n - 1) / 2, -layer)
node_colors = [services[n]["color"] for n in G.nodes()]
fig, ax = plt.subplots(figsize=(13, 7))
nx.draw_networkx_nodes(G, pos, node_color=node_colors, node_size=1800, ax=ax, alpha=0.9)
nx.draw_networkx_labels(G, pos, font_size=9, font_color="white", font_weight="bold", ax=ax)
nx.draw_networkx_edges(G, pos, edge_color="#555555", arrows=True,
arrowsize=18, width=1.5, ax=ax,
connectionstyle="arc3,rad=0.05",
node_size=1800)
legend_handles = [
mpatches.Patch(color="#4C72B0", label="Reverse proxy"),
mpatches.Patch(color="#DD8452", label="Services applicatifs"),
mpatches.Patch(color="#55A868", label="Stockage / Infra"),
mpatches.Patch(color="#C44E52", label="Outils dev"),
mpatches.Patch(color="#8172B2", label="Observabilité"),
]
ax.legend(handles=legend_handles, loc="lower right", fontsize=9)
ax.set_title("Graphe de dépendances entre services Compose\n(flèches = depends_on)", fontsize=12, fontweight="bold")
ax.axis("off")
plt.show()
Radar chart : Compose vs Kubernetes vs Swarm#
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
categories = [
"Simplicité\nd'utilisation", "Fonctionnalités\nprod", "Multi-hôte",
"Autoscaling", "Ecosystème\noutils", "Courbe\nd'apprentissage\n(inverse)"
]
N = len(categories)
# Scores /10
compose = [10, 5, 2, 2, 7, 10]
swarm = [ 8, 6, 8, 5, 5, 8]
kube = [ 4, 10, 10, 10, 10, 3]
def close(lst):
return lst + [lst[0]]
angles = [n / float(N) * 2 * np.pi for n in range(N)]
angles_p = angles + [angles[0]]
fig, ax = plt.subplots(figsize=(8, 8), subplot_kw=dict(polar=True))
ax.plot(angles_p, close(compose), "o-", linewidth=2, label="Docker Compose", markersize=5)
ax.fill(angles_p, close(compose), alpha=0.15)
ax.plot(angles_p, close(swarm), "s-", linewidth=2, label="Docker Swarm", markersize=5)
ax.fill(angles_p, close(swarm), alpha=0.15)
ax.plot(angles_p, close(kube), "^-", linewidth=2, label="Kubernetes", markersize=5)
ax.fill(angles_p, close(kube), alpha=0.15)
ax.set_xticks(angles)
ax.set_xticklabels(categories, size=9)
ax.set_ylim(0, 10)
ax.set_yticks([2, 4, 6, 8, 10])
ax.set_yticklabels(["2", "4", "6", "8", "10"], size=8)
ax.set_title("Compose vs Swarm vs Kubernetes\n(prod-readiness et complexité)", size=12, fontweight="bold", pad=20)
ax.legend(loc="upper right", bbox_to_anchor=(1.35, 1.15), fontsize=11)
plt.show()
Simulation de l’ordre de démarrage avec healthchecks#
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import seaborn as sns
import time as time_mod
sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
# Machine à états simulant le démarrage de services avec healthchecks
# États : WAITING → STARTING → HEALTHY / UNHEALTHY
import numpy as np
np.random.seed(7)
services_def = [
{"name": "postgres", "start_t": 0, "healthy_t": 8, "depends": []},
{"name": "redis", "start_t": 0, "healthy_t": 3, "depends": []},
{"name": "api", "start_t": 8, "healthy_t": 14, "depends": ["postgres", "redis"]},
{"name": "worker", "start_t": 8, "healthy_t": 12, "depends": ["postgres", "redis"]},
{"name": "nginx", "start_t": 14, "healthy_t": 16, "depends": ["api"]},
{"name": "prometheus", "start_t": 14, "healthy_t": 17, "depends": ["api"]},
{"name": "grafana", "start_t": 17, "healthy_t": 19, "depends": ["prometheus"]},
]
# Palette de couleurs
palette = sns.color_palette("muted", len(services_def))
fig, ax = plt.subplots(figsize=(13, 6))
y_labels = []
for i, svc in enumerate(services_def):
name = svc["name"]
s = svc["start_t"]
h = svc["healthy_t"]
y_labels.append(name)
color = palette[i]
# Phase WAITING (gris) avant démarrage
if s > 0:
ax.barh(i, s, left=0, height=0.5, color="#cccccc", alpha=0.8)
ax.text(s / 2, i, "waiting", ha="center", va="center", fontsize=7, color="#555555")
# Phase STARTING (orange) : entre start_t et healthy_t
ax.barh(i, h - s, left=s, height=0.5, color="#DD8452", alpha=0.85)
ax.text(s + (h - s) / 2, i, "starting", ha="center", va="center", fontsize=7, color="white")
# Phase HEALTHY (vert) : après healthy_t
ax.barh(i, 22 - h, left=h, height=0.5, color="#55A868", alpha=0.85)
ax.text(h + (22 - h) / 2, i, "healthy", ha="center", va="center", fontsize=7, color="white")
# Repère du moment healthy
ax.axvline(x=h, color=color, linestyle=":", alpha=0.4, linewidth=1)
# Dépendances
if svc["depends"]:
deps_str = "dépend de : " + ", ".join(svc["depends"])
ax.text(22.2, i, deps_str, va="center", fontsize=7, color="#333333")
ax.set_yticks(range(len(services_def)))
ax.set_yticklabels(y_labels)
ax.set_xlabel("Temps (secondes)")
ax.set_title("Ordre de démarrage avec depends_on condition: service_healthy", fontsize=12, fontweight="bold")
ax.set_xlim(0, 30)
legend_handles = [
mpatches.Patch(color="#cccccc", label="En attente (waiting)"),
mpatches.Patch(color="#DD8452", label="Démarrage (starting)"),
mpatches.Patch(color="#55A868", label="Prêt (healthy)"),
]
ax.legend(handles=legend_handles, loc="lower right", fontsize=9)
plt.show()
Résumé#
Les YAML anchors (
&anchor,<<: *merge) éliminent la duplication intra-fichier ;extends:partage la configuration entre fichiers Compose distincts.La section
deploy:contrôle le comportement de déploiement : nombre de replicas, limites CPU/mémoire, politique de redémarrage, stratégie de mise à jour et de rollback.Compose Watch (
develop.watch:) remplace les volumes bind-mount pour le développement local en synchronisant les fichiers sans redémarrer le conteneur — ou en reconstruisant l’image si les dépendances changent.En CI,
--waitattend que tous les services soient healthy avant de lancer les tests ;--exit-code-frompropage le code de sortie du service de test au pipeline.Les override files (
compose.override.ymlchargé automatiquement,-f compose.staging.ymlchargé explicitement) permettent de gérer plusieurs environnements avec un seul socle de configuration.COMPOSE_PROJECT_NAMEisole les ressources de projets distincts sur un même hôte, évitant les collisions de noms lors de tests parallèles en CI.Les secrets Compose (
/run/secrets/) sont plus sûrs que les variables d’environnement, mais n’ont pas le chiffrement, le RBAC et la rotation automatique des secrets Kubernetes.La migration vers Kubernetes est justifiée quand apparaissent des besoins multi-hôtes, d’autoscaling, ou de haute disponibilité que Compose ne peut pas satisfaire seul.
kompose convertautomatise la traduction initiale d’uncompose.ymlvers des manifestes Kubernetes, réduisant le coût de migration.L’ordre de démarrage avec
depends_on condition: service_healthygarantit que chaque service attend réellement que ses dépendances soient opérationnelles — critère indispensable pour les tests d’intégration fiables.