CodeGym /Cours /Docker for Spring /Docker Compose

Docker Compose

Docker for Spring
Niveau 15, Leçon 0
Disponible

1. docker run : pratique pour un seul conteneur

Après les premiers pas avec Docker, il est facile de tomber amoureux de docker run : une commande et le service est en vie. Cela ressemble à « je vais vite jeter quelque chose dans le terminal » (et six mois plus tard, ce « jeter » devient la prod, mais n'en parlons pas). Avec un seul conteneur, docker run reste acceptable, surtout si vous lancez tout vous-même, sur votre machine, dans un environnement familier.

Lorsque nous avons un seul service avec presque aucune configuration, docker run est un outil parfaitement fonctionnel. Il est honnête : vous voyez clairement quelle image vous lancez, quels ports vous exposez et quelles variables d'environnement vous transmettez à l'intérieur. Pour un premier résultat conteneurisé, c'est même utile d'un point de vue méthodique : vous ressentez avec vos mains qu'un conteneur est un processus, que le port à l'intérieur du conteneur et le port sur l'hôte sont des entités différentes, et que les « paramètres de lancement » ne sont pas un réglage mystique, mais des options spécifiques.

Mais dès que ce « seul conteneur » commence à se heurter à la réalité (et la réalité ne demande généralement pas la permission), la commande cesse d'être une commande et se transforme en un petit roman. Et le pire de tout, un roman sans sommaire : tout tient sur une seule ligne, et le sens est plus difficile à lire qu'une stack trace sur 3 écrans.

Pour être tout à fait honnête : le problème n'est pas que docker run est « mauvais ». Le problème est que docker run est une méthode de lancement, et non une méthode pour stocker la description de l'environnement. Il répond à la question « comment lancer maintenant », mais répond mal à la question « comment garantir de lancer à l'identique demain » — et encore moins « comment une autre personne lancera-t-elle sur une autre machine ».

2. Comment la commande docker run se développe

À un moment donné, vous remarquez que le lancement du conteneur n'est plus « juste lancer ». Un port apparaît, un profil d'application apparaît, des variables d'environnement apparaissent, des répertoires à monter apparaissent, et soudain apparaît la nécessité de répéter le même lancement encore et encore. Et c'est à ce moment-là que la commande commence à gonfler.

Regardons un lancement tout à fait normal et réaliste de notre service d'apprentissage. Rien d'« avancé » — juste ce que vous savez déjà faire :

# Lancement du conteneur avec des paramètres d'environnement explicites.
# Important : tout cela repose sur le fait que vous n'oublierez aucun flag.
docker run --name catalog-app -p 8080:8080 \
  -e SPRING_PROFILES_ACTIVE=standalone \
  -e APP_EXPORT_DIR=/data/exports \
  -v ./data/exports:/data/exports \
  docker-java-catalog-service

Si on lit cela avec les yeux d'une personne qui sait ce qui se passe, tout est logique. Nous définissons le nom du conteneur (pour pouvoir y faire référence facilement), publions le port, activons le profil standalone, disons à l'application où écrire l'export, et montons un répertoire local pour que les fichiers d'export soient visibles sur la machine hôte.

Le problème est que cette commande n'est déjà plus une « simple commande ». C'est un ensemble de conventions qui reposent sur votre attention. Il suffit d'oublier un paramètre pour que l'application change de comportement, ou bien « fonctionne, mais pas comme prévu ». Et le plus désagréable : elle peut ne pas planter, mais se comporter silencieusement différemment. Dans le monde du backend, c'est l'une des classes d'erreurs les plus coûteuses : tout est vert, mais les données « ne sont pas là pour une raison quelconque ».

Imaginez maintenant que vous décidez de changer « rapidement » de port (par exemple, 8080 est occupé). Cela semble anodin :

# Oui, le port a été changé.
# Mais notez : il est facile de « perdre accidentellement » des paramètres importants (env/volumes),
# et le conteneur peut tout à fait démarrer quand même.
docker run --name catalog-app -p 8090:8080 \
  -e SPRING_PROFILES_ACTIVE=standalone \
  docker-java-catalog-service

La commande est devenue plus courte… et c'est là que la magie inversée commence. Vous pouvez facilement oublier APP_EXPORT_DIR et le mount, parce que « j'ai juste changé le port ». Le conteneur démarrera, l'API s'ouvrira, /actuator/health dira que tout va bien. Ensuite, vous lancerez l'export et chercherez le fichier sur l'hôte — et vous ne le trouverez pas. Parce qu'il se trouvera à l'intérieur du conteneur, dans la couche inscriptible, et ne vivra que jusqu'à la première recréation du conteneur.

Chez les débutants, cela provoque le sentiment que « Docker est imprévisible ». En réalité, Docker est très prévisible. Ce qui est imprévisible, c'est nous, lorsque nous gardons l'environnement dans notre tête et dans l'historique du terminal.

On peut imaginer cela comme le lancement d'une application « de mémoire ». Tant que le système est petit, c'est acceptable. Dès que les paramètres deviennent nombreux, la mémoire se transforme en source de bugs aléatoires. À ce moment-là, nous commençons à vouloir non pas un nouveau flag, mais une nouvelle approche : stocker la description du lancement comme un document, et non comme « récite une incantation dans le terminal et ne te trompe pas ».

3. Dérive du lancement : un flag oublié

Il y a un type particulier de douleur que tous les développeurs aiment : « ça marche chez moi ». Dans le contexte de Docker, cela sonne souvent comme : « je lance le conteneur, tout va bien », alors qu'un collègue lance « presque pareil » et chez lui, « pour une raison quelconque », ça ne va pas. Et vous avez tous les deux honnêtement raison — parce que vous ne lancez pas la même chose.

La dérive commence très tôt. Vous écrivez la commande dans le README. Ensuite, vous ajoutez un export au projet. Ensuite, vous réalisez que le chemin d'export doit être un paramètre. Ensuite, vous ajoutez un mount. Ensuite, vous remarquez que le port peut entrer en conflit. Ensuite, vous avez besoin d'un autre profil. Et voilà que le README ne contient plus une seule commande, mais quatre variantes, chacune « presque comme la précédente, mais légèrement différente ».

Le moment le plus perfide : docker run ne vous aide pas à comprendre à quel point une commande est « similaire » à une autre. Ce ne sont que des chaînes de caractères. L'œil humain compare 6 à 8 flags — et en oublie facilement un. Résultat : vous vous retrouvez dans une situation où le conteneur s'appelle de la même manière (catalog-app), l'image est la même (docker-java-catalog-service), mais l'environnement réel est différent.

Pour prendre conscience de la dangerosité de cela, il suffit de se rappeler que nous savons déjà configurer l'application via une configuration externe : profils, variables d'environnement, paramètres. C'est correct et nécessaire. Mais précisément à cause de cette approche correcte, il devient encore plus important de ne pas perdre une partie des paramètres lors du lancement.

Dans le développement réel, cela se présente de manière très « humaine ». Par exemple, vous avez mis à jour la branche du projet, reconstruit l'image, lancez le conteneur… et vous avez oublié de passer SPRING_PROFILES_ACTIVE. L'application démarre en mode par défaut, peut utiliser un mauvais référentiel (en mémoire au lieu de celui attendu), de mauvais paramètres, un autre port. Et vous perdez du temps non pas à développer, mais à comprendre « pourquoi tout à coup c'est devenu différent, alors que je n'ai rien changé ».

C'est là qu'apparaît l'idée principale d'aujourd'hui : la configuration de l'environnement ne doit pas être « dans la tête » ni « dans l'historique des commandes ». Il faut en faire un artéfact du projet — comme Dockerfile, comme build.gradle.kts, comme application.yml. C'est-à-dire un fichier que l'on peut lire, vérifier, commenter et modifier en connaissance de cause.

4. Le terminal et le README comme dépôt de l'environnement

À ce stade, un contre-argument surgit généralement : « Bon ok, je vais juste écrire la bonne commande dans le README, et voilà. » Cela semble raisonnable… jusqu'à ce que vous essayiez de maintenir cela ne serait-ce qu'une semaine.

Le README est une documentation. Il est bon pour expliquer : de quoi parle le projet, comment le construire, comment le lancer. Mais le README fonctionne mal en tant que source de vérité pour l'environnement. La raison est simple : le README est un texte que personne n'est obligé d'exécuter littéralement. Vous le lisez, vous en copiez manuellement un passage, vous le modifiez un peu, vous lancez… et ensuite chaque personne de l'équipe a « sa propre version légèrement corrigée ». Et la divergence commence.

L'historique du terminal est encore pire. Il n'est pas du tout conçu pour être une configuration. Vous pouvez y trouver par hasard la commande « correcte », mais il est très difficile de la distinguer de celle « presque correcte », et il est presque impossible de comprendre le contexte : pourquoi ces valeurs, pourquoi ce port, pourquoi ce paramètre est apparu et ce qu'il signifie.

Il y a encore une chose importante : une commande dans le terminal est un acte unique. Un fichier est un objet qui vit dans le dépôt et évolue avec le projet. Un fichier a un diff. Un fichier a un historique des modifications. Un fichier est passé en revue. Si la commande change — vous le remarquerez, parce que le fichier a changé. Si la commande « change dans votre tête » — vous ne le remarquerez pas, jusqu'à ce que vous obteniez une erreur.

Cela est particulièrement visible lorsque vous n'êtes pas seul. Et un projet backend n'est presque jamais « seul » : aujourd'hui c'est vous, demain un collègue, après-demain une nouvelle personne qui fait son onboarding. Et c'est là que se produit la chose la plus désagréable : au lieu de faire confiance au projet, les gens commencent à faire confiance à « celui qui a déjà lancé ». Une dépendance au porteur de connaissance apparaît : « demande à Pierre comment démarrer ». Pierre est bien sûr un héros. Mais Pierre est parfois en vacances. Et en plus, Pierre peut ne pas avoir le bon port.

Docker résout en général le problème de la reproductibilité. Mais si le lancement de l'environnement vit dans des commandes qui sont tapées manuellement à chaque fois, nous nous ramenons nous-mêmes le même problème — seulement maintenant il a une apparence « conteneurisée ».

5. Le fichier comme description unique de l'environnement

Si l'on met de côté les belles paroles, nous avons besoin de quelque chose de très simple : un artéfact unique où est décrit « comment lancer » — de la même manière que Dockerfile décrit « comment construire ». Et c'est exactement ce que fournit Docker Compose : une description de l'environnement dans un fichier.

Important : à cette étape, nous n'avons pas besoin d'une liste complète des clés YAML. Ce qui est important, c'est de voir le changement de rôle lui-même — le lancement cesse d'être une chaîne de caractères et devient une structure.

Regardez comment la même idée se présente dans un fichier :

services:
  app:
    image: docker-java-catalog-service
    ports:
      - "8080:8080"
    environment:
      SPRING_PROFILES_ACTIVE: standalone

Même sur un fragment aussi court, on voit l'essentiel : le lancement a cessé d'être une chaîne de flags et est devenu une arborescence de paramètres. Les ports sont à côté des ports, les variables d'environnement sont à côté des variables d'environnement, et si le service a besoin de mounts ou de paramètres supplémentaires, ils s'ajoutent à la même structure, sans se perdre dans l'historique du terminal.

Cela apporte une caractéristique d'ingénierie très utile : le fichier montre ce que vous avez oublié. Dans une ligne de commande, il est facile de perdre un flag. Dans une structure YAML, il est plus facile de remarquer que le service n'a pas du tout de volumes, alors que l'export devrait sortir à l'extérieur, ou que le profil a été oublié alors que l'application doit démarrer dans un mode spécifique.

Une autre chose qui devient soudainement possible : vous pouvez faire des modifications comme vous faites des modifications dans le code. Par exemple, « extrayons le port dans une variable », « ajoutons un paramètre supplémentaire », « renommons le service ». Vous faites cela par diff. Ce n'est pas « j'ai maintenant une autre commande », c'est « le projet a maintenant une autre configuration d'environnement ». Et c'est un niveau de discipline qualitativement différent.

Si vous aimez les analogies : docker run équivaut à lancer une application via la fenêtre « Exécuter » et à taper les arguments à la main à chaque fois. Compose, c'est comme garder la configuration de lancement à côté du projet, et lancer avec une seule commande courte. Sauf que sans bouton — nous restons des gens du CLI, et c'est parfait.

6. Compose : fichier + commandes courtes

L'idée principale ici est simple : Compose n'est pas nécessaire parce que nous avons déjà une base de données et la moitié du datacenter. Il devient rentable plus tôt — au moment où vous voulez reproductiblement déployer le même environnement.

Compose fournit un modèle dans lequel la « description de l'environnement » se trouve dans un fichier, et la commande devient courte :

# Déployer l'environnement déjà décrit dans le fichier compose.
docker compose up --build

La commande ne porte plus tout le poids des paramètres. Elle dit simplement : « déploie l'environnement décrit dans le fichier ». C'est un changement de rôle très sain : au lieu de garder chaque détail dans une seule ligne, nous transférons les détails là où ils doivent être — dans un document de configuration.

Pour bien visualiser cela, voici un petit schéma montrant la différence entre l'approche « commande » et l'approche « fichier » :

flowchart TD
  A["Commande longue docker run
dans le terminal/README"] --> B["Copier-coller + modifications manuelles"] B --> C["Lancement sur différentes machines"] C --> D["Dérive de l'environnement
et 'ça marche chez moi'"] E["compose.yaml comme artéfact du projet"] --> F["Commande courte docker compose up"] F --> G["Lancement identique pour tous"] G --> H["Environnement prévisible"]

Compose n'« active pas de super-pouvoirs ». Il transfère simplement la configuration de lancement d'une chaîne de caractères vers un fichier. C'est exactement ce que nous faisions auparavant à d'autres niveaux. Nous n'écrivons pas les commandes Gradle de mémoire — nous avons build.gradle.kts. Nous ne lançons pas l'application « parce que Pierre l'a dit » — nous avons application.yml. Maintenant, la même chose se produit avec l'environnement conteneurisé : au lieu d'un « ensemble de commandes » apparaît une « description de l'environnement ».

Tant qu'il n'y a qu'un seul service, cela suffit déjà. Et quand un conteneur voisin apparaîtra à côté, vous n'aurez pas à inventer une nouvelle façon de démarrer — vous étendrez simplement la même description de l'environnement.

7. Erreurs typiques lors du travail avec Docker Compose

Erreur n°1 : penser que Compose n'est nécessaire que « quand la base de données apparaît ».
C'est une logique très courante, et elle semble raisonnable : « un conteneur à la main, deux conteneurs — Compose ». En pratique, Compose devient rentable plus tôt : au moment où le même lancement doit être répété, expliqué, maintenu et ne pas être cassé par une modification accidentelle de la commande. Si vous lancez le service tous les jours, vous n'avez pas besoin d'un deuxième conteneur, vous avez besoin de reproductibilité.

Erreur n°2 : considérer une longue commande dans le README comme un remplacement valable de la configuration de l'environnement.
Le README est important, mais il n'est pas une « source de vérité ». Les gens copient la commande, la modifient « un tout petit peu », oublient un flag, et une nouvelle « version locale de la réalité » apparaît. Ensuite, vous examinez un bug et vous ne savez pas si c'est un problème de code ou simplement des paramètres de lancement différents. Le fichier d'environnement résout exactement cela : il fige les paramètres comme un artéfact du projet, et non comme une tradition orale.

Erreur n°3 : stocker plusieurs commandes docker run similaires et maintenir leur synchronisation manuellement.
D'abord vous avez le « lancement normal », puis le « lancement sur un autre port », puis le « lancement avec export », puis le « lancement sans exports » — et chaque commande commence à vivre sa propre vie. Au final, vous perdez du temps à maintenir un ensemble de chaînes de caractères au lieu de maintenir une seule description de l'environnement. L'approche Compose est justement nécessaire pour reprendre le contrôle et rendre le lancement unique et prévisible.

Erreur n°4 : percevoir le passage à Compose comme un passage à la « magie », et non comme à une discipline.
Si vous considérez Compose comme « juste une autre façon de lancer quelque chose », il n'apportera pas le principal bénéfice. Son sens est que l'environnement devient lisible, versionné et vérifiable. Ce n'est pas de la magie — c'est simplement le transfert de la configuration au bon endroit. C'est pourquoi nous commençons par la motivation et la douleur, et non par une liste de clés YAML.

Erreur n°5 : ne pas comprendre que la commande est devenue courte, mais que la responsabilité n'a disparu nulle part.
Quand vous passez à Compose, il est facile de se détendre : « bon, maintenant tout est dans le fichier ». Mais si vous modifiez manuellement compose.yaml, faites des changements locaux « temporaires » et ne les validez pas comme partie du projet, vous obtenez la même dérive — seulement maintenant elle ne vit pas dans le terminal, mais dans un fichier non commité.

1
Mission
Docker for Spring, niveau 15, leçon 0
Bloqué
Lancement reproductible d'un conteneur via `run-standalone.sh`
Lancement reproductible d'un conteneur via `run-standalone.sh`
1
Mission
Docker for Spring, niveau 15, leçon 0
Bloqué
docker run paramétrisé par host-port
docker run paramétrisé par host-port
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION