1. Compose : Spring plante à cause du YAML
Lorsque vous exécutez le Container-Ready Catalog Service localement via bootRun, vous avez une image familière : si ça plante, c’est dans le code ou dans la configuration de l’application. Compose casse cette habitude de manière très douce et très perfide : vous ne modifiez pas le code Java, mais l’environnement, et l’application échoue honnêtement… il semble donc que « Spring Boot soit cassé ». En réalité, Spring joue ici le rôle d’un thermomètre qui affiche le premier la température, mais n’est pas la cause du rhume.
Dans Compose, vous avez des voisins : postgres, redis, rabbitmq. Vous avez le réseau Compose, les volumes, plusieurs fichiers Compose (compose.yaml et compose.dev.yaml), la substitution de variables depuis .env. Et vous ne vivez plus dans une « seule application », mais dans une petite ville. Si l’eau est coupée dans la ville (si postgres ne démarre pas), le premier à souffrir est celui qui essayait de se laver les mains (votre application), et elle proteste comme si « sa salle de bain était cassée ». Par conséquent, dans Compose, il est très facile de confondre le lieu de manifestation de l’erreur avec sa cause première.
L’idée pratique la plus importante ici est simple : dans un environnement multi-conteneurs, une partie des erreurs doit être diagnostiquée en dehors de l’application aussi attentivement qu’à l’intérieur. Si vous voyez une stack trace dans les logs de app, cela ne signifie pas que « le code est en faute ». Cela signifie seulement une chose : app rapporte honnêtement qu’on ne lui a pas donné ce qu’il attend de l’environnement.
Avec les pannes isolées, c’était relativement honnête : le symptôme se trouvait généralement non loin de la cause. Dans Compose, apparaît un piège distinct : l’erreur vit dans le modèle de lancement, mais la stack trace que vous voyez en premier est dans app. Il est donc important de garder à l’esprit deux niveaux : ce que Compose va lancer d’après le YAML et ce qui existe réellement dans les conteneurs créés.
2. YAML vs runtime : ce qui a été lancé
L’une des raisons pour lesquelles une erreur Compose se déguise si facilement en erreur d’application est le décalage entre ce que vous pensez exécuter et ce qui résulte réellement après toutes les fusions. Dans l’esprit d’un débutant, compose.yaml est la vérité, et les conteneurs ne sont que son « exécution ». Dans la réalité, Compose ressemble plutôt à une compilation : vous avez des sources (compose.yaml, compose.dev.yaml, .env) et en sortie vous avez une configuration finale « compilée ». C’est sur celle-ci que Compose crée les conteneurs.
Voici une petite analogie (et utile) : compose.yaml est comme du code Java, et docker compose config est comme le bytecode après compilation. Vous pouvez regarder le source mille fois et dire « tout est correct ici », mais ce qui sera exécuté est ce qui résulte des substitutions et des fusions.
Dans le monde Compose, plusieurs raisons expliquent souvent pourquoi « ce qui est écrit » n’est pas égal à « ce qui est obtenu » :
Premièrement, plusieurs fichiers Compose. Dans notre cours, c’est un canon : il y a compose.yaml et compose.dev.yaml, et le second fichier remplace le premier. Si vous ne regardez que compose.yaml, vous voyez la moitié du tableau (un peu comme lire uniquement les pages impaires d’un livre et s’étonner que l’histoire soit étrange).
Deuxièmement, la substitution de variables. Compose peut remplacer ${SOME_VAR} par une chaîne vide si la variable n’est pas définie, ce qui crée des erreurs très « plausibles » au niveau de l’application. Et oui, l’application dira honnêtement « impossible de configurer la source de données », mais la cause première sera que la chaîne de connexion est devenue vide à cause de la substitution.
Troisièmement, les « vieux conteneurs ». Vous avez peut-être modifié l’environnement ou les volumes dans le YAML, mais les conteneurs en cours d’exécution ne sont pas toujours recréés automatiquement comme vous le pensez intuitivement. En conséquence, vous corrigez le YAML, mais en réalité vous continuez à observer le comportement de l’ancienne configuration.
Et ici, il est facile de confondre deux sources différentes de vérité. docker compose config affiche le modèle de lancement résolu : ce que Compose va construire à partir des fichiers, des overrides et des substitutions. docker inspect et, lorsque le conteneur est vivant, docker compose exec ... printenv montrent l’état réel du conteneur déjà créé : ce qu’il contient actuellement. Après une modification du YAML, ces niveaux peuvent diverger : la configuration souhaitée est déjà correcte, mais le conteneur est toujours l’ancien. Dans une telle situation, vous recréez d’abord le service nécessaire, puis vous répétez le même test qui échouait.
Par conséquent, dans le diagnostic Compose, vous avez une question obligatoire supplémentaire : qu’est-ce qui a réellement été appliqué.
3. docker compose config : la vérité fusionnée
Si vous retenez exactement une commande de cette leçon, que ce soit docker compose config. Elle n’est pas là « pour faire joli » ou pour du chamanisme DevOps. Elle montre la vérité résolue du modèle de lancement : ce que Compose va construire à partir des fichiers, des overrides et de .env. Ensuite, vous comparez cela avec l’état réel des conteneurs pour arrêter de vous disputer avec votre imagination.
Le point le plus important : lorsque vous avez deux fichiers, vous devez vérifier la configuration dans la même forme que celle avec laquelle vous lancez réellement la stack. Si vous montez le banc de développement ainsi :
# On lance la stack exactement avec les mêmes fichiers que ceux utilisés en dev
docker compose -f compose.yaml -f compose.dev.yaml up
alors vous devez aussi consulter « la vérité » de cette façon :
# Affiche la configuration finale (fusionnée) après tous les overrides et substitutions
docker compose -f compose.yaml -f compose.dev.yaml config
Cette commande vous montrera le YAML final après les substitutions et la fusion. Ensuite, vous pouvez chercher visuellement les éléments clés : quels ports sont réellement publiés, quelles variables d’environnement sont réellement transmises, quels volumes sont réellement montés, quelles conditions depends_on sont réellement définies.
Si, après avoir modifié le YAML, config montre déjà les bonnes variables d’environnement ou les bons montages, mais que l’application se comporte toujours comme avant, ce n’est pas une contradiction. Cela signifie qu’il est temps de comparer l’état réel via docker inspect et de recréer le service, plutôt que de réécrire le même YAML encore une fois.
Un déguisement très fréquent se produit lorsque les deux fragments YAML semblent « logiques » individuellement, mais ensemble donnent une construction incompatible. Par exemple, le fichier de base définit un chemin d’exportation, et le fichier de dev monte un autre chemin :
# compose.yaml (fragment)
services:
app:
environment:
# C'est le chemin où l'application tentera d'écrire des fichiers à l'intérieur du conteneur
APP_EXPORT_DIR: /app/exports
# compose.dev.yaml (fragment)
services:
app:
volumes:
# À gauche le chemin sur l'hôte, à droite le chemin dans le conteneur (et il est facile de se tromper sur la cible)
- ./data/exports:/app/export # notez : export, pas exports
Chaque morceau pris séparément semble plausible. Au final, l’application écrit dans /app/exports, alors que le mount est connecté à /app/export. Côté Java, vous verrez « le fichier n’apparaît pas » ou « Permission refusée », et il est très facile de commencer à fouiller le code d’exportation. En réalité, c’est un pur problème Compose, et docker compose config le montre littéralement sous vos yeux.
Un autre détail important qui perturbe souvent les étudiants : .env et environment: sont des niveaux différents. .env dans Compose participe d’abord à la substitution de valeurs dans le YAML, et n’est pas une « transmission automatique de toutes les variables à l’intérieur du conteneur ». Si vous vous attendez à ce que « puisque .env contient SPRING_PROFILES_ACTIVE=postgres, alors le conteneur le recevra forcément », vous pourriez avoir une surprise. Il faut vérifier de la même manière : via docker compose config et si nécessaire via docker exec … printenv.
4. Les masques de Compose dans les logs de Spring Boot
Dans un environnement multi-conteneurs, l’application devient une vitrine : elle affiche l’erreur en premier, car elle est la première à « essayer de fonctionner ». Mais la nature de l’erreur peut être du niveau Compose. Pour éviter de deviner, il est utile d’avoir en tête quelques correspondances typiques entre « ce que je vois dans les logs de l’application » et « ce qui peut réellement être cassé dans le modèle Compose ».
Voici un petit tableau aide-mémoire. Ne le prenez pas comme un guide « tous azimuts », c’est plutôt un moyen de formuler rapidement une première hypothèse et de choisir une vérification peu coûteuse.
| Ce qui est visible dans les logs de app (Spring Boot) | Perception | Cause fréquente dans Compose | Que vérifier en premier |
|---|---|---|---|
| UnknownHostException: postgres ou Name or service not known | « L’application ne voit pas la BDD » | Faute de frappe dans le hostname, mauvais nom de service, mauvais réseau | docker compose config (URL), docker compose ps (le service existe‑t‑il ?) |
| Connection refused sur postgres:5432 | « La base refuse la connexion » | La base n’est pas encore prête, pas de service_healthy, mauvais port dans le réseau | docker compose ps (health), logs de postgres |
| Failed to configure a DataSource / pas d’URL de datasource | « Spring ne peut pas démarrer le contexte » | Variables d’environnement non transmises, substitution vide de ${…}, mauvais profil | docker compose config (environment), puis docker exec app printenv |
| Erreur de connexion à Redis/RabbitMQ avec les profils activés | « Le code cache/messaging est cassé » | Profils activés mais stack partielle sans dépendances | Ce qui est réellement lancé (docker compose ps) et quels profils sont actifs |
| AccessDeniedException, Permission denied, « impossible de créer le fichier » | « L’exportation est cassée » | Mount connecté au mauvais endroit ou pas de droits sur le répertoire cible | docker compose config (volumes + APP_EXPORT_DIR), docker exec app ls -la |
Remarquez la structure générale : presque partout, la première vérification n’est pas « ouvrir l’IDE et chercher un bug », mais « regarder les paramètres réels d’exécution ». Dans Docker/Compose, il est particulièrement dangereux de traiter les symptômes « par habitude » : vous voyez une stack trace, le cerveau passe automatiquement en mode « je suis développeur Java, donc je vais réparer Java ». Alors qu’il est plus juste de devenir pendant 2 minutes un « développeur d’environnement » et de confirmer que vous avez bien donné à l’application ce qu’elle attend.
Il convient de mentionner séparément le démarrage partiel (partial startup), car c’est l’une des sources les plus plausibles d’erreurs « fausses ». Vous avez peut-être lancé seulement app + postgres, mais laissé SPRING_PROFILES_ACTIVE=postgres,cache. L’application tentera honnêtement de se connecter à Redis et se plaindra. Dans les logs, cela ressemble à « le cache est cassé ». En réalité, le problème est une discordance : les profils actifs exigent une dépendance que vous n’avez pas lancée dans le scénario actuel.
De la même manière, vous pouvez activer accidentellement le profil messaging et obtenir des erreurs RabbitMQ, alors que vous déboguez quelque chose de totalement différent, par exemple l’exportation de fichiers. Et encore une fois : l’erreur est dans les logs de l’application, mais la cause première est dans le modèle de lancement.
5. Logs de plusieurs services
Dans un conteneur unique, tout est simple : un conteneur, un processus, un flux de logs. Dans une stack Compose, cela ne fonctionne pas ainsi car vous avez au moins deux processus qui communiquent entre eux, et souvent c’est dans ce dialogue que se cache la cause première. Il est donc très utile d’apprendre à lire les logs comme une chaîne de causes et d’effets, et non comme un ensemble de textes indépendants.
En pratique, cela se présente ainsi : vous voyez que app écrit « impossible de se connecter à PostgreSQL ». Si vous ne lisez que les logs de app, vous aurez une belle stack trace et l’impression que « tout va mal ». Mais si vous lisez simultanément les logs de postgres, vous verrez souvent une réponse bien plus précise : la base est encore en train de démarrer, la base n’a pas accepté le mot de passe, la base n’a pas créé l’utilisateur, la base a échoué lors de la migration (selon le scénario). L’application n’est à ce moment qu’un client qui reflète le symptôme.
Le moyen le plus simple de reconstituer la chaîne est d’utiliser la variante Compose des logs :
# On consulte les logs du client (app) et de la dépendance (postgres) pour voir la chaîne de cause à effet
docker compose logs app postgres
Si vous voulez voir en direct, ajoutez le mode « follow » :
# Le mode follow est pratique lorsque vous redémarrez des services et capturez l’instant de l’échec/de la disponibilité
docker compose logs -f app postgres
Et si les logs sont trop nombreux (ce qui arrive avec Spring Boot, même un lundi), une petite « queue » est utile :
# On prend seulement les dernières lignes pour voir rapidement la cause actuelle sans défiler
docker compose logs --tail=50 app postgres
Ce qui compte ici, ce n’est pas le nombre de flags, mais l’habitude : dès que vous avez une dépendance, vous devez toujours avoir au moins deux flux de logs en tête. Et lorsque vous construisez une hypothèse, vous ne demandez pas « pourquoi l’application a planté », mais « qui a dit le premier que quelque chose n’allait pas ».
Dans le diagnostic Compose, il est parfois utile de commencer non pas par les logs, mais par l’état des services :
docker compose ps
Vous obtenez un tableau où l’on voit quels conteneurs sont Up, lesquels Exited, lesquels ont un statut healthy/starting. Si postgres est encore starting alors que app essaye déjà de se connecter et échoue, c’est presque un manuel sur les problèmes de readiness. Et encore une fois : de l’extérieur, on dirait que « Spring est cassé » – en réalité, vous lancez simplement la stack sans que la dépendance soit réellement prête.
6. Erreur dans le YAML résolu
Il existe une catégorie particulière de problèmes, psychologiquement la plus difficile : vous regardez compose.yaml, tout est parfait, et même un collègue dit « oui, tout va bien ». Mais la stack continue de casser. Dans ces cas, c’est presque toujours dû au fait que la configuration réelle de lancement n’est pas compose.yaml, mais le résultat de la fusion de plusieurs fichiers et substitutions. Autrement dit, le problème vit dans le YAML résolu.
Le scénario le plus fréquent est un override inattendu dans compose.dev.yaml. Par exemple, vous vouliez en mode dev ajouter un volume pour plus de commodité ou ouvrir un port de débogage, mais vous avez accidentellement remplacé la mauvaise section et cassé le mode de base. Cela ressemble beaucoup à une situation en Java où vous vouliez « modifier un peu la configuration pour les tests », mais vous avez accidentellement remplacé le paramètre de production – sauf qu’ici cela se produit au niveau du YAML.
La deuxième histoire fréquente est un conflit de chemins. Pour les mounts dans Compose, c’est le chemin cible (target path) à l’intérieur du conteneur qui compte. Si APP_EXPORT_DIR pointe vers un endroit et que le volume est monté ailleurs, le comportement sera « magique » et irritant : l’exportation « fonctionne » (vous voyez un succès dans les logs), mais le fichier « n’est pas là » sur l’hôte. Et vous commencez à suspecter le CSV, le code, les droits d’accès, la phase de la lune… alors qu’il suffisait de comparer les deux chemins dans docker compose config.
La troisième histoire est la readiness et depends_on. Si vous voyez dans la configuration résolue que vous avez condition: service_started, cela signifie seulement « le conteneur postgres a démarré », mais pas « PostgreSQL est prêt à accepter des connexions ». L’application peut parfaitement échouer honnêtement au démarrage avec une erreur de connexion, et cela ressemblera à un échec de démarrage de l’application. La cause première est que le modèle de lancement n’a pas attendu la disponibilité de la dépendance. C’est un masque si fréquent qu’il est utile de le reconnaître visuellement.
Voici un exemple qui semble « presque correct », mais qui donne un comportement instable :
services:
app:
depends_on:
postgres:
# Le conteneur a démarré, mais le service à l’intérieur peut ne pas encore être prêt
condition: service_started
Et voici une variante qui exprime mieux la dépendance réelle :
services:
app:
depends_on:
postgres:
# On attend précisément la disponibilité (healthcheck), pas seulement le démarrage du conteneur
condition: service_healthy
La différence est petite, mais l’effet est énorme : vous arrêtez de jouer à « devine si la base a eu le temps de se réveiller ».
Enfin, la cause la plus « invisible » : la substitution de variables a donné une valeur vide. Par exemple, dans le YAML il est écrit :
services:
app:
environment:
SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE}
Si la variable n’est pas définie, la configuration résolue finale peut contenir une valeur vide, et l’application démarrera dans un mode complètement différent de celui attendu. Et vous obtiendrez l’erreur « tout marchait hier, pas aujourd’hui », alors qu’en réalité l’environnement dont Compose tire la valeur a simplement changé. docker compose config vous montrera cela « tel quel », sans fantaisie.
7. Mini‑schéma de diagnostic d’un masque Compose
Il est important de ne pas transformer cette leçon en un « playbook complet » prématuré (nous le construirons soigneusement dans la prochaine leçon du jour). Mais le schéma minimal spécifique au masque Compose vous est déjà nécessaire pour ne pas faire le réflexe typique « IDE‑first ».
Vous pouvez garder en tête ce court chemin : d’abord vous vérifiez le modèle de lancement, ensuite l’état des services, puis vous construisez la chaîne via les logs, et seulement après vous entrez dans le conteneur. Cela semble trop rationnel, alors fixons‑le sous la forme d’un simple organigramme.
flowchart TD
A["L’erreur est apparue dans l’application au sein de Compose"] --> B["Consulter le modèle résolu : docker compose config"]
B --> C["Consulter l’état : docker compose ps"]
C --> D["Lire la chaîne de logs : docker compose logs app + dépendances"]
D --> E["L’hypothèse est‑elle devenue concrète ?"]
E -->|"Oui"| F["Vérification ciblée : inspect/exec uniquement sur le sujet"]
E -->|"Non"| G["Préciser : quel a été le premier symptôme et où ?"]
F --> H["Une correction"]
H --> I["Répéter la vérification initiale"]
Ce qui est important dans ce schéma, c’est qu’il vous protège de deux erreurs coûteuses. La première erreur est de corriger l’application sans avoir confirmé que l’environnement correspond bien à ce qui est attendu. La deuxième erreur est de faire « cent vérifications à la suite » sans avoir formulé d’hypothèse. Le masque Compose aime particulièrement le chaos : plus vous modifiez le YAML de manière chaotique, moins vous comprenez pourquoi c’est devenu mieux ou pire.
Et encore un petit détail pratique : lorsque vous modifiez le YAML, assurez-vous d’avoir vraiment appliqué le changement. Parfois, il est nécessaire de recréer le service, sinon vous continuez à observer l’ancienne configuration. Ce n’est pas de la « magie Docker », c’est simplement la mécanique : le conteneur a déjà été créé avec certaines variables d’environnement et certains mounts, et tant que vous ne le recréez pas, il ne deviendra pas « tout seul » autre. Par conséquent, dans le diagnostic, il est toujours utile de se rappeler : vous ne modifiez pas du texte, vous modifiez un runtime réel.
8. Erreurs typiques de diagnostic des masques Compose
Erreur n°1 : Lire uniquement les logs de l’application et ignorer les logs des dépendances.
Cela semble naturel : vous êtes développeur Java, vous avez une stack trace dans les logs, donc vous devez la lire. Mais dans une stack Compose, la stack trace reflète souvent seulement le problème du voisin. Si postgres est encore en train de démarrer ou a reçu un mauvais mot de passe, l’application plantera en premier et le plus fort, mais la cause première vit dans un autre conteneur.
Erreur n°2 : Faire confiance à un fragment de YAML plutôt qu’à la configuration résolue.
Lorsque vous avez compose.yaml et compose.dev.yaml, ne regarder qu’un seul fichier revient à diagnostiquer un bug avec une seule méthode sans lire le code appelant. En conséquence, vous corrigez ce qui « n’était déjà pas utilisé », tandis que la valeur d’override réelle continue de casser le lancement. La commande docker compose config économise des heures ici.
Erreur n°3 : Lancer un démarrage partiel et oublier de le comparer aux profils Spring actifs.
Le démarrage partiel est un excellent outil, mais il exige de la discipline. Si vous n’avez lancé que app + postgres, alors que SPRING_PROFILES_ACTIVE inclut cache ou messaging, l’application attendra honnêtement Redis/RabbitMQ et se plaindra. Le symptôme ressemblera à « le code cache/messaging est cassé », alors que vous avez simplement demandé à l’application de vivre dans un mode pour lequel vous ne lui avez pas fourni l’infrastructure.
Erreur n°4 : Confondre service_started et la disponibilité réelle de la dépendance.
depends_on avec service_started crée souvent l’impression que « parfois ça marche, parfois non ». Le débutant perçoit cela comme un hasard ou comme un « Spring qui rame ». En réalité, c’est une question de timing : la base est lancée en tant que conteneur, mais pas encore prête à accepter des connexions. Si vous voulez de la stabilité, vous devez exprimer dans Compose l’état de disponibilité (ready), et pas seulement l’ordre de lancement.
Erreur n°5 : Monter un chemin et configurer l’application sur un autre.
Cela arrive particulièrement souvent avec le répertoire d’exportation : dans APP_EXPORT_DIR figure une valeur, et dans volumes une cible différente. L’application écrit les fichiers « à l’intérieur du conteneur », vous ne les voyez pas sur l’hôte, vous commencez à suspecter le code ou le format CSV. Alors que c’est un pur mismatch dans le modèle Compose, qui se vérifie plus rapidement que la lecture de n’importe quelle stack trace.
GO TO FULL VERSION