# 20 — Docker Compose : inventaire, flux et pseudo-shell > Axe D + livrables §4.1/§4.2. Conçu pour cocher chaque case de `validation_tache2.md §6` (« Focus Docker Compose »). Gestion **par SSH sur la machine cible** via `server/ssh/client.ts` et templates versionnés ; pas de moteur parallèle. --- ## 1. Méthode retenue (MVP) - **Gestion par SSH** sur la machine cible, réutilisant `runScriptSudo` / `runPlain` et la table `executions`, le WebSocket terminal, `rawLogPath`, `reportPath`, statut `ok|warning|error`. **Pas de second système de jobs Docker.** - Variante `docker context` over SSH : **citée comme alternative opérateur**, pas le moteur MVP. - **Découverte des stacks** depuis des **racines déclarées par machine** (`composeRoots`, ex. `/opt/stacks`, `/srv/docker`), scan limité en profondeur (`composeScanDepth`, défaut 4), puis validation UI. - **Détection par labels Compose** (`com.docker.compose.project`, `com.docker.compose.service`, et si présent `com.docker.compose.project.working_dir`) = **complément** pour retrouver les stacks actifs, pas l'unique source de vérité. - Cycle de vie d'un stack : `candidate` (juste détecté) → `enabled` (validé par l'utilisateur) → actions autorisées. `pull`, `up`, `down`, `prune` **uniquement sur un stack `enabled`/validé**. Sources citées : `docker compose pull` https://docs.docker.com/reference/cli/docker/compose/pull/ · `up` https://docs.docker.com/reference/cli/docker/compose/up/ · `config` https://docs.docker.com/reference/cli/docker/compose/config/ · `ps` https://docs.docker.com/reference/cli/docker/compose/ps/ · `images` https://docs.docker.com/reference/cli/docker/compose/images/ · `down` https://docs.docker.com/reference/cli/docker/compose/down/ · `image inspect` https://docs.docker.com/reference/cli/docker/image/inspect/ · `image prune` https://docs.docker.com/reference/cli/docker/image/prune/ --- ## 2. Inventaire des templates Docker | Template | Action (`ActionType`) | Rôle | Effet disque | Destructif ? | Validation UI | |---|---|---|---|---|---| | `docker/scan-compose.sh.tpl` | `docker_scan` | Scanne les racines déclarées, trouve les fichiers compose, valide chaque candidat. **Passif.** | non | non | non | | `docker/inspect-compose.sh.tpl` | `docker_inspect_current` | État actuel sans changement : config images, ps, images, inspect. **Passif.** | non | non | non | | `docker/pull-check.sh.tpl` | `docker_pull_check` | `docker compose pull` (télécharge sans démarrer), compare ID/digest/labels avant-après. **Écrit sur le disque Docker** (pas un scan pur). | oui (cache images) | non (pas applicatif) | non (mais non passif) | | `docker/apply-compose.sh.tpl` | `docker_compose_apply` | `docker compose up -d --remove-orphans` après validation. Recapture ps/images/inspect. | oui | oui (recrée conteneurs) | **oui, explicite** | | `docker/prune-images.sh.tpl` | `docker_prune_images` | `docker image prune -f` (safe) ; mode agressif `-a -f --filter "until=168h"`. | oui | oui (agressif) | **oui pour agressif** | | `docker/down-compose.sh.tpl` | `docker_compose_down` | `docker compose down`. **Action séparée et destructive**, hors chemin de mise à jour normal. | oui | oui | **oui, forte** | > `--volumes` et `--rmi` sur `down` : **interdits au MVP** (ou protégés par une validation forte distincte). Le chemin de mise à jour normal n'utilise jamais `down` : `up -d` recrée les conteneurs quand l'image ou la config change, en préservant les volumes montés. Tous les templates : `LC_ALL=C`, marqueurs `===SU:DOCKER_*===`, sortie parsable, log brut archivé. --- ## 3. Flux de mise à jour Docker (formalisé) 1. `docker_scan` — découverte des stacks candidats (racines déclarées + labels actifs). 2. `docker_inspect_current` — état actuel des services, conteneurs, images. 3. `docker_pull_check` — téléchargement des images candidates **sans démarrage de conteneurs**. 4. Comparaison déterministe — image ref, image ID, repo digest, labels OCI (`org.opencontainers.image.version`, `revision`, `source`, `created`) si présents. 5. Proposition UI/Hermes — liste des stacks/services avec update dispo, erreurs de pull, inconnues. 6. `docker_compose_apply` après validation utilisateur — `docker compose up -d --remove-orphans`. 7. Vérification après application — conteneurs recréés, état `running/exited`, health si dispo, erreurs. 8. `docker_prune_images` après succès ou action séparée — images supprimées, espace récupéré, erreurs. **Points clés validés** : - `docker compose pull` télécharge mais **ne démarre pas** les conteneurs → bon pré-check applicatif, pas un scan sans effet. - `docker compose up -d` recrée les conteneurs quand l'image/config a changé, **en préservant les volumes** → `down` inutile pour une MAJ normale. - `docker image prune -f` = images *dangling* (sûr) ; `docker image prune -a` = **toutes** les images non référencées par un conteneur (destructif). --- ## 4. Pseudo-shell des templates ### 4.1 `docker/scan-compose.sh.tpl` ```sh #!/bin/sh export LC_ALL=C echo "===SU:DOCKER_SCAN===" # {{composeRoots}} rendu en liste shell-safe par le backend (une racine par ligne). ROOTS="{{composeRoots}}" DEPTH="{{composeScanDepth}}" for root in $ROOTS; do [ -d "$root" ] || continue find "$root" -maxdepth "$DEPTH" -type f \ \( -name 'compose.yaml' -o -name 'compose.yml' \ -o -name 'docker-compose.yaml' -o -name 'docker-compose.yml' \) \ -not -path '*/.git/*' -not -path '*/node_modules/*' \ -not -path '*/backup/*' -not -path '*/old/*' -not -path '*/archive/*' \ 2>/dev/null | while IFS= read -r f; do dir=$(dirname "$f") # Valide le candidat ; n'applique rien. if docker compose -f "$f" config --quiet >/dev/null 2>&1; then echo "STACK_OK\tdir=$dir\tfile=$f" else echo "STACK_INVALID\tdir=$dir\tfile=$f" fi done done echo "===SU:DOCKER_LABELS===" # Complément : stacks actifs détectés par labels. docker ps --format '{{ "{{.ID}}" }}' 2>/dev/null | while read -r id; do proj=$(docker inspect --format '{{ "{{index .Config.Labels \"com.docker.compose.project\"}}" }}' "$id" 2>/dev/null) wd=$(docker inspect --format '{{ "{{index .Config.Labels \"com.docker.compose.project.working_dir\"}}" }}' "$id" 2>/dev/null) [ -n "$proj" ] && echo "ACTIVE\tproject=$proj\tworking_dir=$wd" done echo "===SU:EXIT=0===" ``` > Note de rendu : les `{{ }}` Docker Go-template sont échappés ici pour ne pas être interprétés par Mustache (le moteur de rendu réel utilisera des délimiteurs Mustache personnalisés ou un échappement, à fixer en implémentation). Seules `composeRoots`/`composeScanDepth` sont des variables Mustache. ### 4.2 `docker/inspect-compose.sh.tpl` ```sh #!/bin/sh export LC_ALL=C cd "{{stackDir}}" || { echo "===SU:DOCKER_ERR===\ncompose_not_found"; echo "===SU:EXIT=2==="; exit 2; } echo "===SU:DOCKER_CONFIG_IMAGES===" docker compose config --images 2>&1 echo "===SU:DOCKER_PS===" docker compose ps --format json 2>&1 echo "===SU:DOCKER_IMAGES===" docker compose images --format json 2>&1 echo "===SU:DOCKER_INSPECT===" # Pour chaque image utilisée : Id, RepoDigests, labels OCI. docker compose config --images 2>/dev/null | while IFS= read -r img; do docker image inspect "$img" \ --format '{{ "IMG\t{{.Id}}\t{{join .RepoDigests \",\"}}\t{{index .Config.Labels \"org.opencontainers.image.version\"}}\t{{index .Config.Labels \"org.opencontainers.image.source\"}}" }}' 2>/dev/null \ || echo "IMG_MISSING\t$img" done echo "===SU:EXIT=0===" ``` ### 4.3 `docker/pull-check.sh.tpl` ```sh #!/bin/sh export LC_ALL=C cd "{{stackDir}}" || { echo "compose_not_found"; echo "===SU:EXIT=2==="; exit 2; } echo "===SU:DOCKER_INSPECT_BEFORE===" docker compose config --images 2>/dev/null | while IFS= read -r img; do id=$(docker image inspect "$img" --format '{{ "{{.Id}}" }}' 2>/dev/null || echo "") dg=$(docker image inspect "$img" --format '{{ "{{join .RepoDigests \",\"}}" }}' 2>/dev/null || echo "") echo "BEFORE\t$img\t$id\t$dg" done echo "===SU:DOCKER_PULL===" # Télécharge les images candidates SANS démarrer de conteneurs. docker compose pull --policy always --ignore-buildable 2>&1 CODE=$? echo "===SU:DOCKER_INSPECT_AFTER===" docker compose config --images 2>/dev/null | while IFS= read -r img; do id=$(docker image inspect "$img" --format '{{ "{{.Id}}" }}' 2>/dev/null || echo "") dg=$(docker image inspect "$img" --format '{{ "{{join .RepoDigests \",\"}}" }}' 2>/dev/null || echo "") ver=$(docker image inspect "$img" --format '{{ "{{index .Config.Labels \"org.opencontainers.image.version\"}}" }}' 2>/dev/null || echo "") echo "AFTER\t$img\t$id\t$dg\t$ver" done echo "===SU:EXIT=${CODE}===" ``` Backend : compare `BEFORE`/`AFTER` par `image ref`. Si `id`/`digest` change → `updates_available`. Erreurs `pull_failed`/`registry_auth_failed` nettoyées (jamais d'URL/token sensible vers UI/MCP). ### 4.4 `docker/apply-compose.sh.tpl` ```sh #!/bin/sh export LC_ALL=C cd "{{stackDir}}" || { echo "compose_not_found"; echo "===SU:EXIT=2==="; exit 2; } echo "===SU:DOCKER_APPLY===" docker compose up -d --remove-orphans 2>&1 CODE=$? echo "===SU:DOCKER_PS_AFTER===" docker compose ps --format json 2>&1 echo "===SU:DOCKER_INSPECT_AFTER===" docker compose config --images 2>/dev/null | while IFS= read -r img; do docker image inspect "$img" --format '{{ "IMG\t{{.Id}}\t{{join .RepoDigests \",\"}}" }}' 2>/dev/null || echo "IMG_MISSING\t$img" done echo "===SU:EXIT=${CODE}===" ``` ### 4.5 `docker/prune-images.sh.tpl` ```sh #!/bin/sh export LC_ALL=C echo "===SU:DOCKER_PRUNE===" {{#aggressive}} # Mode agressif : nécessite validation UI explicite distincte. docker image prune -a -f --filter "until=168h" 2>&1 {{/aggressive}} {{^aggressive}} # Mode sûr par défaut : dangling images uniquement. docker image prune -f 2>&1 {{/aggressive}} CODE=$? echo "===SU:EXIT=${CODE}===" ``` Le backend parse `Total reclaimed space` et `deleted` pour `bytesReclaimed` et la liste d'images supprimées. ### 4.6 `docker/down-compose.sh.tpl` ```sh #!/bin/sh export LC_ALL=C cd "{{stackDir}}" || { echo "compose_not_found"; echo "===SU:EXIT=2==="; exit 2; } echo "===SU:DOCKER_DOWN===" # --volumes et --rmi INTERDITS au MVP. down simple uniquement. docker compose down 2>&1 CODE=$? echo "===SU:EXIT=${CODE}===" ``` --- ## 5. Réduction Hermes (Docker) Seules ces lignes (+ le JSON canonique) sont transmises : `Pulling`, `Digest`, `Status`, `Downloaded newer image`, `Recreating`, `Started`, `Error`, `deleted`, `Total reclaimed space`. Le log brut complet reste archivé (`raw_artifacts` / `rawLogPath`). --- ## 6. Insertion dans la webapp existante - **Config machine** : nouveaux champs `dockerEnabled`, `composeRoots[]`, `composeScanDepth`, `composeStacks[]` — **optionnels** ou dans un endpoint dédié ; `MachineView` n'est pas cassé (les champs sont ajoutés en option). Stockés dans `docker_settings` / `docker_compose_roots` / `docker_compose_stacks` (`tache1.9.md`). - **Refresh/snapshot** : le refresh machine peut produire un snapshot combiné `apt` + `docker`, **ou** un refresh Docker séparé pour éviter de lancer `docker_pull_check` automatiquement (recommandé : pull-check séparé car non-passif). - **Actions** : extension progressive de `ActionType` (`docker_scan`, `docker_pull_check`, `docker_compose_apply`, `docker_prune_images`, `docker_compose_down`) avec filtrage d'autorisation conservé sur `POST /api/machines/:id/actions`. - **Executions/rapports** : réutilisation de la table `executions`, du WebSocket terminal, de `rawLogPath`/`reportPath`/statut. Pas de second système. - **UI machine** : compteur Docker séparé du compteur APT (ex. stacks avec updates) ; vue détail par stack/service ; boutons d'action validés (`Pull/check`, `Appliquer`, `Prune`, `Down`). - **Validation utilisateur** : `docker_compose_apply`, `docker_prune_images` agressif et `docker_compose_down` passent par une confirmation UI explicite (via `action_requests`). Hermes propose, ne déclenche jamais. - **Secrets** : credentials registry (`~/.docker/config.json`, helpers, tokens) **jamais lus ni renvoyés** ; erreurs nettoyées si elles exposent une URL sensible (voir `70-securite.md`).