00. Docker Compose, le point de départ
Avant le cluster, la stack de développement — et elle en apprend plus qu'on ne croit sur la mise en route d'un système distribué.
depends_on qui
séquence réellement le démarrage. C'est là que se règle l'essentiel des
problèmes d'ordre — et ce qu'on résout ici se retrouve, sous une autre
forme, dans les manifestes Kubernetes.
Une commande#
cp .env.template .env
docker compose up --build # → http://localhost:8000/docsPour faire tourner l’API sur l’hôte, démarrer aussi RabbitMQ : le lifespan de l’API s’y connecte. Appliquer d’abord les migrations en connexion directe via le service migrate :
cp .env.template .env # une seule fois ; adapter les valeurs
docker compose up -d postgres pgbouncer valkey rabbitmq mailpit
docker compose run --rm migrate
cd backend
uv sync
make run| Service | Image | Rôle |
|---|---|---|
postgres | postgres:17-alpine | La base |
pgbouncer | edoburu/pgbouncer:v1.25.2-p0 | Pooler, mode transaction |
rabbitmq | rabbitmq:4-management-alpine | Broker + console |
valkey | valkey/valkey:8-alpine | Cache, --appendonly yes |
mailpit | axllent/mailpit:latest | SMTP de test + webmail |
prometheus | prom/prometheus:v3.1.0 | Métriques de l'API |
redisinsight | redis/redisinsight:latest | Inspecter le cache |
migrate | image du backend | alembic upgrade head, puis s'arrête |
api | image du backend | Gunicorn / Uvicorn |
worker | image du backend | Consumer RabbitMQ |
frontend | image du frontend | Preact + serveur Vite (cible dev) |
Le vrai sujet : l'ordre de démarrage#
Onze conteneurs qui démarrent en même temps, c'est onze façons d'échouer. L'API qui se connecte à une base pas encore prête, le worker qui déclare une file sur un broker en cours d'initialisation, les migrations qui partent avant que PostgreSQL n'accepte les connexions.
La réponse tient en deux mécanismes qui n'ont d'intérêt qu'ensemble.
1. Les healthchecks : « prêt » plutôt que « démarré »#
postgres:
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
retries: 5
rabbitmq:
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
interval: 15s
retries: 5
valkey:
healthcheck:
test: ["CMD", "valkey-cli", "ping"]Ces sondes ont une portée limitée : pg_isready vérifie l’acceptation des connexions, pas le schéma ni les droits applicatifs ; rabbitmq-diagnostics ping vérifie le processus Erlang et ne prouve pas qu’une file est disponible. Les dépendances Compose sont vérifiées au démarrage ; une panne ultérieure ne provoque pas automatiquement le redémarrage des services dépendants. Voir l’ordre de démarrage Compose.
2. depends_on avec condition#
C'est le point que la plupart des fichiers Compose ratent. Un
depends_on nu ne fait qu'ordonner le lancement : Docker
démarre le conteneur A avant le B, sans jamais vérifier que A fonctionne. Ici,
chaque dépendance porte une condition :
api:
depends_on:
migrate:
condition: service_completed_successfully # le schéma est en place
pgbouncer:
condition: service_healthy # le pooler répond
valkey:
condition: service_healthy
rabbitmq:
condition: service_healthy
mailpit:
condition: service_started # démarré suffit| Condition | Signifie | Utilisée pour |
|---|---|---|
service_started | Le conteneur tourne | Mailpit — pas de sonde, et sa panne n'est pas bloquante |
service_healthy | Le healthcheck passe | postgres, pgbouncer, valkey, rabbitmq, api |
service_completed_successfully | Le conteneur s'est arrêté en succès | migrate — une tâche, pas un service |
service_completed_successfully sur migrate transforme un
conteneur éphémère en barrière : ni l'API ni le worker ne
démarrent tant que les migrations n'ont pas réussi. C'est exactement le rôle que
joue le Job Alembic côté Kubernetes
(ch. 01) — et
deploy.sh le rend explicite avec un
kubectl wait --for=condition=complete. Même problème, même solution,
deux vocabulaires.
Le graphe de démarrage#
Le worker est le plus contraint : il attend les migrations, le pooler, le broker, Mailpit… et l'API en bonne santé. Ce dernier lien n'est pas une nécessité technique — le worker ne parle jamais à l'API. C'est un choix de confort en développement : si l'API ne démarre pas, on veut le voir sans que les logs du worker ne défilent par-dessus.
Ce que Compose fait mieux que Kubernetes#
La comparaison n'est pas à sens unique, et il serait malhonnête de présenter le cluster comme un progrès sur toute la ligne.
| Docker Compose | Kubernetes (Kind) | |
|---|---|---|
| Démarrer | docker compose up | Un script, deux opérateurs à installer, des images à charger |
| Ordre de démarrage | Déclaratif et garanti (depends_on + conditions) | À reconstruire : wait, Jobs, initContainers |
| Rechargement à chaud | Un bind-mount suffit | Rebuild + kind load + rollout restart |
| Lire les logs | docker compose logs -f api | kubectl logs -f deployment/api, par pod |
| Haute disponibilité | — | Réplicas, failover, anti-affinité |
| Segmentation réseau | Réseaux Docker, grossier | NetworkPolicy par cible |
| Montée en charge | Manuelle | HPA |
L'ordre de démarrage est le cas le plus frappant : Kubernetes n'a pas
d'équivalent direct de depends_on: condition. Un pod démarre
quand le scheduler le décide, et on rétablit la séquence avec des sondes de
readiness, des initContainers, un Job barrière et des
kubectl wait dans le script de déploiement. Ce que Compose exprime en
trois lignes se répartit dans quatre mécanismes.
Ce qu'on emporte vers le cluster#
Écrire le Compose d'abord n'est pas une étape jetable : les décisions qu'on y prend se retrouvent presque toutes, et c'est ce qui rend la traduction si rapide.
| Décision prise dans Compose | Ce qu'elle devient |
|---|---|
| Pooler en mode transaction | La même contrainte, et les mêmes trois réglages côté code (ch. 01) |
migrate comme service éphémère | Un Job + un kubectl wait |
| Healthcheck HTTP de l'API | readinessProbe et livenessProbe sur /health |
Variables APP_* | Une ConfigMap, plus les URLs interpolées (ch. 02) |
| Une image pour api / worker / migrate | Trois workloads, une image (ch. 02) |
| Volumes nommés | Stockage géré par les opérateurs |
La seule décision qui ne se transporte pas est justement l'ordre de démarrage : il faut le réexprimer autrement. Tout le reste est une traduction de vocabulaire.
À retenir#
- Un
depends_onnu ne garantit rien : sanscondition, il n'ordonne que le lancement. - Un healthcheck interroge le service dans sa langue — un port ouvert ne veut pas dire prêt.
service_completed_successfullytransforme un conteneur éphémère en barrière : c'est leJobAlembic avant l'heure.- Compose reste meilleur pour développer : démarrage, rechargement à chaud, ordre déclaratif.
- Presque toutes les décisions se transportent vers le cluster — sauf l'ordre de démarrage, qu'il faut réexprimer en quatre mécanismes.