Docker Compose : le guide complet avec exemples
Services, volumes, réseaux, variables d'environnement, healthchecks : tout ce qu'il faut pour écrire un compose.yaml propre et le faire vivre.
Lancer une application web avec docker run, c'est vite une commande de six lignes : ports, volumes, variables d'environnement, réseau, politique de redémarrage. Ajoutez une base de données et un cache, et vous avez trois commandes à retenir, à lancer dans le bon ordre, sans faute de frappe.
Personne ne travaille comme ça longtemps.
Docker Compose est l'outil qui décrit une application composée de plusieurs conteneurs dans un seul fichier YAML, puis la démarre, l'arrête et la met à jour avec une seule commande. Si Docker emballe une application pour qu'elle tourne partout, Compose orchestre les morceaux de cette application sur une machine.
Compose v2 : ce qui a changé
Si vous lisez de vieux tutoriels, vous verrez docker-compose avec un tiret et un champ version: "3.8" en haut des fichiers. Les deux sont obsolètes.
| Avant | Aujourd'hui |
|---|---|
docker-compose up (programme Python séparé) | docker compose up (plugin intégré à Docker) |
docker-compose.yml | compose.yaml (l'ancien nom fonctionne toujours) |
version: "3.8" en première ligne | Supprimé, ignoré avec un avertissement |
Docker Desktop et les paquets Docker officiels sur Linux installent le plugin Compose d'office. Pour vérifier :
docker compose versionUn premier compose.yaml
Prenons une application réaliste : une API Node.js, une base PostgreSQL et Adminer pour consulter la base.
services:
api:
build: .
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://app:${DB_PASSWORD}@db:5432/app
depends_on:
db:
condition: service_healthy
restart: unless-stopped
db:
image: postgres:17
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: app
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 5s
timeout: 3s
retries: 10
restart: unless-stopped
adminer:
image: adminer
ports:
- "127.0.0.1:8080:8080"
profiles: ["debug"]
volumes:
db-data:Et à côté, un fichier .env que Compose lit automatiquement :
DB_PASSWORD=change-moi-par-un-vrai-secretLancement :
docker compose up -dTrois conteneurs décrits, un réseau privé créé, un volume persistant monté, et la base démarre avant l'API. Une commande.
Les services : un bloc par conteneur
Chaque entrée sous services devient un conteneur. Les clés les plus utiles :
image: l'image à télécharger (postgres:17). Toujours fixer une version.latestvous réserve des surprises le jour d'une mise à jour majeure.build: construire l'image depuis unDockerfile.build: .utilise le dossier courant.ports: publier un port sur la machine hôte, au formathôte:conteneur.restart:unless-stoppedredémarre le conteneur après un crash ou un reboot du serveur, sauf si vous l'avez arrêté vous-même.command: remplacer la commande de démarrage de l'image.
Un détail qui évite bien des failles : "8080:8080" publie le port sur toutes les interfaces réseau, donc sur Internet si le serveur est exposé. Docker contourne même le pare-feu UFW pour ça. Pour un outil d'administration, écrivez "127.0.0.1:8080:8080" : il n'est accessible que depuis le serveur lui-même.
Les réseaux : les conteneurs se parlent par leur nom
Compose crée un réseau privé pour chaque projet. Dans ce réseau, chaque service est joignable par son nom. C'est pour ça que l'API se connecte à db:5432 et pas à une adresse IP.
┌──────────────── réseau monprojet_default ────────────────┐
│ │
│ api ──────► db:5432 │
│ │ │
└────┼──────────────────────────────────────────────────────┘
│
port 3000 publié sur l'hôteLa base n'a pas de section ports : elle n'est pas joignable depuis l'extérieur, seulement par les autres services du projet. C'est le comportement à viser pour toute base de données.
Pour isoler davantage, on peut déclarer plusieurs réseaux, par exemple un réseau front pour le reverse proxy et l'API, et un réseau back pour l'API et la base. Sur la plupart des projets, le réseau par défaut suffit.
Les volumes : ce qui survit au conteneur
Un conteneur est jetable. Tout ce qu'il écrit disparaît quand on le supprime, sauf ce qui est stocké dans un volume. Deux types coexistent :
| Type | Syntaxe | Usage |
|---|---|---|
| Volume nommé | db-data:/var/lib/postgresql/data | Données gérées par Docker (bases, caches) |
| Bind mount | ./config:/app/config | Fichiers que vous éditez depuis l'hôte (configuration, code en dev) |
Les volumes nommés se déclarent en bas du fichier, sous la clé volumes. Docker les stocke dans /var/lib/docker/volumes/.
Ma règle : volume nommé pour les données qu'un programme écrit, bind mount pour les fichiers qu'un humain écrit. Et dans les deux cas, ces dossiers sont la seule chose à sauvegarder. Le reste se reconstruit à partir du compose.yaml.
Variables d'environnement et secrets
Compose remplace ${VARIABLE} par la valeur trouvée dans le fichier .env du dossier, ou dans l'environnement du shell. On peut fournir une valeur par défaut :
environment:
LOG_LEVEL: ${LOG_LEVEL:-info}Pour passer beaucoup de variables à un service sans les lister une par une, env_file charge un fichier entier :
services:
api:
env_file: .env.apiDeux réflexes à prendre :
- Ajouter
.envau.gitignore. Un mot de passe de base de données sur GitHub est trouvé par des robots en quelques minutes. - Commiter un
.env.exampleavec les noms des variables et des valeurs factices, pour que le projet reste documenté.
Pour vérifier le fichier final après remplacement des variables :
docker compose configCette commande affiche la configuration complète interprétée. C'est le premier réflexe quand quelque chose ne se comporte pas comme prévu.
depends_on et healthcheck : démarrer dans le bon ordre
depends_on: [db] seul garantit que le conteneur db démarre avant api. Pas qu'il soit prêt. PostgreSQL met quelques secondes à accepter des connexions, et une API qui se connecte au démarrage plante pendant ce temps.
La solution est la combinaison montrée plus haut : un healthcheck sur la base, et condition: service_healthy dans le depends_on de l'API. Compose attend que pg_isready réponde avant de lancer l'API.
Les commandes de healthcheck classiques :
| Service | Test |
|---|---|
| PostgreSQL | pg_isready -U utilisateur |
| MySQL / MariaDB | mysqladmin ping -h localhost |
| Redis | redis-cli ping |
| Application HTTP | curl -f http://localhost:3000/health (si curl est dans l'image) |
Les commandes du quotidien
docker compose up -d # créer et démarrer en arrière-plan
docker compose ps # état des services
docker compose logs -f api # suivre les logs d'un service
docker compose exec db psql -U app # ouvrir un shell dans un conteneur
docker compose restart api # redémarrer un service
docker compose stop # arrêter sans supprimer
docker compose down # arrêter et supprimer conteneurs et réseau
docker compose --profile debug up -d # démarrer aussi les services du profil "debug"Les profils servent aux services optionnels. Dans l'exemple, Adminer a profiles: ["debug"] : il ne démarre pas avec un simple up, seulement quand on active le profil. Pratique pour garder des outils de diagnostic dans le fichier sans qu'ils tournent en permanence.
Attention à une variante : docker compose down -v supprime aussi les volumes nommés. Votre base de données part avec. Je ne tape jamais cette commande sur un serveur de production.
Mettre à jour une application
C'est là que Compose montre sa valeur sur un serveur self-hosted :
docker compose pull # télécharger les nouvelles versions des images
docker compose up -d # recréer uniquement les conteneurs dont l'image a changé
docker image prune -f # supprimer les anciennes imagesCompose compare l'état voulu au fichier et ne recrée que ce qui a changé. Les volumes restent en place, les données aussi.
Pour une mise à jour majeure (PostgreSQL 16 vers 17, par exemple), lisez les notes de version. Certaines images exigent une migration manuelle des données, et un simple changement de tag peut empêcher la base de démarrer.
Développement et production avec le même fichier
Compose fusionne automatiquement compose.yaml avec compose.override.yaml s'il existe. On met dans le premier ce qui est commun, et dans le second ce qui ne sert qu'en développement :
services:
api:
build:
target: dev
volumes:
- ./src:/app/src
environment:
NODE_ENV: developmentEn local, docker compose up charge les deux fichiers. En production, on ignore l'override en précisant le fichier :
docker compose -f compose.yaml up -dCompose ou autre chose ?
Compose gère des conteneurs sur une seule machine. C'est sa force et sa limite.
| Besoin | Outil |
|---|---|
| Une application sur un serveur ou en local | Docker Compose |
| Gérer plusieurs stacks Compose depuis une interface web | Portainer |
| Plusieurs serveurs, haute disponibilité, montée en charge automatique | Kubernetes |
Pour 95 % des projets personnels et des petites équipes, Compose suffit. Passer à Kubernetes sans besoin réel de multi-serveurs, c'est ajouter une couche de complexité pour rien.
Docker Compose transforme une liste de commandes fragiles en un fichier qu'on peut lire, versionner et rejouer. Un compose.yaml bien écrit tient lieu de documentation d'infrastructure : services, ports, données, dépendances, tout y est. La seule chose qu'il ne protège pas, ce sont les volumes eux-mêmes, et c'est pour ça que sauvegarder son serveur avec Restic est l'étape à faire juste après. Et quand une stack a besoin d'une base, les bases de PostgreSQL restent le meilleur point de départ.