Skip to Content

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 run avec 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 fichier compose.yaml : quels services, quelles images, quels ports, quels volumes, quel réseau. Ensuite, une commande — docker compose up -d — démarre tout, et docker compose down arrête tout. Ce chapitre construit le compose.yaml de 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) et build: (construire depuis un Dockerfile).
  • 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 binaire docker-compose (v1, tiret, en fin de vie). Le fichier s’appelle idéalement compose.yaml (nom moderne recommandé) ; docker-compose.yml reste reconnu. La spécification Compose est documentée sur docs.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: . vs image: postgres:16 — le front, on le construit depuis notre Dockerfile (chapitre 12.2) ; Postgres, on tire l’image officielle telle quelle. Un même compose.yaml mélange les deux sans problème.
  • Le réseau internal — les deux services y sont branchés. Depuis le conteneur web, la base est joignable à l’hôte db (le nom du service), sur le port 5432. Ta chaîne de connexion devient donc quelque chose comme postgres://formacampus:motdepasse@db:5432/formacampuspas 127.0.0.1, mais db.
  • depends_on avec condition: service_healthy — Compose démarre db avant web, et attend que le healthcheck de Postgres passe au vert. Sans la condition, web démarrerait dès que le conteneur db existe, 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 db range ses données dans /var/lib/postgresql/data. Sans la ligne volumes: pgdata:/var/lib/postgresql/data, ces données vivent dans la couche éphémère du conteneur. Le jour où tu fais docker compose down puis up (ou une mise à jour de l’image Postgres), Compose recrée le conteneur dbtoutes tes données sont perdues. Le volume nommé pgdata est découplé du conteneur : détruis et recrée db autant que tu veux, le volume et la base restent. Vérifie-le avec docker volume ls.

🔒 Sécurité — ne pas exposer Postgres. Remarque l’absence de ports: sur le service db. C’est voulu. Si tu écrivais ports: - "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 joint db par le réseau interne de Compose, invisible de l’extérieur. Règle générale : un service n’a de ports: que s’il doit être atteint depuis l’hôte. Une base ne l’est jamais.

💡 Réflexe — publier sur 127.0.0.1 seulement. Pour le service web, 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é) puis docker compose logs -f web (regarde le démarrage) est ton cycle de travail au quotidien. Et docker compose down ne touche pas aux volumes nommés : c’est down -v qui les efface. Grave la différence — down -v sur 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.productionjamais. 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 le compose.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. Le compose.yaml référence des variables (${DB_PASSWORD}) ; les valeurs vivent dans un .env ignoré 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.yaml est 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, tape docker compose up -d et 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 service api avec son build:, branché au même réseau internal et à la même base). Postgres jamais exposé, front publié sur 127.0.0.1:3000 pour Nginx de l’hôte, données de la base dans le volume pgdata sauvegardé (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: - internal

Il 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 ?

À 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 ?

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: ?

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 ?

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.

Last updated on