Version initiale : script, README et schéma d'architecture
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
719a533130
4 files changed
+3970
No files matched your search
@@ -0,0 +1,156 @@
|
||||
# maj-stack-compose
|
||||
|
||||
Script shell qui parcourt toutes les stacks **docker compose** d'un répertoire racine, détecte celles dont au moins une image a une nouvelle version, et les relance proprement (`down` puis `up -d`).
|
||||
|
||||

|
||||
|
||||
## Principe
|
||||
|
||||
Chaque sous-répertoire de la racine (par défaut `/home`) contenant un fichier compose est une stack :
|
||||
|
||||
```
|
||||
/home
|
||||
├── freshrss/ compose.yaml → stack « freshrss »
|
||||
├── gitea/ docker-compose.yml → stack « gitea »
|
||||
├── immich/ compose.yml → stack « immich »
|
||||
└── notes/ (pas de fichier compose → ignoré)
|
||||
```
|
||||
|
||||
Pour chaque stack, le script :
|
||||
|
||||
1. **vérifie le fichier compose** avec `docker compose config -q` :
|
||||
- fichier vide → stack ignorée sans rien afficher ;
|
||||
- fichier invalide → erreur affichée en rouge ;
|
||||
2. **ignore les stacks arrêtées** (sauf avec `-a`), pour ne pas démarrer ce qui a été coupé volontairement ;
|
||||
3. **télécharge les images** avec `docker compose pull`. Cette étape ne touche pas aux conteneurs qui tournent ;
|
||||
4. **compare**, pour chaque conteneur, l'image qu'il utilise avec l'image qui vient d'être téléchargée pour le même tag ;
|
||||
5. si au moins une image a changé (ou avec `-f`), **relance toute la stack** :
|
||||
`docker compose down` puis `docker compose up -d`.
|
||||
|
||||
Les fichiers compose reconnus sont, dans cet ordre : `compose.yaml`, `compose.yml`, `docker-compose.yaml`, `docker-compose.yml`.
|
||||
|
||||
## Prérequis
|
||||
|
||||
- Linux avec **Docker** et **Docker Compose v2** (commande `docker compose`) ;
|
||||
- `flock` (paquet `util-linux`, présent sur toutes les distributions courantes) ;
|
||||
- un utilisateur ayant accès à Docker (root ou membre du groupe `docker`).
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
git clone https://gitea.vlab.bzh/vLab-BZH/maj-stack-compose.git
|
||||
cd maj-stack-compose
|
||||
chmod +x update-stacks.sh
|
||||
```
|
||||
|
||||
## Utilisation
|
||||
|
||||
```
|
||||
update-stacks.sh [-v | -q] [-n] [-f] [-p] [-a] [RACINE]
|
||||
```
|
||||
|
||||
| Option | Effet |
|
||||
|---|---|
|
||||
| `-v` | **verbose** : détaille chaque conteneur (image actuelle → nouvelle), chaque commande docker et sa sortie |
|
||||
| `-q` | **quiet** : une seule ligne par stack (nom, répertoire, état) |
|
||||
| `-n` | **dry-run** : détecte les mises à jour mais ne relance rien |
|
||||
| `-f` | **force** : `down` + `up -d` de chaque stack, même sans mise à jour |
|
||||
| `-p` | **prune** : supprime les images orphelines à la fin (`docker image prune -f`) |
|
||||
| `-a` | **all** : traite aussi les stacks arrêtées (sinon elles sont ignorées) |
|
||||
| `-h` | affiche l'aide |
|
||||
| `RACINE` | répertoire contenant les stacks (défaut : `/home`) |
|
||||
|
||||
### Exemples
|
||||
|
||||
Voir ce qui serait mis à jour, sans rien toucher :
|
||||
|
||||
```bash
|
||||
./update-stacks.sh -n /home
|
||||
```
|
||||
|
||||
Mettre à jour, avec un affichage minimal :
|
||||
|
||||
```bash
|
||||
./update-stacks.sh -q /home
|
||||
```
|
||||
|
||||
Mettre à jour, puis supprimer les anciennes images :
|
||||
|
||||
```bash
|
||||
./update-stacks.sh -p /home
|
||||
```
|
||||
|
||||
Relancer toutes les stacks, y compris celles qui sont arrêtées :
|
||||
|
||||
```bash
|
||||
./update-stacks.sh -f -a /home
|
||||
```
|
||||
|
||||
## Affichage
|
||||
|
||||
### Couleurs
|
||||
|
||||
| Couleur | Signification |
|
||||
|---|---|
|
||||
| 🟢 vert | stack à jour |
|
||||
| 🟡 jaune | mise à jour détectée, appliquée (ou à faire en dry-run), ou redémarrage forcé |
|
||||
| 🔴 rouge | erreur (fichier compose invalide, échec du `pull`, du `down` ou du `up -d`) |
|
||||
| gris | stack arrêtée, ignorée |
|
||||
|
||||
Les couleurs sont désactivées automatiquement quand la sortie n'est pas un terminal (cron, redirection vers un fichier), ou si la variable `NO_COLOR` est définie.
|
||||
|
||||
### Mode quiet (`-q`)
|
||||
|
||||
```
|
||||
arcane /home/arcane ✔ à jour
|
||||
arcane-agent /home/arcane-agent arrêtée
|
||||
freshrss /home/freshrss ✔ à jour
|
||||
immich /home/immich ↑ mise à jour appliquée
|
||||
broken /home/broken ✘ erreur (fichier compose invalide)
|
||||
yaml: line 3: mapping values are not allowed
|
||||
```
|
||||
|
||||
### Mode normal (par défaut)
|
||||
|
||||
```
|
||||
[2026-10-02 04:00:01] Racine : /home
|
||||
[2026-10-02 04:00:01] == Stack : immich (/home/immich/compose.yml)
|
||||
[2026-10-02 04:00:09] ↑ immich-server (ghcr.io/immich-app/immich-server:release) : nouvelle image disponible
|
||||
[2026-10-02 04:00:31] ✔ stack relancée (down + up -d) : mise à jour
|
||||
[2026-10-02 04:00:31] Terminé. Stacks relancées : 1 immich | Échecs : 0
|
||||
```
|
||||
|
||||
### Mode verbose (`-v`)
|
||||
|
||||
Ajoute l'état de chaque conteneur avec les identifiants courts des images, ainsi que chaque commande docker (`$ docker compose pull …`) suivie de sa sortie complète.
|
||||
|
||||
En mode normal et quiet, la sortie de docker est masquée ; elle n'est affichée, sous la stack concernée, qu'en cas d'échec.
|
||||
|
||||
## Automatisation (cron)
|
||||
|
||||
Exemple : chaque nuit à 4 h, avec nettoyage des images et journal dans `/var/log` (crontab de root) :
|
||||
|
||||
```
|
||||
0 4 * * * /opt/maj-stack-compose/update-stacks.sh -p /home >> /var/log/update-stacks.log 2>&1
|
||||
```
|
||||
|
||||
Un verrou (`/tmp/update-stacks.lock`) empêche deux exécutions simultanées.
|
||||
|
||||
## Code de retour
|
||||
|
||||
| Code | Signification |
|
||||
|---|---|
|
||||
| `0` | toutes les stacks ont été traitées sans erreur |
|
||||
| `1` | au moins une stack en erreur, une autre exécution en cours, Docker Compose v2 absent ou racine introuvable |
|
||||
|
||||
## Limites
|
||||
|
||||
- **Tags figés** : seules les nouvelles images publiées **sous le même tag** sont détectées (`latest`, `release`, `16`…). Une image figée sur `nginx:1.25.3` ne passera jamais en `1.25.4` : changer de version reste une décision manuelle.
|
||||
- **Images construites localement** : les services avec `build:` sont ignorés au `pull` (`--ignore-buildable`) ; ils ne déclenchent donc pas de mise à jour.
|
||||
- **Coupure de service** : `down` puis `up -d` arrête toute la stack quelques secondes, y compris les conteneurs dont l'image n'a pas changé.
|
||||
- **Profondeur** : seuls les sous-répertoires directs de la racine sont examinés.
|
||||
|
||||
## Documentation
|
||||
|
||||
- [`docs/architecture.png`](docs/architecture.png) : schéma de fonctionnement du script ;
|
||||
- [`docs/architecture.excalidraw`](docs/architecture.excalidraw) : source du schéma, modifiable sur [excalidraw.com](https://excalidraw.com) (menu *Ouvrir*).
|
||||
File diff suppressed because it is too large.
Load diff
Binary file not shown.
|
After Width: | Height: | Size: 865 KiB |
@@ -0,0 +1,216 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# update-stacks.sh — Met à jour les stacks docker compose d'une racine donnée.
|
||||
#
|
||||
# Pour chaque sous-répertoire de la racine contenant un fichier compose :
|
||||
# 1. télécharge les images (pull) — sans toucher aux conteneurs en cours ;
|
||||
# 2. compare l'image utilisée par chaque conteneur à l'image fraîchement tirée ;
|
||||
# 3. si au moins une image a changé : down puis up -d de toute la stack.
|
||||
# Les sous-répertoires sans fichier compose, ou avec un fichier compose vide,
|
||||
# sont ignorés sans rien afficher. Un fichier compose invalide est signalé en erreur.
|
||||
#
|
||||
# Usage : update-stacks.sh [-v | -q] [-n] [-f] [-p] [-a] [RACINE]
|
||||
# -v verbose : détaille chaque conteneur et chaque commande docker
|
||||
# -q quiet : n'affiche qu'une ligne par stack (nom, répertoire, état)
|
||||
# -n dry-run : détecte les mises à jour mais ne relance rien
|
||||
# -f force : down + up -d de chaque stack, même sans mise à jour
|
||||
# -p prune : supprime les images orphelines (dangling) à la fin
|
||||
# -a all : traite aussi les stacks arrêtées (par défaut : ignorées)
|
||||
# RACINE : répertoire contenant les stacks (défaut : /home)
|
||||
#
|
||||
# Couleurs : vert = à jour, jaune = mise(s) à jour détectée(s) ou forcée(s), rouge = erreur.
|
||||
# Désactivées automatiquement hors terminal (cron, redirection) ou si NO_COLOR est défini.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
VERBOSITY=1 # 0 = quiet, 1 = normal, 2 = verbose
|
||||
DRY_RUN=0
|
||||
FORCE=0
|
||||
PRUNE=0
|
||||
INCLUDE_STOPPED=0
|
||||
|
||||
while getopts "vqnfpah" opt; do
|
||||
case "$opt" in
|
||||
v) VERBOSITY=2 ;;
|
||||
q) VERBOSITY=0 ;;
|
||||
n) DRY_RUN=1 ;;
|
||||
f) FORCE=1 ;;
|
||||
p) PRUNE=1 ;;
|
||||
a) INCLUDE_STOPPED=1 ;;
|
||||
h|*) sed -n '2,23p' "$0"; exit 0 ;;
|
||||
esac
|
||||
done
|
||||
shift $((OPTIND - 1))
|
||||
|
||||
ROOT="${1:-/home}"
|
||||
ROOT="${ROOT%/}"
|
||||
COMPOSE_FILES=(compose.yaml compose.yml docker-compose.yaml docker-compose.yml)
|
||||
LOCK_FILE="/tmp/update-stacks.lock"
|
||||
|
||||
if [[ -t 1 && -z "${NO_COLOR:-}" ]]; then
|
||||
GREEN=$'\e[32m' YELLOW=$'\e[33m' RED=$'\e[31m' BOLD=$'\e[1m' DIM=$'\e[2m' RESET=$'\e[0m'
|
||||
else
|
||||
GREEN="" YELLOW="" RED="" BOLD="" DIM="" RESET=""
|
||||
fi
|
||||
|
||||
ts() { date '+%F %T'; }
|
||||
log() { (( VERBOSITY >= 1 )) && printf '[%s] %s\n' "$(ts)" "$*"; }
|
||||
debug() { (( VERBOSITY >= 2 )) && printf '[%s] %s%s%s\n' "$(ts)" "$DIM" "$*" "$RESET"; }
|
||||
ok() { log " ${GREEN}✔ $*${RESET}"; }
|
||||
warn() { log " ${YELLOW}↑ $*${RESET}"; }
|
||||
error() { printf '[%s] %s✘ %s%s\n' "$(ts)" "$RED" "$*" "$RESET" >&2; }
|
||||
|
||||
# En mode quiet : une seule ligne par stack, l'état s'affiche au bout
|
||||
quiet_status() { (( VERBOSITY == 0 )) && printf '%s%s%s\n' "$1" "$2" "$RESET"; }
|
||||
|
||||
# Exécute une commande docker. En verbose, affiche la commande et sa sortie ;
|
||||
# sinon la sortie est gardée dans RUN_OUTPUT pour être affichée par fail().
|
||||
RUN_OUTPUT=""
|
||||
run() {
|
||||
RUN_OUTPUT=""
|
||||
if (( VERBOSITY >= 2 )); then
|
||||
debug " \$ $*"
|
||||
"$@"
|
||||
else
|
||||
RUN_OUTPUT=$("$@" 2>&1)
|
||||
fi
|
||||
}
|
||||
|
||||
# Signale l'échec d'une stack, avec la sortie de la commande fautive
|
||||
fail() {
|
||||
quiet_status "$RED" "✘ erreur ($1)"
|
||||
(( VERBOSITY >= 1 )) && error "$name : erreur ($1)"
|
||||
[[ -n "$RUN_OUTPUT" ]] && printf '%s\n' "$RUN_OUTPUT" | sed 's/^/ /' >&2
|
||||
failed_stacks+=("$name")
|
||||
}
|
||||
|
||||
# Empêche deux exécutions simultanées (utile en cron)
|
||||
exec 9>"$LOCK_FILE"
|
||||
if ! flock -n 9; then
|
||||
error "Une autre exécution est déjà en cours, abandon."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! docker compose version >/dev/null 2>&1; then
|
||||
error "'docker compose' (v2) introuvable."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
[[ -d "$ROOT" ]] || { error "$ROOT n'est pas un répertoire."; exit 1; }
|
||||
|
||||
# Renvoie 0 si au moins un conteneur de la stack tourne sur une image obsolète
|
||||
stack_has_updates() {
|
||||
local cid running_id image_ref latest_id service updated=1
|
||||
for cid in $(docker compose ps -q 2>/dev/null); do
|
||||
running_id=$(docker inspect -f '{{.Image}}' "$cid")
|
||||
image_ref=$(docker inspect -f '{{.Config.Image}}' "$cid")
|
||||
service=$(docker inspect -f '{{index .Config.Labels "com.docker.compose.service"}}' "$cid")
|
||||
latest_id=$(docker image inspect -f '{{.Id}}' "$image_ref" 2>/dev/null) || {
|
||||
debug " · $service ($image_ref) : image locale introuvable, ignoré"
|
||||
continue
|
||||
}
|
||||
if [[ "$running_id" != "$latest_id" ]]; then
|
||||
warn "$service ($image_ref) : nouvelle image disponible"
|
||||
debug " actuelle : ${running_id:7:12} → nouvelle : ${latest_id:7:12}"
|
||||
updated=0
|
||||
else
|
||||
debug " · $service ($image_ref) : à jour (${running_id:7:12})"
|
||||
fi
|
||||
done
|
||||
return $updated
|
||||
}
|
||||
|
||||
updated_stacks=()
|
||||
failed_stacks=()
|
||||
|
||||
log "${BOLD}Racine : $ROOT${RESET}$( (( DRY_RUN )) && echo ' [dry-run]')$( (( FORCE )) && echo ' [force]')"
|
||||
|
||||
for dir in "$ROOT"/*/; do
|
||||
dir="${dir%/}"
|
||||
name=$(basename "$dir")
|
||||
|
||||
found=""
|
||||
for f in "${COMPOSE_FILES[@]}"; do
|
||||
[[ -f "$dir/$f" ]] && { found="$f"; break; }
|
||||
done
|
||||
[[ -n "$found" ]] || continue
|
||||
|
||||
pushd "$dir" >/dev/null || continue
|
||||
|
||||
# Valide le fichier compose avant tout affichage : un fichier vide est ignoré
|
||||
# comme un répertoire sans stack, un fichier invalide est signalé.
|
||||
config_ok=1
|
||||
RUN_OUTPUT=$(docker compose config -q 2>&1) || config_ok=0
|
||||
if (( ! config_ok )) && [[ "$RUN_OUTPUT" == *"empty compose file"* ]]; then
|
||||
popd >/dev/null; continue
|
||||
fi
|
||||
|
||||
log "${BOLD}== Stack : $name${RESET} ($dir/$found)"
|
||||
(( VERBOSITY == 0 )) && printf '%-25s %-45s ' "$name" "$dir"
|
||||
|
||||
if (( ! config_ok )); then
|
||||
fail "fichier compose invalide"
|
||||
popd >/dev/null; continue
|
||||
fi
|
||||
|
||||
if [[ -z "$(docker compose ps -q 2>/dev/null)" && $INCLUDE_STOPPED -eq 0 ]]; then
|
||||
log " ${DIM}stack arrêtée, ignorée (utiliser -a pour l'inclure)${RESET}"
|
||||
quiet_status "$DIM" "arrêtée"
|
||||
popd >/dev/null; continue
|
||||
fi
|
||||
|
||||
# Le pull ne modifie pas les conteneurs en cours : il sert à la détection
|
||||
debug " Téléchargement des images…"
|
||||
if ! run docker compose pull --ignore-buildable; then
|
||||
fail "pull"
|
||||
popd >/dev/null; continue
|
||||
fi
|
||||
|
||||
# Stack arrêtée incluse via -a : aucun conteneur à comparer, on la lance.
|
||||
# stack_has_updates est toujours appelé pour afficher le détail, même avec -f.
|
||||
has_updates=0
|
||||
if [[ -z "$(docker compose ps -q 2>/dev/null)" ]] || stack_has_updates; then
|
||||
has_updates=1
|
||||
fi
|
||||
|
||||
if (( has_updates || FORCE )); then
|
||||
if (( has_updates )); then
|
||||
label="mise à jour" done_label="↑ mise à jour appliquée"
|
||||
else
|
||||
label="redémarrage forcé" done_label="↻ redémarrage forcé effectué"
|
||||
fi
|
||||
if [[ $DRY_RUN -eq 1 ]]; then
|
||||
warn "[dry-run] $label nécessaire, rien n'est relancé"
|
||||
quiet_status "$YELLOW" "↑ $label à faire"
|
||||
updated_stacks+=("$name")
|
||||
elif ! run docker compose down; then
|
||||
fail "down"
|
||||
elif ! run docker compose up -d; then
|
||||
fail "up -d"
|
||||
else
|
||||
log " ${YELLOW}✔ stack relancée (down + up -d) : $label${RESET}"
|
||||
quiet_status "$YELLOW" "$done_label"
|
||||
updated_stacks+=("$name")
|
||||
fi
|
||||
else
|
||||
ok "à jour"
|
||||
quiet_status "$GREEN" "✔ à jour"
|
||||
fi
|
||||
|
||||
popd >/dev/null
|
||||
done
|
||||
|
||||
if [[ $PRUNE -eq 1 && $DRY_RUN -eq 0 ]]; then
|
||||
log "Nettoyage des images orphelines…"
|
||||
run docker image prune -f
|
||||
fi
|
||||
|
||||
if (( ${#failed_stacks[@]} )); then
|
||||
color=$RED
|
||||
elif (( ${#updated_stacks[@]} )); then
|
||||
color=$YELLOW
|
||||
else
|
||||
color=$GREEN
|
||||
fi
|
||||
log "${color}Terminé. Stacks relancées : ${#updated_stacks[@]} ${updated_stacks[*]:-} | Échecs : ${#failed_stacks[@]} ${failed_stacks[*]:-}${RESET}"
|
||||
[[ ${#failed_stacks[@]} -eq 0 ]]
|
||||
Reference in new issue
Block a user