cursus.

Cours 2 · ConteneurisationLeçon 2 sur 2

Composition multi-services

6 h de lecture8 sections Version PDF

À la fin de cette leçon, vous saurez

Docker Compose, application à plusieurs conteneurs, dépendances et sondes de santé, environnements de développement reproductibles, débogage d'un conteneur.

L'application démarre, se connecte à la base, et échoue : ECONNREFUSED. On relance, et cette fois tout marche. Le diagnostic est immédiat — la base n'était pas encore prête — et le correctif qui vient à l'esprit est presque toujours le même :

command: sh -c "sleep 10 && node serveur.js"

Il fonctionne sur le portable de son auteur. Il échoue en intégration continue, où la machine est plus lente. On passe à sleep 30, ce qui ralentit chaque démarrage de trente secondes et échoue quand même le jour où la base met trente-cinq secondes à ouvrir ses fichiers.

Ce chapitre fait tenir plusieurs conteneurs ensemble, et le fait proprement — ce qui commence par renoncer au sleep.

Un fichier, une commande

docker compose décrit une application à plusieurs services dans un fichier versionné, puis la démarre entière.

services:  appli:    build: .    ports:      - "8080:3000"    environment:      DATABASE_URL: postgres://app:secret@basededonnees:5432/app      REDIS_URL: redis://cache:6379    depends_on:      basededonnees:        condition: service_healthy   basededonnees:    image: postgres:16-alpine    environment:      POSTGRES_PASSWORD: secret    volumes:      - donnees:/var/lib/postgresql/data    healthcheck:      test: ["CMD-SHELL", "pg_isready -U app"]      interval: 5s      retries: 10   cache:    image: redis:7-alpine volumes:  donnees:

Quatre points se lisent directement dans ce fichier.

Les noms de services sont des noms d'hôtes. L'application atteint la base à basededonnees:5432 : Compose crée un réseau et y inscrit chaque service sous son nom. C'est la découverte de service du chapitre 3, et elle explique pourquoi la chaîne de connexion ne contient aucune adresse IP.

Seul ce qui doit sortir est publié. appli publie un port, la base et le cache n'en publient aucun : ils sont joignables entre conteneurs et invisibles de l'extérieur. Publier le port de la base « pour pouvoir s'y connecter » est le réflexe à combattre — c'est une base exposée sur la machine.

Le volume nommé porte les données. Il survit à docker compose down, contrairement aux conteneurs, et c'est bien le but.

build contre image. On construit ce qu'on écrit, on tire ce qu'on consomme. En production, l'application aussi sera une image tirée d'un registre — construite par le pipeline du chapitre 5.

Le vrai problème : l'ordre de démarrage

C'est le cœur du chapitre, et il faut être précis sur ce que depends_on fait.

depends_on seul n'attend pas que le service soit prêt. Il attend qu'il soit démarré, c'est-à-dire que son processus existe. Or un PostgreSQL démarré met encore quelques secondes à accepter les connexions. La dépendance est respectée, et l'application échoue quand même.

D'où la sonde de santé (healthcheck) : une commande que Docker exécute périodiquement dans le conteneur, et dont le code de retour dit si le service est utilisable. Combinée à condition: service_healthy, elle donne enfin l'attente correcte.

healthcheck:  test: ["CMD-SHELL", "pg_isready -U app"]  interval: 5s        # tous les 5 secondes  timeout: 3s         # au-delà, c'est un échec  retries: 10         # 10 échecs d'affilée → conteneur « unhealthy »  start_period: 20s   # délai de grâce initial, sans compter les échecs

Trois qualités d'une bonne sonde, qu'on retrouvera au chapitre 9 avec Kubernetes. Elle doit tester ce dont l'appelant a besoinpg_isready teste l'acceptation de connexions, pas seulement l'existence du processus. Elle doit être rapide et sans effet de bord : une sonde qui écrit en base ou interroge un service tiers transforme un incident distant en panne locale. Et start_period doit couvrir le démarrage normal, faute de quoi un service lent est déclaré malade avant d'avoir eu sa chance.

Reste une nuance essentielle, et c'est celle que les étudiants manquent : la sonde règle le démarrage, pas la vie du système. En production, une base redémarre, un réseau se coupe, un service devient indisponible dix secondes. Une application qui ne survit pas à cela est fragile quel que soit l'ordre de démarrage.

La réponse robuste est donc dans l'application : tenter la connexion, échouer, réessayer avec un délai croissant. Compose garantit un démarrage propre ; la reprise sur erreur, elle, est une propriété du code. Les deux se cumulent, et la seconde est la seule qui tienne en production.

Quiz · vérifiez votre compréhension Sans réponse

Une application dépend d'une base via depends_on, et échoue pourtant au démarrage avec « connexion refusée ». Pourquoi ?

Un environnement de développement reproductible

C'est le bénéfice quotidien du chapitre, et il se mesure à une phrase : git clone puis docker compose up, et l'on a un environnement complet qui tourne.

Ce que cela remplace : une page de documentation d'installation, périmée, qui demande la bonne version de PostgreSQL, la bonne version de Redis, et trois variables d'environnement dont l'une n'est mentionnée nulle part. Un nouvel arrivant devient productif en dix minutes au lieu de deux jours, et surtout tout le monde travaille sur les mêmes versions — ce qui supprime une classe entière de bogues « chez moi ça marche ».

Deux mécanismes servent à adapter sans dupliquer. Les fichiers de surcharge : compose.override.yml est lu automatiquement en plus du fichier principal, ce qui permet de monter le code en développement — pour éditer sans reconstruire — sans polluer la définition commune. Et les profils, qui permettent de ne démarrer qu'une partie des services : l'outillage d'observation du chapitre 10 n'a pas à tourner pour écrire une fonctionnalité.

Déboguer un conteneur

Quatre commandes suffisent à traiter la quasi-totalité des situations, et l'ordre compte.

docker compose logs -f service en premier, toujours. Un conteneur qui s'arrête immédiatement a presque toujours écrit pourquoi.

docker compose ps montre l'état et le code de sortie. Un service qui redémarre en boucle y est visible, et le code de sortie oriente : 0 arrêt normal — donc le processus principal s'est terminé, ce qui est une erreur de conception —, 1 erreur applicative, 137 tué par SIGKILL, typiquement une limite de mémoire dépassée.

docker compose exec service sh ouvre un interpréteur dans le conteneur en marche. C'est là qu'on vérifie ce que l'application voit vraiment : variables d'environnement présentes, fichier de configuration au bon endroit, base joignable par son nom.

docker inspect donne la configuration effective — réseaux, volumes, variables — quand on soupçonne un écart entre ce qui est écrit et ce qui tourne.

Un piège récurrent mérite d'être nommé : un conteneur s'arrête quand son processus principal s'arrête. Une image dont la commande est un script qui lance un service en arrière-plan puis se termine produit un conteneur qui « ne veut pas rester allumé ». Il n'y a rien à réparer côté Docker : le processus doit tourner au premier plan.

Quiz · vérifiez votre compréhension Sans réponse

Dans un fichier Compose, la base de données ne publie aucun port. L'application s'y connecte pourtant. Comment ?

À vous

L'exercice simule le démarrage d'une pile à plusieurs services et rend visible ce qui, en vrai, se manifeste par un message d'erreur intermittent.

D'abord la course : trois services, des temps de démarrage variables, et une application qui se connecte dès qu'elle est lancée. Vous ferez varier la vitesse de la machine et compterez les échecs — c'est ce qui explique qu'un sleep 10 marche sur un portable et échoue en intégration.

Ensuite les trois correctifs, comparés sur les mêmes scénarios : le sleep fixe, la sonde de santé, et la reprise avec délai croissant côté application. Vous constaterez que seule la dernière survit au cas « la base redémarre en cours de vie », que les deux autres ne traitent pas du tout.

La dernière partie résout l'ordre de démarrage à partir du graphe de dépendances, et détecte le cas où quelqu'un a introduit un cycle — ce que Compose refuse, avec un message qu'il vaut mieux savoir lire.

Exercice · JavaScript · à vous de jouer

Reproduisez la course au démarrage, comparez les trois correctifs, puis ordonnez le graphe de dépendances.

En attente
// ── Une pile à trois services ─────────────────────────────────────────────
// pret : instant (en secondes) où le service accepte des connexions.
const PILE = {
  cache:         { pret: 0.4, depend: [] },
  basededonnees: { pret: 6.0, depend: [] },
  appli:         { pret: 0.3, depend: ["basededonnees", "cache"] },
};

// Un démarrage : l'appli se connecte dès qu'elle est lancée.
function demarrer(pile, strategie, lenteur, evenements = []) {
  const journal = [];
  const pret = {};
  for (const [nom, s] of Object.entries(pile)) pret[nom] = s.pret * lenteur;

  let instantAppli;
  if (strategie.type === "naif") instantAppli = pret.appli;
  if (strategie.type === "sleep") instantAppli = strategie.secondes;
  if (strategie.type === "healthy") instantAppli = 0;   // ← à écrire
  if (strategie.type === "reprise") instantAppli = pret.appli;

  // L'application tente de se connecter ; selon la stratégie, elle réessaie.
  let t = instantAppli, essais = 0, connecte = false, attente = 0.5;
  while (essais < 8) {
    essais++;
    const baseDisponible = t >= pret.basededonnees && !evenements.some(
      (e) => e.quoi === "panne base" && t >= e.de && t < e.a);
    journal.push("t=" + t.toFixed(1) + "s  essai " + essais +
                 (baseDisponible ? "  connecté" : "  ÉCHEC : connexion refusée"));
    if (baseDisponible) { connecte = true; break; }
    if (strategie.type !== "reprise") break;      // les autres abandonnent
    t += attente; attente *= 2;                    // délai croissant
  }
  return { connecte, t, essais, journal };
}

// ── À VOUS ────────────────────────────────────────────────────────────────
// 1. Écrivez la stratégie « healthy » : l'appli démarre quand la base est
//    RÉELLEMENT prête, quelle que soit la lenteur de la machine.
// 2. Comparez les quatre stratégies sur une machine rapide (lenteur 1) puis
//    lente (lenteur 3). Laquelle tient dans les deux cas ?
// 3. Ajoutez l'événement « la base redémarre à t = 40 s pendant 8 s ».
//    Laquelle des quatre survit ?
// 4. Écrivez l'ordre de démarrage à partir du graphe, et détectez un cycle.

const r = demarrer(PILE, { type: "naif" }, 1);
console.log(r.journal.join("\n"));

Console de sortie
Le résultat s'affiche dans la console

En travaux pratiques

Travaux pratiques 4 · sur machine

La pile complète, et la course au démarrage

Faire tenir ensemble l'application, sa base et son cache ; provoquer la course au démarrage, puis la corriger proprement — et rendre l'application robuste à une base qui redémarre.

3 h
Avant de commencer
  • L'image du TP 3, qui se construit et se lance
  • Docker Compose disponible (docker compose version)
  1. 1. Écrire la pile à trois services

    Décrivez l'application, une base PostgreSQL et un cache Redis. L'application se connecte aux deux. Ne publiez QUE le port de l'application.

  2. 2. Provoquer la course

    Lancez la pile à froid, plusieurs fois de suite. L'application échoue au démarrage une fois sur deux ou trois. Lisez les journaux et identifiez le message exact.

  3. 3. Essayer le mauvais correctif

    Ajoutez une attente fixe de dix secondes avant le démarrage de l'application. Relancez : cela fonctionne. Puis limitez les ressources de la base pour ralentir son démarrage, et relancez. Concluez.

  4. 4. Écrire une sonde de santé

    Remplacez l'attente par une sonde sur la base, et faites dépendre l'application de son état de santé et non de son simple démarrage. Vérifiez avec les ressources toujours limitées.

  5. 5. Rendre l'application robuste

    La pile démarre proprement. Redémarrez maintenant la base SEULE, pendant que l'application tourne. Que se passe-t-il ? Ajoutez une reprise avec délai croissant dans le code de connexion, et refaites l'essai.

  6. 6. Déboguer un conteneur

    L'enseignant fournit une variante dont un service s'arrête immédiatement. Diagnostiquez-la sans modifier le code, en suivant l'ordre du cours.

C'est réussi quand
  • docker compose up réussit dix fois d'affilée, y compris avec une base ralentie
  • Un redémarrage de la base seule ne fait plus tomber l'application
  • Le port de la base n'est pas publié, et l'application l'atteint quand même

Ce que la suite en fait

Le bloc III automatise ce que ce chapitre a rendu reproductible : le pipeline du chapitre 5 construira l'image du chapitre 3 et lancera les tests dans une pile Compose, ce qui donne des tests d'intégration réalistes sans machine dédiée.

Et les sondes de santé reviendront au chapitre 9, où elles cessent d'être un confort de démarrage pour devenir le mécanisme qui décide si un exemplaire reçoit du trafic. C'est la même idée, avec des conséquences bien plus grandes.

À retenir

Flashcards · 1 / 5Toucher pour retourner
Fin de la leçon

Vous avez parcouru les 8 sections.

Marquez-la terminée pour faire avancer votre parcours, ou revenez sur un point avant de passer à la suite.