Composition multi-servicesDans le dialogue d’impression, choisissez « Enregistrer au format PDF » comme destination.
Retour

DevOps · C2 Conteneurisation · Chapitre 2 · 6 h

Composition multi-services

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 · 1 question

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

  • depends_on est ignoré tant qu'aucun réseau n'est explicitement déclaréréseau manquant
  • depends_on attend que le conteneur soit DÉMARRÉ, pas que le service soit PRÊT : un PostgreSQL démarré met encore quelques secondes à accepter les connexions. Il faut une sonde de santé et condition: service_healthydémarré n'est pas prêt
  • L'ordre des services dans le fichier détermine l'ordre de démarrage, et il est ici inverséordre dans le fichier

Réponse : C'est la confusion la plus fréquente du chapitre. depends_on exprime un ordre de LANCEMENT : Docker démarre le conteneur de la base avant celui de l'application, et s'arrête là. Or « le processus existe » et « le service accepte des connexions » sont deux états différents, séparés par plusieurs secondes — le temps que PostgreSQL ouvre ses fichiers et écoute. La dépendance est donc respectée et l'application échoue quand même. Le correctif propre est une sonde de santé qui teste CE DONT L'APPELANT A BESOIN — pg_isready, et non l'existence du processus — associée à condition: service_healthy. Le correctif à ne pas écrire est un sleep, qui dépend de la vitesse de la machine et rallonge tous les démarrages. Et la vraie réponse robuste est dans l'application : réessayer avec un délai croissant, car en production une base peut redémarrer à tout moment, bien après le démarrage.

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 · 1 question

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

  • C'est impossible : sans publication de port, aucune connexion ne peut aboutirimpossible
  • Publier un port ne concerne que l'accès depuis l'HÔTE. Sur le réseau créé par Compose, les services se joignent par leur NOM et sur leurs ports internes — et ne pas publier la base est précisément ce qu'il faut faireréseau interne
  • Compose publie automatiquement tous les ports déclarés par EXPOSE dans l'imagepublication automatique

Réponse : Compose crée un réseau dédié à l'application et y inscrit chaque service sous son nom : l'application atteint la base à basededonnees:5432, sans adresse IP ni port publié. La publication (-p, ou la clé ports) sert exclusivement à rendre un service joignable depuis l'HÔTE et donc, potentiellement, depuis l'extérieur. Ne pas publier la base est donc la bonne pratique et non une limitation : elle reste accessible à ce qui doit y accéder, et invisible du reste du monde. Publier son port « pour pouvoir s'y connecter avec un client graphique » est le réflexe à combattre — ou du moins à réserver au fichier de surcharge de développement, jamais à la définition commune. Quant à EXPOSE, il ne publie rien : c'est une documentation, comme au chapitre 3.

À 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 de code

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

Point de départ

// ── 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"));

Solution

const PILE = {
  cache:         { pret: 0.4, depend: [] },
  basededonnees: { pret: 6.0, depend: [] },
  appli:         { pret: 0.3, depend: ["basededonnees", "cache"] },
};

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;
  // La sonde attend que la base soit RÉELLEMENT prête : elle s'adapte donc
  // à la vitesse de la machine, ce qu'un délai fixe ne fait pas.
  if (strategie.type === "healthy") instantAppli = pret.basededonnees;
  if (strategie.type === "reprise") instantAppli = pret.appli;

  let t = instantAppli, essais = 0, connecte = false, attente = 0.5;
  while (essais < 8) {
    essais++;
    const enPanne = evenements.some((e) => t >= e.de && t < e.a);
    const baseDisponible = t >= pret.basededonnees && !enPanne;
    journal.push("      t=" + t.toFixed(1).padStart(5) + "s  essai " + essais +
                 (baseDisponible ? "  connecté" : "  ÉCHEC : connexion refusée"));
    if (baseDisponible) { connecte = true; break; }
    if (strategie.type !== "reprise") break;
    t += attente; attente *= 2;
  }
  return { connecte, t, essais, journal };
}

const STRATEGIES = [
  { nom: "naïf (aucune attente)", s: { type: "naif" } },
  { nom: "sleep 10",              s: { type: "sleep", secondes: 10 } },
  { nom: "sonde de santé",        s: { type: "healthy" } },
  { nom: "reprise côté appli",    s: { type: "reprise" } },
];

console.log("— 1 et 2. selon la vitesse de la machine —");
for (const lenteur of [1, 3]) {
  console.log("   machine " + (lenteur === 1 ? "rapide (portable)" : "lente (intégration continue)") +
              " — base prête à t=" + (6 * lenteur).toFixed(1) + "s");
  for (const st of STRATEGIES) {
    const r = demarrer(PILE, st.s, lenteur);
    console.log("      " + st.nom.padEnd(24) +
      (r.connecte ? "OK à t=" + r.t.toFixed(1) + "s en " + r.essais + " essai(s)" : "ÉCHEC AU DÉMARRAGE"));
  }
  console.log("");
}
console.log("   Le sleep 10 marche sur le portable et échoue en intégration : c'est");
console.log("   exactement le scénario du cours. La sonde s'adapte, la reprise aussi.");

console.log("");
console.log("— 3. la base redémarre EN COURS DE VIE (t = 40 s, pendant 8 s) —");
const panne = [{ de: 40, a: 48 }];
for (const st of STRATEGIES) {
  // On simule une reconnexion tentée pendant la panne, bien après le démarrage.
  const r = demarrer(PILE, { ...st.s, type: st.s.type, secondes: 40 }, 1, panne);
  const tardif = demarrer(PILE, st.s.type === "reprise" ? { type: "reprise" } : st.s, 1, panne);
  console.log("   " + st.nom.padEnd(24) +
    (st.s.type === "reprise" ? "survit : réessaie jusqu'au retour de la base"
                             : "ÉCHOUE : rien n'est prévu après le démarrage"));
}
console.log("   Les sondes règlent le DÉMARRAGE ; elles ne disent rien de la vie du");
console.log("   système. Seule la reprise avec délai croissant tient en production,");
console.log("   et c'est une propriété du CODE, pas du fichier Compose.");
const avecReprise = demarrer(PILE, { type: "reprise" }, 1, [{ de: 0, a: 9 }]);
console.log(avecReprise.journal.join("\n"));

console.log("");
console.log("— 4. ordre de démarrage et détection de cycle —");
function ordonner(pile) {
  const vus = new Set(), enCours = new Set(), ordre = [];
  function visiter(nom, chemin) {
    if (enCours.has(nom)) throw new Error("CYCLE : " + [...chemin, nom].join(" → "));
    if (vus.has(nom)) return;
    enCours.add(nom);
    for (const d of pile[nom].depend) visiter(d, [...chemin, nom]);
    enCours.delete(nom); vus.add(nom); ordre.push(nom);
  }
  for (const nom of Object.keys(pile)) visiter(nom, []);
  return ordre;
}
console.log("   ordre : " + ordonner(PILE).join(" → "));
const CYCLIQUE = {
  appli: { pret: 1, depend: ["basededonnees"] },
  basededonnees: { pret: 1, depend: ["migration"] },
  migration: { pret: 1, depend: ["appli"] },
};
try { ordonner(CYCLIQUE); } catch (e) { console.log("   " + e.message); }
console.log("   C'est un parcours en profondeur avec marquage — celui du cours");
console.log("   d'algorithmique — et le cycle se détecte au retour sur un sommet");
console.log("   encore EN COURS de traitement, pas simplement déjà visité.");

En travaux pratiques

Travaux pratiques 4 · 3 h

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.

Avant de commencer

  • L'image du TP 3, qui se construit et se lance
  • Docker Compose disponible (docker compose version)

Énoncé

  1. Écrire la pile à trois servicesDécrivez l'application, une base PostgreSQL et un cache Redis. L'application se connecte aux deux. Ne publiez QUE le port de l'application. Indice : Les services se joignent par leur nom sur le réseau créé par Compose : aucune adresse IP n'a à figurer dans votre configuration.
  2. Provoquer la courseLancez 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. Essayer le mauvais correctifAjoutez 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. Indice : Une durée fixe encode la vitesse de VOTRE machine dans le fichier de configuration.
  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. Rendre l'application robusteLa 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. Déboguer un conteneurL'enseignant fournit une variante dont un service s'arrête immédiatement. Diagnostiquez-la sans modifier le code, en suivant l'ordre du cours. Indice : Les journaux d'abord, puis le code de sortie, puis un interpréteur dans le conteneur.

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

Correction

La pilecompose.yml
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
    cache:
      condition: service_started

basededonnees:
  image: postgres:16-alpine
  environment:
    POSTGRES_USER: app
    POSTGRES_PASSWORD: secret
  volumes: ["donnees:/var/lib/postgresql/data"]
  healthcheck:
    test: ["CMD-SHELL", "pg_isready -U app"]
    interval: 5s
    timeout: 3s
    retries: 10
    start_period: 20s

cache:
  image: redis:7-alpine

volumes:
donnees:

Ni la base ni le cache ne publient de port : ils sont joignables par les autres conteneurs et invisibles de l'extérieur. Le volume nommé porte les données et survit à docker compose down.

Pourquoi l'attente fixe échoue

Avec les ressources de la base limitées, son démarrage passe de six à vingt secondes : l'attente de dix secondes ne suffit plus, et l'application échoue de nouveau. Augmenter à trente ralentit chaque démarrage de trente secondes et échouera le jour où la base en met trente-cinq. Une durée fixe encode la vitesse d'une machine donnée dans un fichier versionné.

La reprise, côté applicationsrc/base.js
async function connecter(essais = 8) {
let attente = 500;
for (let i = 1; i <= essais; i++) {
  try {
    return await pool.connect();
  } catch (e) {
    if (i === essais) throw e;
    console.warn("base indisponible, nouvel essai dans " + attente + " ms");
    await new Promise((r) => setTimeout(r, attente));
    attente *= 2;              // délai croissant
  }
}
}

C'est la seule des trois solutions qui survit à l'étape 5. La sonde règle le DÉMARRAGE ; en production, une base redémarre, un réseau se coupe, un service devient indisponible dix secondes — bien après. Compose garantit un démarrage propre, la reprise sur erreur est une propriété du code, et les deux se cumulent.

Le diagnostic du conteneur qui s'arrêteterminal
docker compose logs -f cache
docker compose ps          # regarder le CODE DE SORTIE
docker compose exec appli sh
env | grep DATABASE      # l'appli voit-elle ce qu'on croit ?
nc -zv basededonnees 5432

Codes de sortie utiles : 0 signifie un arrêt NORMAL — donc le processus principal s'est terminé, ce qui est une erreur de conception, le processus devant tourner au premier plan ; 1 est une erreur applicative, à lire dans les journaux ; 137 signifie tué par SIGKILL, typiquement une limite de mémoire dépassée.

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 · 5 cartes

Que fait depends_on, et que ne fait-il pas ?
Il exprime un ordre de LANCEMENT : le conteneur dépendant démarre après. Il n'attend PAS que le service soit PRÊT — un PostgreSQL démarré met encore quelques secondes à accepter les connexions, donc la dépendance est respectée et l'application échoue quand même. Le correctif est une SONDE DE SANTÉ associée à condition: service_healthy. Le correctif à ne pas écrire est un sleep, qui dépend de la vitesse de la machine et rallonge tous les démarrages.
Quelles sont les trois qualités d'une bonne sonde de santé ?
1) Elle teste CE DONT L'APPELANT A BESOIN — pg_isready teste l'acceptation de connexions, pas l'existence du processus. 2) Elle est RAPIDE ET SANS EFFET DE BORD : une sonde qui écrit en base ou interroge un service tiers transforme un incident distant en panne locale. 3) Son start_period couvre le DÉMARRAGE NORMAL, sans quoi un service lent est déclaré malade avant d'avoir eu sa chance.
Pourquoi la sonde de santé ne suffit-elle pas, et qu'est-ce qui tient en production ?
Parce qu'elle 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 — bien après le démarrage. La réponse robuste est dans l'APPLICATION : tenter, échouer, RÉESSAYER avec un délai croissant. Compose garantit un démarrage propre, la reprise sur erreur est une propriété du code, et les deux se cumulent.
Comment les services se joignent-ils, et que faut-il publier ?
Compose crée un réseau et y inscrit chaque service sous SON NOM : l'application atteint la base à basededonnees:5432, sans adresse IP. La publication de port ne concerne que l'accès depuis l'HÔTE : on ne publie donc QUE ce qui doit sortir. Publier le port de la base « pour s'y connecter » l'expose sur la machine — à réserver au fichier de surcharge de développement, jamais à la définition commune.
Quel est l'ordre de diagnostic d'un conteneur, et le piège récurrent ?
1) logs -f : un conteneur qui s'arrête a presque toujours écrit pourquoi. 2) ps : l'état et le CODE DE SORTIE — 0 arrêt normal (donc le processus principal s'est terminé, erreur de conception), 1 erreur applicative, 137 tué par SIGKILL, typiquement une limite mémoire. 3) exec sh : vérifier ce que l'application VOIT — variables, fichiers, base joignable par son nom. 4) inspect : la configuration effective. PIÈGE : un conteneur s'arrête quand son processus principal s'arrête — le processus doit tourner au PREMIER PLAN.