1. compose.yaml en tant que description de l’environnement
Si avant cela, Docker était pour vous un ensemble de commandes, alors Compose ajoute exactement une idée clé : « que l’environnement vive dans un fichier, et que les commandes soient courtes ». Il est important de ne pas confondre : Compose ne remplace pas Docker Engine et ne crée pas un « autre Docker ». Il vous offre simplement un moyen pratique de décrire les conteneurs et leurs paramètres, sans avoir à les garder en tête ni à les copier dans des chats.
Que fait Docker Compose en substance ? Il lit le YAML et le transforme en exactement les mêmes actions que vous auriez faites à la main : construire une image (si nécessaire), créer un réseau (si nécessaire), créer un volume (si nécessaire), lancer le(s) conteneur(s) avec les bons ports, variables et montages. Autrement dit, Compose n’est pas un « second système », mais une « couche normale au-dessus de ce que vous faisiez déjà à la main ».
Il est pratique de se représenter cela ainsi :
flowchart TD A["compose.yaml (description)"] --> B["docker compose up"] B --> C["Docker Engine : construction de l'image (optionnel)"] B --> D["Docker Engine : création du réseau (optionnel)"] B --> E["Docker Engine : création des volumes (optionnel)"] B --> F["Docker Engine : exécution des conteneurs"] F --> G["Vous vérifiez l'API /actuator/health et les logs"]
Dans le cadre de cette journée, nous maintenons volontairement un environnement simple : un seul service app, sans base de données externe ni « zoo de dépendances ». Cela aidera à comprendre la mécanique de Compose sur un exemple pur, sans se noyer dans ce qui ne démarre pas : l’application ou la base.
2. Noms canoniques et commande Compose
Lorsque vous commencez à chercher des exemples sur Internet, le monde se divise en deux camps : certains montrent le fichier docker-compose.yml, d’autres compose.yaml. Et parfois on rencontre la commande docker-compose au lieu de docker compose. Ce n’est pas parce que les gens aiment le chaos (même si parfois on le croirait). C’est parce que Compose a historiquement évolué, et aujourd’hui nous avons une voie canonique moderne.
Dans ce cours, nous fixons une base de référence : le fichier s’appelle compose.yaml, la commande est docker compose (avec un espace). Cela correspond à la version moderne Compose v2, qui fait partie de Docker CLI. Du point de vue de votre future expérience « adulte », c’est avantageux : moins de confusion, moins de « mais j’ai une autre version », moins de « pourquoi la commande n’est pas trouvée ».
Concrètement, cela signifie trois règles simples.
Première règle : le fichier se trouve à la racine du dépôt, à côté de Dockerfile, .dockerignore, README.md. Ne le cachez pas dans docker/compose/compose.yaml, à moins que vous ne donniez un cours séparé sur la recherche de fichiers.
Deuxième règle : vous lancez Compose depuis le répertoire du projet (ou vous spécifiez explicitement le fichier). Le scénario le plus fréquent dans un projet pédagogique ressemblera à ceci :
cd docker-java-catalog-service # On se rend dans le répertoire du projet (où se trouve compose.yaml)
docker compose up # On monte l'environnement selon la description de compose.yaml
Troisième règle : nous ne transformons pas le cours en archéologie, mais le contexte est utile. L’ancien nom docker-compose.yml et la commande docker-compose sont une voie héritée que vous pouvez rencontrer dans d’autres dépôts. Savoir les lire est utile. Mais nous ne les utilisons pas comme chemin d’apprentissage principal, afin de ne pas multiplier les variantes.
3. YAML dans Compose : l’indentation est vos nouvelles « parenthèses »
Le fichier Compose est du YAML. Et YAML est un format qui semble amical… jusqu’à ce que vous ajoutiez un espace de trop. Si en Java vous oubliez de fermer une accolade, le compilateur vous dit : « mon ami, tu n’as pas fermé le bloc ». En YAML, le rôle des « parenthèses » est joué par l’indentation. Et oui, c’est ce rare cas où les espaces sont vraiment importants, et pas seulement « pour faire joli ».
Convenons d’une grammaire minimale YAML nécessaire spécifiquement pour Compose.
Clé-valeur – c’est une « map » (dictionnaire). Exemple :
services: # Section des services de l'environnement
app: # Nom du service (nom logique dans Compose)
build: . # Construire l'image à partir du répertoire actuel (Dockerfile par défaut)
Ici, services est une clé de premier niveau, et la valeur est un objet imbriqué. services a une clé app, et app a une clé build.
Liste – c’est un « array » (tableau) avec -. Exemple avec les ports :
ports: # Liste des redirections de ports
- "8080:8080" # host:container (à gauche le port sur votre machine, à droite dans le conteneur)
ports est une liste où chaque élément est une chaîne. Pourquoi une chaîne entre guillemets ? Parce que c’est plus fiable et plus lisible. YAML est certes intelligent, mais nous ne rivalisons pas de QI avec lui – nous voulons que le fichier soit lu de la même manière sur toutes les machines et par tous les étudiants.
Et voici un conseil pratique important : quand le fichier se brise à cause de l’indentation, vous obtenez généralement une erreur du genre mapping values are not allowed here ou quelque chose d’aussi « poétique ». Dans ces moments-là, il ne faut pas ressasser tous les péchés des deux dernières semaines. Il suffit de lire le fichier de haut en bas et de vérifier que les niveaux d’imbrication sont logiques.
Squelette minimal de compose.yaml
Le fichier Compose se lit presque toujours à partir de la section services. C’est le point d’entrée. On pourrait dire que services est comme public static void main dans le monde de l’environnement : c’est là que commence la « vie ».
Le fichier Compose minimal qui ait un sens ressemble à ceci :
services: # Toujours commencer par la description des services
app: # Nom du service (vous vous y référerez dans les commandes : logs, exec, etc.)
build: . # Source de construction de l'image (contexte)
Oui, c’est tout. Trois lignes et cela a déjà un sens : « il y a un service app, il est construit à partir du répertoire actuel ». Et maintenant, docker compose up --build fera ce que vous faisiez à la main : docker build … et docker run …
Le plus souvent, dans un projet réel, vous ajouterez ports, car sinon le service se lance mais n’est « visible que de lui-même » (du conteneur). On obtient donc ce minimum de base :
services:
app:
build: .
ports:
- "8080:8080" # Publier le port vers l'extérieur : localhost:8080 -> conteneur:8080
Maintenant, il y a un « pont vers l’extérieur » : le port hôte 8080 est redirigé vers le conteneur sur 8080.
Notez que le fichier Compose n’est pas l’endroit où vous « inventez une nouvelle façon de lancer l’application ». Si vous avez déjà un ENTRYPOINT correct dans le Dockerfile, Compose n’a pas à le répéter. Compose doit dire : quelle image lancer et quels paramètres d’exécution ajouter.
4. build et image : ne pas confondre
L’une des premières confusions fréquentes : « est-ce que je mets build ou image ? » Ce n’est pas une question de goût. Ce sont deux scénarios différents.
build signifie : « construis l’image à partir des sources ». Compose utilisera le contexte de construction et le Dockerfile (par défaut Dockerfile dans le répertoire spécifié). Exemple simple :
services:
app:
build: . # Construire l'image localement à partir du Dockerfile et du contexte du projet
Nous disons donc : « prends le Dockerfile du projet actuel et construis ».
image signifie : « prends une image déjà prête ». En général, soit vous l’avez déjà construite auparavant (docker build -t ...), soit elle se trouve dans un registre. Exemple :
services:
app:
image: docker-java-catalog-service:latest # Prendre une image existante par son nom:tag
Et c’est important : image ne « construit » pas, il « utilise ». Donc si l’image n’existe pas localement et ne peut pas être tirée, le lancement échouera.
Il existe un troisième cas, très pratique : utiliser build et image ensemble – par exemple, build: . plus image: docker-java-catalog-service. Alors build gère la source de construction, et image gère le nom du résultat. Pour le développement local, c’est pratique : Compose construit lui-même le service et enregistre simultanément le résultat sous un nom prévisible, indépendant du nom du répertoire du projet.
Dans un projet pédagogique, au début de l’apprentissage de Compose, il est souvent plus commode de commencer avec build: ., car cela nécessite moins d’étapes : vous n’avez pas à vous rappeler sous quel tag vous avez construit l’image. Compose construit et lance tout seul. Mais il est utile de comprendre les deux modes, car dans la vraie vie, vous utiliserez les deux : build pour le développement local, image quand vous voulez exécuter un artefact pré-construit et fixe.
Si vous souhaitez rendre le build un peu plus explicite (et un peu moins magique), vous pouvez l’écrire ainsi :
services:
app:
build:
context: . # Où se trouve le contexte de construction (ce qu'on envoie au démon Docker)
dockerfile: Dockerfile # Indiquer explicitement le nom du Dockerfile
C’est plus long, mais cela se lit comme un « contrat ». Cependant, au début du cours, ce n’est pas obligatoire – parfois la concision est plus utile que la rigueur idéologique.
5. Paramètres du service : ports, env, volumes
Quand vous passez de docker run à Compose, le moment le plus agréable est que vous commencez à voir les paramètres de lancement comme une structure. Là où il y avait avant un long one-liner, il y a maintenant du YAML où l’on voit d’un coup d’œil : « les ports sont ici, les variables sont ici, les volumes sont ici ».
Pour que le cerveau s’habitue plus vite, on peut garder un petit tableau de correspondance. Ce n’est pas un « guide complet de Compose », mais plutôt un pont entre les drapeaux déjà connus de docker run et les clés YAML :
| Ce que vous faisiez avec docker run | À quoi cela ressemble dans compose.yaml | Signification en langage clair |
|---|---|---|
| -p 8080:8080 | ports: ["8080:8080"] | « Ouvrir le port sur l’hôte et le transférer dans le conteneur » |
| -e SPRING_PROFILES_ACTIVE=standalone | environment: { SPRING_PROFILES_ACTIVE: standalone } | « Placer une variable d’environnement à l’intérieur du conteneur » |
| -v ./data/exports:/data/exports | volumes: ["./data/exports:/data/exports"] | « Monter un répertoire hôte dans le conteneur » |
Un service très simple avec un port et une variable d’env ressemble à ceci :
services:
app:
build: .
ports:
- "8080:8080"
environment:
SPRING_PROFILES_ACTIVE: standalone
C’est déjà suffisant pour voir la structure : ports gère l’entrée externe dans le conteneur, environment les variables d’environnement à l’intérieur. Si le service a besoin de montages bind ou de paramètres supplémentaires, ils s’ajoutent simplement dans les sections voisines volumes et environment, et la structure du fichier reste la même.
On voit bien ici deux points importants.
Premièrement, les « paramètres d’environnement » sont séparés par sens : les ports à part, les variables à part, les volumes à part. Vous ne cherchez pas des yeux le \ et vous ne cherchez pas dans la ligne où se trouve -e et où se trouve -v.
Deuxièmement, Compose ne vous oblige pas à retenir l’ordre des drapeaux. Avec docker run, on peut écrire -p avant -e ou après – Docker comprendra. Mais un humain lit cela difficilement. YAML est par nature structurel : on regroupe par sections, pas par ordre.
6. Niveau supérieur : networks et volumes
Même si aujourd’hui nous n’avons qu’un seul service, vous voyez déjà que Compose a des entités « de niveau environnement », pas « de niveau conteneur ». C’est une idée importante, car Compose n’est pas un « lancement d’un seul conteneur », mais une « description d’un banc d’essai ».
Il y a des sections de niveau supérieur qui apparaissent souvent :
services – qui participe à l’environnement (conteneurs) ;
networks – comment ils sont connectés en réseau ;
volumes – comment l’état/les données sont stockés via des volumes nommés.
Aujourd’hui, il suffit de comprendre le point structurel : ces sections s’écrivent au même niveau d’imbrication que services, et non à l’intérieur de services.app. C’est-à-dire comme ceci (correct) :
services:
app:
build: .
volumes:
exports-data: # Déclaration du volume nommé au niveau supérieur (en tant qu'entité d'environnement)
Alors que ceci (incorrect, et YAML/Compose n’apprécieront pas) :
services:
app:
build: .
volumes:
exports-data: # Erreur : ici on attend une liste de connexions, pas une déclaration de volume
À l’intérieur du service, vous montez un volume, et vous le déclarez à l’extérieur. Pourquoi ? Parce que la déclaration signifie « nous créons une entité d’environnement », et le montage signifie « nous utilisons l’entité d’environnement dans un service spécifique ». Cette séparation ressemble à Java : on déclare une classe séparément, puis on l’utilise à des endroits précis.
Je souligne encore : nous n’approfondissons pas aujourd’hui le modèle réseau et le DNS. Nous fixons simplement : si vous voyez networks: et volumes: au niveau supérieur, c’est normal, c’est ainsi que cela doit être. Ce n’est pas de la « bureaucratie superflue », mais une structure qui sauvera plus tard le projet du chaos YAML.
7. Cycle de travail minimal : up, logs, down
Le fichier lui-même ne lance rien. Il est comme build.gradle.kts : tant que vous n’avez pas appelé la commande, ce n’est que du texte. Il est donc important de connaître le « cycle de travail » minimal qui remplace les commandes manuelles docker build + docker run.
Le scénario le plus basique ressemble à ceci :
docker compose up --build # Monter l'environnement et, si nécessaire, reconstruire l'image
L’option --build est utile quand vous utilisez build: . et que vous voulez garantir que l’image sera reconstruite si vous avez modifié le code ou le Dockerfile. Au début de l’apprentissage, cela rend le comportement plus prévisible : moins de chances de « lancer accidentellement une ancienne image ».
Une fois l’environnement monté, vous voudrez presque immédiatement voir les logs :
docker compose logs app # Afficher les logs du service app (le nom provient de compose.yaml)
app est le nom du service (pas du conteneur). C’est là tout l’intérêt de Compose : vous pensez en « services d’environnement », pas en « conteneurs aléatoires ».
Quand vous devez arrêter l’environnement, utilisez :
docker compose down # Arrêter et supprimer les conteneurs/réseaux créés (par défaut)
Et c’est là une sensation très agréable : vous ne cherchez pas comment s’appelait exactement le conteneur, vous ne vous souvenez pas de docker stop ... et docker rm .... Compose sait lui-même ce qu’il a créé et le nettoie proprement.
Si vous aimez « garder la fenêtre du terminal propre », vous pouvez lancer en arrière-plan (detached) :
docker compose up -d --build # Lancement en arrière-plan pour libérer le terminal immédiatement
Mais au début de l’apprentissage, il est souvent plus pratique de ne pas utiliser -d, afin de voir les logs directement dans la console et de relier plus rapidement l’action au symptôme.
8. Erreurs typiques lors de la création et de la lecture de compose.yaml
Erreur n°1 : « J’ai écrit du YAML, mais Compose m’insulte avec des mots incompréhensibles. »
C’est généralement l’indentation. YAML ne pardonne pas un « niveau légèrement décalé », car le niveau d’imbrication est le sens même. Le moyen le plus efficace de s’en sortir n’est pas de deviner, mais d’ouvrir le fichier et de lire : services → app → clés du service. Si une clé se retrouve soudainement à un autre niveau, vous le verrez. Et oui, il arrive que ce soit littéralement un seul espace qui vous coûte 20 minutes et quelques questions philosophiques sur l’univers.
Erreur n°2 : confusion entre build et image – « Je pensais que ça se construirait tout seul. »
Si vous avez écrit image: docker-java-catalog-service, Compose n’est pas obligé de construire cette image. Il est obligé d’essayer de l’utiliser. Si l’image n’existe pas, vous obtiendrez une erreur. Si vous voulez une construction à partir du dépôt actuel, utilisez build: .. Inversement, si vous voulez exécuter strictement un artefact spécifique et prêt (surtout en équipe), image devient plus honnête et prévisible.
Erreur n°3 : s’attendre à ce que Compose « comprenne tout tout seul », même si je n’ai pas décrit les ports.
Compose n’est pas télépathe. Si vous n’avez pas spécifié ports, l’application peut très bien fonctionner à l’intérieur du conteneur, mais vous ne pourrez pas y accéder de l’extérieur via localhost:8080. Ce n’est pas une panne de Spring Boot, c’est simplement l’absence de publication du port. Compose ne publie pas les ports « par défaut », car ce serait non sécurisé et inattendu.
Erreur n°4 : essayer de faire un premier compose.yaml trop gros.
On a très envie d’ajouter tout de suite des sections « pour plus tard » : networks, volumes, restart-policies, healthchecks, profiles, depends_on, et cinq autres choses que vous avez vues dans un dépôt quelconque. En pratique, cela tue l’apprentissage. Le bon premier fichier se lit d’un coup d’œil et répond à la question : « comment lancer notre service dans le mode dont nous avons besoin maintenant ». Tout le reste est du bruit superflu, qu’il sera difficile de distinguer de ce qui est vraiment important.
Erreur n°5 : mélanger les « paramètres Compose » et les « paramètres de l’application » en un seul fouillis.
Compose gère la façon dont le conteneur est lancé : ports, variables d’environnement, montages. Spring Boot gère le comportement de l’application à l’intérieur du conteneur : profil, port, source de données, etc. Dans le fichier Compose, vous pouvez passer des valeurs à l’intérieur du conteneur via environment, mais si vous commencez à essayer de « configurer Spring Boot avec les clés de Compose » (ou l’inverse), vous cesserez rapidement de comprendre où se trouve la cause du comportement.
GO TO FULL VERSION