Chapitre 12.3 — docker compose
⏱️ TL;DR — Lancer un conteneur à la main, ça va. Mais une vraie app, c’est plusieurs conteneurs qui doivent démarrer ensemble, se parler, partager des volumes : le front, l’API, la base. Enchaîner dix
docker runavec les bons ports, les bonnes variables et les bons réseaux devient vite ingérable.docker compose(v2, en deux mots) décrit toute la stack dans un seul fichiercompose.yaml: quels services, quelles images, quels ports, quels volumes, quel réseau. Ensuite, une commande —docker compose up -d— démarre tout, etdocker compose downarrête tout. Ce chapitre construit lecompose.yamlde FormaCampus (Next + Postgres), avec les deux réflexes qui sauvent : un volume nommé pour que la base survive, et pas de port Postgres exposé vers l’hôte.
🎯 Objectifs
- Décrire une stack multi-services dans un
compose.yaml(v2). - Distinguer
image:(image toute prête) etbuild:(construire depuis unDockerfile). - Persister les données d’une base avec un volume nommé (et éviter la perte au recreate).
- Câbler les services entre eux via un réseau et
depends_on. - N’exposer que le strict nécessaire (surtout pas le port Postgres).
- Piloter la stack :
up -d,down,logs -f,ps,exec. - Garder les secrets hors de git (
.env).
Ce que Compose remplace
Sans Compose, faire tourner un front qui parle à une base ressemble à ça : créer un réseau à la main, lancer Postgres avec le bon volume et les bonnes variables, lancer l’app avec la bonne connexion, dans le bon ordre, en retenant tous les noms. À la moindre modif, tu détricotes tout. Compose remplace cette gymnastique par un fichier déclaratif : tu décris l’état voulu, Compose se charge de créer réseau, volumes et conteneurs dans le bon ordre.
Un compose.yaml s’articule autour de trois blocs de haut niveau : services (les conteneurs), volumes (le stockage persistant), networks (la connectique). Un service, c’est un conteneur (ou plusieurs répliques) décrit par son image, ses ports, ses variables, ses volumes.
📚 La doc — On écrit
docker compose(v2, plugin), jamais l’ancien binairedocker-compose(v1, tiret, en fin de vie). Le fichier s’appelle idéalementcompose.yaml(nom moderne recommandé) ;docker-compose.ymlreste reconnu. La spécification Compose est documentée surdocs.docker.com— la référence quand tu doutes d’une clé.
Le compose.yaml de FormaCampus
Voici une stack front Next + Postgres. Lis les commentaires : chaque clé a une raison d’être.
services:
# ── Le front Next.js : on le CONSTRUIT depuis notre Dockerfile ──
web:
build: . # construit l'image via le Dockerfile local
restart: unless-stopped # redémarre sauf si on l'a arrêté exprès
env_file:
- .env.production # injecte les variables AU RUNTIME (hors git)
ports:
- "127.0.0.1:3000:3000" # publie SEULEMENT sur localhost de l'hôte
depends_on:
db:
condition: service_healthy # attend que Postgres soit VRAIMENT prêt
networks:
- internal
# ── La base Postgres : on utilise l'image officielle ──
db:
image: postgres:16 # image toute prête depuis Docker Hub
restart: unless-stopped
environment:
POSTGRES_DB: formacampus
POSTGRES_USER: formacampus
POSTGRES_PASSWORD: ${DB_PASSWORD} # lu depuis le fichier .env (hors git)
volumes:
- pgdata:/var/lib/postgresql/data # ← LE volume qui sauve les données
# PAS de 'ports:' ici : la base n'est JAMAIS exposée vers l'hôte.
# Seuls les services du réseau 'internal' peuvent l'atteindre.
healthcheck:
test: ["CMD-SHELL", "pg_isready -U formacampus"]
interval: 5s
timeout: 5s
retries: 5
networks:
- internal
# ── Volumes nommés : gérés par Docker, persistants ──
volumes:
pgdata:
# ── Réseau privé : les services se parlent par leur nom (web, db) ──
networks:
internal:Ce qui se joue dans ce fichier
build: .vsimage: postgres:16— le front, on le construit depuis notreDockerfile(chapitre 12.2) ; Postgres, on tire l’image officielle telle quelle. Un mêmecompose.yamlmélange les deux sans problème.- Le réseau
internal— les deux services y sont branchés. Depuis le conteneurweb, la base est joignable à l’hôtedb(le nom du service), sur le port5432. Ta chaîne de connexion devient donc quelque chose commepostgres://formacampus:motdepasse@db:5432/formacampus— pas127.0.0.1, maisdb. depends_onaveccondition: service_healthy— Compose démarredbavantweb, et attend que lehealthcheckde Postgres passe au vert. Sans la condition,webdémarrerait dès que le conteneurdbexiste, avant que Postgres soit prêt à accepter des connexions — d’où des erreurs au boot.
⚠️ Piège — le volume oublié, la perte de données. Le piège le plus coûteux de tout Docker. Le service
dbrange ses données dans/var/lib/postgresql/data. Sans la lignevolumes: pgdata:/var/lib/postgresql/data, ces données vivent dans la couche éphémère du conteneur. Le jour où tu faisdocker compose downpuisup(ou une mise à jour de l’image Postgres), Compose recrée le conteneurdb→ toutes tes données sont perdues. Le volume nommépgdataest découplé du conteneur : détruis et recréedbautant que tu veux, le volume et la base restent. Vérifie-le avecdocker volume ls.
🔒 Sécurité — ne pas exposer Postgres. Remarque l’absence de
ports:sur le servicedb. C’est voulu. Si tu écrivaisports: - "5432:5432", tu ouvrirais la base sur toutes les interfaces de l’hôte — donc potentiellement sur Internet — et les scanners la trouveraient en minutes. Le front n’a pas besoin de ça : il jointdbpar le réseau interne de Compose, invisible de l’extérieur. Règle générale : un service n’a deports:que s’il doit être atteint depuis l’hôte. Une base ne l’est jamais.
💡 Réflexe — publier sur
127.0.0.1seulement. Pour le serviceweb, on a écrit"127.0.0.1:3000:3000"et non"3000:3000". La différence est cruciale : la première forme publie le port uniquement sur le localhost de l’hôte, où Nginx (installé sur l’hôte) viendra le chercher pour le servir en HTTPS. La seconde l’ouvrirait sur toutes les interfaces. On garde Nginx en façade et l’app derrière — exactement la logique de la Partie 7.
Piloter la stack
Une fois le compose.yaml écrit, tout se pilote depuis le dossier qui le contient.
# Construire (si besoin) et démarrer toute la stack en arrière-plan
docker compose up -d
# up : crée réseau, volumes, conteneurs et les démarre
# -d : en arrière-plan (detached)
# Voir l'état des services de la stack
docker compose ps
# Suivre les logs de TOUS les services (ou d'un seul)
docker compose logs -f
docker compose logs -f web
# Ouvrir un shell dans un service (ex. psql dans la base)
docker compose exec db psql -U formacampus -d formacampus
# Reconstruire l'image du front après un changement de code
docker compose up -d --build web
# Tout arrêter et SUPPRIMER conteneurs + réseau (les volumes RESTENT)
docker compose down
# ⚠️ down -v supprime AUSSI les volumes → efface les données. À manier avec soin.
docker compose down -v🐚 Au terminal — Le duo
docker compose up -d --build(reconstruit et relance ce qui a changé) puisdocker compose logs -f web(regarde le démarrage) est ton cycle de travail au quotidien. Etdocker compose downne touche pas aux volumes nommés : c’estdown -vqui les efface. Grave la différence —down -vsur une base de prod, c’est la journée gâchée.
Les secrets restent hors de git
Le compose.yaml, lui, va dans git (c’est de l’infrastructure décrite, sans secret). Mais les valeurs sensibles — le ${DB_PASSWORD}, les clés API du .env.production — jamais. Compose lit automatiquement un fichier .env (à côté du compose.yaml) pour résoudre les ${VARIABLE}, et les services chargent leurs secrets via env_file:. Ces fichiers vont dans le .gitignore.
# .gitignore
.env
.env.*⚠️ Piège — Committer un
.env(ou écrire les mots de passe en dur dans lecompose.yaml) est une fuite classique : le secret entre dans l’historique git et y reste même après suppression, visible de quiconque clone le repo. Lecompose.yamlréférence des variables (${DB_PASSWORD}) ; les valeurs vivent dans un.envignoré par git, déposé manuellement sur le serveur (ou via un gestionnaire de secrets / les secrets de la CI, cf. Partie 13).
🧭 Sur FormaCampus — Ce
compose.yamlest le cœur de la nouvelle infra FormaCampus, et il tourne à l’identique en dev et en prod (parité). En dev, un dev clone le repo, dépose son.env, tapedocker compose up -det a toute la stack (front + base) en local — plus de « installe Node, installe Postgres, configure… ». En prod, le même fichier tourne sur le VPS, enrichi de l’API Symfony (un troisième serviceapiavec sonbuild:, branché au même réseauinternalet à la même base). Postgres jamais exposé, front publié sur127.0.0.1:3000pour Nginx de l’hôte, données de la base dans le volumepgdatasauvegardé (cf. Partie 15). Une commande démarre la plateforme entière.
✏️ Exercices
Exercice 1 — Ajoute l’API Symfony. Complète le compose.yaml avec un service api construit depuis ./api/Dockerfile, qui doit joindre la base et n’être atteint que par le réseau interne (pas de ports: vers l’hôte, Nginx le proxifiera). Donne juste le bloc du service.
✅ Solution
api:
build: ./api
restart: unless-stopped
env_file:
- .env.production
depends_on:
db:
condition: service_healthy
networks:
- internalIl joint la base via l’hôte db:5432 sur le réseau internal. Pas de ports: : Nginx (hôte) le proxifiera, ou il ne sert que le front en interne. Si l’API doit être atteinte depuis l’hôte par Nginx, on ajoute ports: - "127.0.0.1:8000:8000" (localhost seulement).
Exercice 2 — Le drame du recreate. Un collègue dit : « J’ai fait docker compose down puis up et ma base est vide ! » Son service db n’a pas de section volumes:. Explique ce qui s’est passé et corrige.
✅ Solution
Sans volumes:, les données de Postgres vivaient dans la couche éphémère du conteneur db. Le down a supprimé le conteneur → données perdues ; le up en a recréé un vide. Correction : monter un volume nommé sur le répertoire de données, et le déclarer :
db:
image: postgres:16
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:Désormais le volume pgdata survit aux down/up (seul down -v l’effacerait).
Exercice 3 — Repère la faille. Ce service pose deux problèmes de sécurité. Lesquels ?
db:
image: postgres:16
ports:
- "5432:5432"
environment:
POSTGRES_PASSWORD: SuperSecret123✅ Solution
(1) Port exposé : ports: - "5432:5432" ouvre Postgres sur toutes les interfaces de l’hôte, donc potentiellement sur Internet — cible immédiate des scanners. Un service de base ne doit pas avoir de ports: : il est joint par le réseau interne de Compose. (2) Mot de passe en dur : SuperSecret123 est écrit dans le compose.yaml, qui part dans git → secret fuité et persistant dans l’historique. Il faut POSTGRES_PASSWORD: ${DB_PASSWORD} avec la valeur dans un .env ignoré par git.
🧠 Quiz de révision
1. À quoi sert docker compose par rapport à docker run ?
docker compose par rapport à docker run ?À décrire et piloter une stack multi-services dans un fichier déclaratif (compose.yaml) : services, volumes, réseaux, ordre de démarrage. Une commande (up -d) démarre tout, une autre (down) arrête tout — au lieu d’enchaîner des docker run fragiles à la main.
2. Comme le service web joint-il la base db ?
web joint-il la base db ?Par le nom du service sur le réseau interne de Compose : l’hôte db (résolu par Docker), port 5432. La chaîne de connexion pointe vers db:5432, pas 127.0.0.1. Aucune exposition vers l’hôte n’est nécessaire.
3. Pourquoi le service Postgres n’a-t-il pas de ports: ?
ports: ?Pour ne pas l’exposer hors du conteneur (et donc potentiellement sur Internet). La base n’a besoin d’être jointe que par les autres services du réseau interne. Ajouter ports: serait une faille de sécurité majeure.
4. Quelle est la différence entre docker compose down et down -v ?
docker compose down et down -v ?down supprime conteneurs et réseau mais conserve les volumes nommés (donc les données). down -v supprime aussi les volumes → efface les données. À manier avec une extrême prudence sur une base.
5. Où mettre le mot de passe de la base, et où surtout pas ?
Pas en dur dans le compose.yaml (qui va dans git). On référence une variable (${DB_PASSWORD}) et la valeur vit dans un fichier .env ignoré par git, déposé sur le serveur ou fourni par la CI / un gestionnaire de secrets.
Chapitre suivant : Docker en production — reverse proxy, redémarrage, registres, logs et nettoyage.