Chapitre 819 minIntermédiaire
Docker Compose : écrire un fichier compose.yaml
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- écrire un fichier compose.yaml actuel, sans la clé version ;
- utiliser les variables et le fichier .env sans exposer de secret ;
- faire attendre un service que sa base soit prête ;
- démarrer, mettre à jour et arrêter une application avec Compose.
4 exercices corrigés
Docker Compose est l'outil qui décrit une application de plusieurs conteneurs dans un seul fichier YAML, compose.yaml, et qui la démarre, la met à jour ou l'arrête d'une seule commande (docker compose up -d). Ce chapitre présente la syntaxe actuelle (sans la clé version, devenue obsolète), les clés d'un service, les volumes et les réseaux, les variables d'environnement et le fichier .env, l'ordre de démarrage avec les contrôles de santé, puis les commandes du quotidien.
Pourquoi Compose
Lancer l'API et sa base à la main demande déjà quatre commandes longues : un réseau, un volume, deux docker run avec leurs options. Les retaper à l'identique à chaque mise à jour, sur chaque serveur, est source d'erreurs. Compose remplace ces commandes par un fichier déclaratif : tu décris l'état voulu (quels services, avec quelles images, quels volumes, quels réseaux), et docker compose up -d crée ou recrée ce qui doit l'être. Le fichier se versionne avec le code dans Git : c'est la documentation exacte du déploiement.
Compose aujourd'hui : ce qui a changé
| Ancienne pratique | Pratique actuelle |
|---|---|
Commande docker-compose (avec tiret), écrite en Python | Commande docker compose (avec une espace), intégrée à Docker |
Fichier docker-compose.yml | Fichier compose.yaml (les anciens noms restent lus) |
Première ligne version: "3.8" | Pas de clé version : le format suit la Compose Specification, qui évolue en continu |
links: pour relier les conteneurs | Le réseau par défaut du projet et son DNS interne |
depends_on: qui n'attend que le lancement | depends_on avec condition: service_healthy |
Avec un fichier qui commence encore par version:, Compose avertit :
WARN[0000] /srv/gestion/docker-compose.yml: the attribute `version` is obsolete, it will be ignored, please remove it to avoid potential confusion
Supprime simplement la ligne : Compose valide toujours le fichier avec le schéma le plus récent, quelle que soit la valeur écrite.
Un premier fichier compose.yaml
Voici l'API sorbier-frais et sa base, pour un environnement de développement :
# compose.yaml
services:
api:
build: .
ports:
- "127.0.0.1:3000:3000"
environment:
DB_HOST: bdd
DB_NAME: frais
DB_USER: frais
DB_PASSWORD: ${DB_PASSWORD:?définis DB_PASSWORD dans .env}
depends_on:
bdd:
condition: service_healthy
restart: unless-stopped
bdd:
image: mysql:8.4
environment:
MYSQL_DATABASE: frais
MYSQL_USER: frais
MYSQL_PASSWORD: ${DB_PASSWORD:?définis DB_PASSWORD dans .env}
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:?définis DB_ROOT_PASSWORD dans .env}
volumes:
- mysql-donnees:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "--silent"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
restart: unless-stopped
volumes:
mysql-donnees:
Et à côté, le fichier .env, jamais versionné (ajoute-le au .gitignore) :
DB_PASSWORD=7uQ2-mVx9-Lr4k-Zp8s
DB_ROOT_PASSWORD=Jw3n-Tb6c-Hq1e-Fy5d
YAML se lit à l'indentation : deux espaces par niveau, jamais de tabulation, et un tiret pour chaque élément de liste.
| Partie | Rôle |
|---|---|
services: | Les conteneurs de l'application ; chaque clé (api, bdd) est un service |
build: . | Construit l'image à partir du Dockerfile du dossier courant |
image: mysql:8.4 | Utilise une image existante |
ports: | Publication de ports ; l'API n'est publiée que sur la boucle locale, pour les tests |
environment: | Variables du conteneur ; ${…} est remplacé par Compose au moment de lire le fichier |
DB_HOST: bdd | Le nom du service sert de nom d'hôte sur le réseau du projet |
depends_on … service_healthy | L'API ne démarre que quand le contrôle de santé de la base réussit |
volumes: (dans le service) | Monte le volume nommé mysql-donnees |
volumes: (en bas) | Déclare les volumes nommés du projet |
Les commandes du quotidien
Toutes les commandes se lancent dans le dossier du compose.yaml :
| Commande | Effet |
|---|---|
docker compose up -d | Crée réseau, volumes et conteneurs, construit les images si besoin, démarre tout en arrière-plan |
docker compose ps | L'état des services, avec la santé |
docker compose logs -f api | Les journaux d'un service, en continu |
docker compose exec bdd mysql -ufrais -p frais | Une commande dans un service qui tourne |
docker compose run --rm api node scripts/migrer.js | Un conteneur ponctuel à partir de la définition d'un service |
docker compose build | (Re)construit les images des services qui ont un build: |
docker compose pull | Télécharge les nouvelles versions des images |
docker compose restart api | Redémarre un service, sans relire le fichier |
docker compose stop | Arrête les conteneurs sans les supprimer |
docker compose down | Arrête et supprime conteneurs et réseau ; garde les volumes |
docker compose down -v | Supprime aussi les volumes : les données partent |
docker compose config | Affiche le fichier final, variables remplacées : le premier réflexe de diagnostic |
docker compose ls | Les projets Compose de l'hôte |
docker compose up -d
[+] Running 4/4
✔ Network sorbier-frais_default Created 0.1s
✔ Volume sorbier-frais_mysql-donnees Created 0.0s
✔ Container sorbier-frais-bdd-1 Healthy 21.4s
✔ Container sorbier-frais-api-1 Started 21.6s
Le projet, ses noms et son réseau
Compose regroupe tout sous un nom de projet, par défaut le nom du dossier (sorbier-frais ici), et nomme les objets à partir de lui :
| Objet | Nom |
|---|---|
| Réseau par défaut | sorbier-frais_default |
Volume mysql-donnees | sorbier-frais_mysql-donnees |
| Conteneurs | sorbier-frais-api-1, sorbier-frais-bdd-1 |
Tous les services d'un projet sont branchés sur son réseau par défaut et se joignent par leur nom de service. Fixe le nom avec la clé name: sorbier-frais en tête de fichier, pour qu'il ne dépende pas du dossier. Évite container_name: : un nom fixe empêche de lancer deux copies du projet et n'apporte rien, puisqu'on désigne les services par leur nom dans toutes les commandes docker compose.
Attendre qu'un service soit prêt
depends_on seul ne fait qu'ordonner le lancement : l'API démarre dès que le conteneur de la base est lancé, alors que MySQL met une vingtaine de secondes à initialiser sa base la première fois. L'API tente de se connecter, échoue et s'arrête.
Avec condition: service_healthy, Compose attend que le contrôle de santé de la base réussisse. Le test mysqladmin ping -h 127.0.0.1 passe par TCP : pendant l'initialisation, l'image fait tourner un serveur temporaire qui n'écoute pas le réseau, donc le test échoue tant que le vrai serveur n'est pas prêt. mysqladmin ping renvoie 0 dès que le serveur répond, même s'il refuse l'authentification : aucun mot de passe n'est nécessaire dans le test.
| Condition | Compose attend que la dépendance… |
|---|---|
service_started (par défaut) | soit lancée |
service_healthy | soit healthy |
service_completed_successfully | se termine avec le code 0 (une migration, par exemple) |
Variables, .env et env_file
Compose lit le fichier .env du dossier du projet et s'en sert pour remplacer les ${VAR} du compose.yaml. Ce fichier ne passe pas tel quel dans les conteneurs : seules les variables listées sous environment: y arrivent.
| Écriture | Résultat |
|---|---|
${DB_PASSWORD} | La valeur, ou une chaîne vide (avec un avertissement) si elle manque |
${TZ:-Europe/Paris} | La valeur, ou Europe/Paris si elle est vide ou absente |
${DB_PASSWORD:?message} | La valeur, ou un arrêt avec ce message si elle est vide ou absente |
La clé env_file: est différente : elle charge un fichier de variables directement dans le conteneur, sans passer par le remplacement. Elle sert pour une longue liste de réglages applicatifs.
services:
api:
env_file: config/api.env
Vérifie toujours le résultat avant de lancer :
docker compose config
La commande affiche le fichier complet, variables remplacées, ou l'erreur exacte (indentation, variable manquante).
Les autres clés utiles
| Clé | Rôle | Exemple |
|---|---|---|
command: | Remplace le CMD de l'image | command: ["node", "src/worker.js"] |
user: | Utilisateur du processus | user: "1000:1000" |
networks: | Réseaux du service (et déclaration en bas de fichier) | Voir le chapitre 9 |
secrets: | Secrets montés dans /run/secrets/ | Voir le chapitre 11 |
deploy.resources.limits | Limites de mémoire et de processeur | memory: 512M, cpus: "1.0" |
read_only:, tmpfs: | Système de fichiers en lecture seule, dossiers temporaires en mémoire | read_only: true |
logging: | Pilote et options de journalisation du service | driver: local |
profiles: | Service lancé seulement avec --profile | profiles: ["outils"] |
labels: | Étiquettes, lues par certains outils | com.sorbier.equipe: "dev" |
Plusieurs fichiers. Compose fusionne automatiquement compose.yaml et compose.override.yaml s'il existe. On garde ainsi un fichier commun et un fichier de surcharge pour le développement (ports publiés, montage du code). En production, on précise les fichiers : docker compose -f compose.yaml -f compose.prod.yaml up -d.
Compose Watch. En développement, la section develop.watch d'un service synchronise les fichiers modifiés dans le conteneur, ou reconstruit l'image, pendant que docker compose watch tourne : une alternative au montage du code.
Mettre à jour une application
docker compose pull # nouvelles images (services avec image:)
docker compose build # nouvelles images construites (services avec build:)
docker compose up -d # recrée uniquement les services dont l'image ou la config a changé
docker image prune # supprime les anciennes images sans étiquette
up -d compare l'état voulu à l'état réel : un service dont rien n'a changé n'est pas touché. Le volume de la base est conservé, puisqu'il n'est pas supprimé par la recréation des conteneurs.
Exercices
Exercice 1 sur 4 Moderniser un ancien fichier Réussi À revoir
Ce fichier docker-compose.yml date de 2019. Liste ce qui est obsolète ou fragile, puis réécris-le en compose.yaml actuel.
version: "3.7"
services:
web:
build: .
container_name: web
ports:
- "80:3000"
links:
- db
depends_on:
- db
environment:
- DB_HOST=db
- DB_PASSWORD=secret123
db:
image: mysql
environment:
- MYSQL_ROOT_PASSWORD=secret123
Voir la réponseMasquer la réponse de l'exercice 1
Problèmes :
version: "3.7"est obsolète : Compose l'ignore et affiche un avertissement.links:est inutile : les services se joignent par leur nom sur le réseau du projet.depends_on: - dbn'attend que le lancement de MySQL, pas qu'il soit prêt.image: mysqlsans étiquette prendlatest: version imprévisible.- Aucun volume : les données partent avec le conteneur.
- Mots de passe en clair dans le fichier versionné.
container_name:fige les noms sans utilité.
services:
web:
build: .
ports:
- "80:3000"
environment:
DB_HOST: db
DB_PASSWORD: ${DB_PASSWORD:?définis DB_PASSWORD dans .env}
depends_on:
db:
condition: service_healthy
restart: unless-stopped
db:
image: mysql:8.4
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD:?définis DB_PASSWORD dans .env}
volumes:
- db-donnees:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "--silent"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
restart: unless-stopped
volumes:
db-donnees:
Encore mieux : un compte MySQL dédié à l'application plutôt que root (MYSQL_USER, MYSQL_PASSWORD), et l'application derrière un reverse proxy HTTPS plutôt que publiée en HTTP sur le port 80 (chapitre 9).
Tu avais trouvé ?
Exercice 2 sur 4 Traduire des commandes en Compose Réussi À revoir
Traduis ces commandes en un fichier compose.yaml équivalent, avec le nom de projet wiki.
docker network create wiki_default
docker volume create wiki_pages
docker run -d --name wiki-app-1 --network wiki_default \
-p 127.0.0.1:8081:80 -v wiki_pages:/var/www/html/data \
-e TZ=Europe/Paris --restart unless-stopped \
ghcr.io/sorbier/wiki:1.4
Voir la réponseMasquer la réponse de l'exercice 2
name: wiki
services:
app:
image: ghcr.io/sorbier/wiki:1.4
ports:
- "127.0.0.1:8081:80"
volumes:
- pages:/var/www/html/data
environment:
TZ: Europe/Paris
restart: unless-stopped
volumes:
pages:
Compose crée lui-même le réseau wiki_default, le volume wiki_pages et le conteneur wiki-app-1 : les noms des commandes d'origine sont exactement ceux qu'il produit pour le projet wiki, le service app et le volume pages.
Tu avais trouvé ?
Exercice 3 sur 4 Les variables Réussi À revoir
Le fichier .env contient TZ= (vide) et ne contient pas DB_PASSWORD. Que devient chaque ligne au moment de docker compose up ?
TZ: ${TZ:-Europe/Paris}TZ: ${TZ-Europe/Paris}DB_PASSWORD: ${DB_PASSWORD}DB_PASSWORD: ${DB_PASSWORD:?mot de passe manquant}
Voir la réponseMasquer la réponse de l'exercice 3
Europe/Paris: avec:-, la valeur par défaut s'applique si la variable est vide ou absente. Ici elle est vide.- Une chaîne vide : sans les deux-points, la valeur par défaut ne s'applique que si la variable est absente. Elle existe (vide), donc on garde le vide.
- Une chaîne vide, et Compose affiche un avertissement :
The "DB_PASSWORD" variable is not set. Defaulting to a blank string. - Compose refuse de démarrer et affiche le message
mot de passe manquant. C'est la forme à préférer pour un secret : mieux vaut un arrêt net qu'une base démarrée avec un mot de passe vide.
Tu avais trouvé ?
Exercice 4 sur 4 down ou down -v ? Réussi À revoir
Pour chaque situation, choisis entre docker compose stop, docker compose down et docker compose down -v.
- Tu changes le port publié de l'API dans
compose.yamlet tu veux appliquer le changement. - Tu veux libérer la mémoire de srv-gestion pendant une heure de maintenance, puis tout relancer à l'identique.
- En développement, ta base de test est corrompue et tu veux repartir d'une base vide.
- L'application est retirée du serveur, après un export de la base archivé ailleurs.
Voir la réponseMasquer la réponse de l'exercice 4
- Aucune des trois n'est nécessaire :
docker compose up -dsuffit, il recrée le service modifié. Undownsuivi d'unup -dfonctionne aussi, avec une coupure plus longue. docker compose stop: les conteneurs sont arrêtés mais gardés,docker compose startles relance tels quels. (downconviendrait aussi : les volumes sont gardés etup -drecrée les conteneurs.)docker compose down -v: la suppression du volume est voulue, la base sera réinitialisée au prochainup.docker compose down -v, après avoir vérifié que l'export est lisible et archivé hors du serveur.
Tu avais trouvé ?