diff --git a/README.md b/README.md index 9da37ad..770bba4 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,20 @@ Dieses Script überprüft mehrere Docker-Compose-Stacks auf Image-Updates und ak ## 🚀 Features - 🔄 Stack-basiertes Update +Aktualisiert komplette Docker-Compose Stacks strukturiert und kontrolliert - 🧪 Dry-Run Modus +Zeigt an, was passieren würde, ohne Änderungen durchzuführen - 📲 ntfy Benachrichtigungen -- ⏭️ Exclude-Liste für ganze Stacks oder einzelne Container +Push-Notifications über ntfy bei Updates, Fehlern oder Status +- ⏭️ Exclude-Liste +Einzelne Container oder komplette Stacks gezielt vom Update ausschließen - 🗑️ Prune Funktion +Entfernt nicht mehr benötigte Images/Container automatisch +- ⚡ Digest-basierter Update-Check (kein Blind-Pull) +Images werden nicht pauschal gepullt. Stattdessen: +Vergleich des lokalen Image-Digests mit dem Remote-Digest +Pull nur bei tatsächlicher Änderung +Spart Bandbreite, Zeit und unnötige Layer-Downloads --- @@ -16,6 +26,7 @@ Dieses Script überprüft mehrere Docker-Compose-Stacks auf Image-Updates und ak - Docker + Docker Compose (v2) - Bash +- jq - Optional: ntfy Server --- @@ -23,93 +34,85 @@ Dieses Script überprüft mehrere Docker-Compose-Stacks auf Image-Updates und ak ## ⚙️ Konfiguration (`config.conf`) ```bash -# ============================= -# ============================= -# Pfade -# ============================= +# ========================================================= +# DOCKER COMPOSE UPDATER - CONFIG +# ========================================================= -# Pfad zu deinen Compose-Files -COMPOSE_DIR="/pfad/zu/deinen/stacks" -# Logging -LOG_FILE="/pfad/zum/log/update.log" -LOG_LEVEL="INFO" # DEBUG=sehr detailliert, INFO=Standard, WARN=nur wichtige Hinweise/Updates, ERROR=nur Fehler -# Dateimuster -COMPOSE_PATTERN="docker-compose.yml" +# --------------------------------------------------------- +# PATH +# --------------------------------------------------------- -# ============================= -# ============================= -# Allgemein Einstellungen -# ============================= +PATH_COMPOSE_DIR="/pfad/zu/deinen/stacks" # Basisverzeichnis der Stacks +PATH_COMPOSE_PATTERN="*compose*.yml" # Compose-Dateiname -# Verhalten bei gestoppten Containern -UPDATE_STOPPED=true # Image aktualisieren -START_STOPPED=false # danach NICHT starten -# Dry Run (true/false) -DRY_RUN=false +# --------------------------------------------------------- +# LOG +# --------------------------------------------------------- -# ============================= -# ============================= -# Exclude -# ============================= +LOG_FILE="/pfad/zum/log/update.log" # Log-Datei +LOG_LEVEL="INFO" # DEBUG [ INFO | WARN | ERROR ] -# Exclude Container -EXCLUDE_SERVICES=( - "example_container_1" - "example_container_2" -) -# Exclude Stack -EXCLUDE_STACKS=( +# --------------------------------------------------------- +# UPDATE +# --------------------------------------------------------- + +UPDATE_DRY_RUN=false # Nur Simulation, keine Änderungen [ true | false ] +UPDATE_INCLUDE_STOPPED=true # Gestoppte Container updaten [ true | false ] +UPDATE_START_STOPPED=false # Danach wieder starten [ true | false ] + + +# --------------------------------------------------------- +# REDEPLOY +# --------------------------------------------------------- +REDEPLOY_WAIT_HEALTHY=true # Warten bis Container healthy [ true | false ] +REDEPLOY_TIMEOUT=60 # Timeout in Sekunden [ Sekunden ] + + +# --------------------------------------------------------- +# EXCLUDES +# --------------------------------------------------------- + +EXCLUDE_STACKS=( # Liste mit ganzen Stacks die nicht geupdated werden "example_stack_1" "example_stack_2" ) -# ============================= -# ============================= -# NTFY -# ============================= +EXCLUDE_SERVICES=( # Liste mit einzelnen Containern die nicht geupdated werden + "example_container_1" + "example_container_2" +) + + +# --------------------------------------------------------- +# NTFY SETTINGS +# --------------------------------------------------------- + NTFY_ENABLED=true -NTFY_TITLE="Docker Update ($(hostname))" -NTFY_TOKEN="DEIN_TOKEN" NTFY_URL="https://ntfy.example.com/topic" -NTFY_IMAGE_URL="http://dein-server/host-icon.png" +NTFY_TOKEN="DEIN_TOKEN" +NTFY_TITLE="Docker Update ($(hostname))" NTFY_TAGS="docker,update" -NTFY_ONLY_ON_CHANGES=false -# Versions Nr. anzeigen (true/false) -SHOW_VERSIONS=true +NTFY_IMAGE_URL="http://dein-server/host-icon.png" +NTFY_ONLY_ON_CHANGES=false # Nur senden wenn Updates vorhanden +NTFY_SHOW_VERSIONS=true # Versionsnummern anzeigen -# ============================= -# ============================= -# Docker Cleanup -# ============================= -ENABLE_CLEANUP=true -CLEANUP_ONLY_ON_UPDATE=true +# --------------------------------------------------------- +# DOCKER CLEANUP +# --------------------------------------------------------- -# Images: -# 🟢 dangling → docker image prune (nur Images) -# 🟡 unused → docker image prune -a (alle ungenutzten Images) -CLEANUP_IMAGES=true -CLEANUP_IMAGES_MODE="unused" # dangling | unused +CLEANUP_ENABLED=true # Aktivieren [ true | false ] +CLEANUP_ONLY_ON_UPDATE=true # Nur nach Updates ausführen [ true | false ] -# Container: -# entfernt gestoppte Container -# 🟢 docker container prune -CLEANUP_CONTAINERS=true +CLEANUP_IMAGES_ENABLED=true # Images löschen [ true | false ] +CLEANUP_IMAGES_MODE="unused" # Methode [ dangling | unused ] -# Volume: -# entfernt ungenutzte Volumes -# ⚠️ kann Daten löschen -CLEANUP_VOLUMES=false - -# Networks: -# entfernt ungenutzte Netzwerke -# 🟢 meist unkritisch -CLEANUP_NETWORKS=true - -# ============================= +CLEANUP_CONTAINERS_ENABLED=true # Container löschen [ true | false ] +CLEANUP_VOLUMES_ENABLED=false # Volumes löschen [ true | false ] +CLEANUP_NETWORKS_ENABLED=true # Networks löschen [ true | false ] ``` --- @@ -125,13 +128,28 @@ chmod +x script.sh ## 🧠 Funktionsweise -1. Alle `docker-compose.yml` Dateien werden gefunden -2. Alphabetisch sortiert -3. Jeder Stack wird geprüft: - - Image wird gepullt - - Vergleich: Container Image-ID vs. aktuelles Image -4. Wenn ein Service ein Update hat: - - kompletter Stack wird neu deployed +1. Alle `*compose*.yml` Dateien werden rekursiv gefunden +2. Verarbeitung erfolgt alphabetisch (deterministische Reihenfolge) +3. Für jeden Stack: + - Compose-Konfiguration wird ausgewertet (docker compose config) + - Verwendete Images werden extrahiert + - Für jedes Image: + - Remote-Digest wird aus der Registry abgefragt + -Lokaler Digest wird ermittelt + -Vergleich lokal vs. remote +4. Entscheidungslogik: + - ❌ Kein Unterschied → kein Pull, kein Restart + - ✅ Digest unterschiedlich → Image wird gepullt +5. Wenn mindestens ein Service ein Update hat: + - kompletter Stack wird neu deployed (docker compose up -d) + +--- + +## ⚡ Verhalten im Detail +- Kein unnötiger Netzwerk-Traffic (kein blindes docker pull) +- Updates erfolgen nur bei tatsächlichen Änderungen +- Mehrere Services im Stack → einheitlicher Redeploy, kein Teilzustand +- Optional: Dry-Run zeigt exakt diese Entscheidungen ohne Ausführung --- diff --git a/config.conf b/config.conf index 47358fb..24c5fdd 100644 --- a/config.conf +++ b/config.conf @@ -1,87 +1,79 @@ -# ============================= -# ============================= -# Pfade -# ============================= +# ========================================================= +# DOCKER COMPOSE UPDATER - CONFIG +# ========================================================= -# Pfad zu deinen Compose-Files -COMPOSE_DIR="/pfad/zu/deinen/stacks" -# Logging -LOG_FILE="/pfad/zum/log/update.log" -LOG_LEVEL="INFO" # DEBUG=sehr detailliert, INFO=Standard, WARN=nur wichtige Hinweise/Updates, ERROR=nur Fehler -# Dateimuster -COMPOSE_PATTERN="docker-compose.yml" +# --------------------------------------------------------- +# PATH +# --------------------------------------------------------- -# ============================= -# ============================= -# Allgemein Einstellungen -# ============================= +PATH_COMPOSE_DIR="/pfad/zu/deinen/stacks" # Basisverzeichnis der Stacks +PATH_COMPOSE_PATTERN="*compose*.yml" # Compose-Dateiname -# Verhalten bei gestoppten Containern -UPDATE_STOPPED=true # Image aktualisieren -START_STOPPED=false # danach NICHT starten -# Dry Run (true/false) -DRY_RUN=false +# --------------------------------------------------------- +# LOG +# --------------------------------------------------------- -# ============================= -# ============================= -# Exclude -# ============================= +LOG_FILE="/pfad/zum/log/update.log" # Log-Datei +LOG_LEVEL="INFO" # DEBUG [ INFO | WARN | ERROR ] -# Exclude Container -EXCLUDE_SERVICES=( - "example_container_1" - "example_container_2" -) -# Exclude Stack -EXCLUDE_STACKS=( +# --------------------------------------------------------- +# UPDATE +# --------------------------------------------------------- + +UPDATE_DRY_RUN=false # Nur Simulation, keine Änderungen [ true | false ] +UPDATE_INCLUDE_STOPPED=true # Gestoppte Container updaten [ true | false ] +UPDATE_START_STOPPED=false # Danach wieder starten [ true | false ] + + +# --------------------------------------------------------- +# REDEPLOY +# --------------------------------------------------------- +REDEPLOY_WAIT_HEALTHY=true # Warten bis Container healthy [ true | false ] +REDEPLOY_TIMEOUT=60 # Timeout in Sekunden [ Sekunden ] + + +# --------------------------------------------------------- +# EXCLUDES +# --------------------------------------------------------- + +EXCLUDE_STACKS=( # Liste mit ganzen Stacks die nicht geupdated werden "example_stack_1" "example_stack_2" ) -# ============================= -# ============================= -# NTFY -# ============================= +EXCLUDE_SERVICES=( # Liste mit einzelnen Containern die nicht geupdated werden + "example_container_1" + "example_container_2" +) + + +# --------------------------------------------------------- +# NTFY SETTINGS +# --------------------------------------------------------- + NTFY_ENABLED=true -NTFY_TITLE="Docker Update ($(hostname))" -NTFY_TOKEN="DEIN_TOKEN" NTFY_URL="https://ntfy.example.com/topic" -NTFY_IMAGE_URL="http://dein-server/host-icon.png" +NTFY_TOKEN="DEIN_TOKEN" +NTFY_TITLE="Docker Update ($(hostname))" NTFY_TAGS="docker,update" -NTFY_ONLY_ON_CHANGES=false -# Versions Nr. anzeigen (true/false) -SHOW_VERSIONS=true +NTFY_IMAGE_URL="http://dein-server/host-icon.png" +NTFY_ONLY_ON_CHANGES=false # Nur senden wenn Updates vorhanden +NTFY_SHOW_VERSIONS=true # Versionsnummern anzeigen -# ============================= -# ============================= -# Docker Cleanup -# ============================= -ENABLE_CLEANUP=true -CLEANUP_ONLY_ON_UPDATE=true +# --------------------------------------------------------- +# DOCKER CLEANUP +# --------------------------------------------------------- -# Images: -# 🟢 dangling → docker image prune (nur Images) -# 🟢 unused → docker image prune -a (alle ungenutzten Images) -CLEANUP_IMAGES=true -CLEANUP_IMAGES_MODE="unused" # dangling | unused +CLEANUP_ENABLED=true # Aktivieren [ true | false ] +CLEANUP_ONLY_ON_UPDATE=true # Nur nach Updates ausführen [ true | false ] -# Container: -# entfernt gestoppte Container -# 🟢 docker container prune -CLEANUP_CONTAINERS=true +CLEANUP_IMAGES_ENABLED=true # Images löschen [ true | false ] +CLEANUP_IMAGES_MODE="unused" # Methode [ dangling | unused ] -# Volume: -# entfernt ungenutzte Volumes -# ⚠️ kann Daten löschen -CLEANUP_VOLUMES=false - -# Networks: -# entfernt ungenutzte Netzwerke -# 🟢 meist unkritisch -CLEANUP_NETWORKS=true - -# ============================= \ No newline at end of file +CLEANUP_CONTAINERS_ENABLED=true # Container löschen [ true | false ] +CLEANUP_VOLUMES_ENABLED=false # Volumes löschen [ true | false ] +CLEANUP_NETWORKS_ENABLED=true # Networks löschen [ true | false ] \ No newline at end of file diff --git a/shell_docker_compose_update.sh b/shell_docker_compose_update.sh index a28c6ca..a24c0cc 100644 --- a/shell_docker_compose_update.sh +++ b/shell_docker_compose_update.sh @@ -1,5 +1,4 @@ #!/bin/bash - set -euo pipefail BASE_DIR="$(cd "$(dirname "$0")" && pwd)" @@ -24,19 +23,14 @@ should_log() { } log() { - local level="$1" - shift + local level="$1"; shift local msg="$*" - if should_log "$level"; then echo "$(date '+%Y-%m-%d %H:%M:%S') | $level | $msg" | tee -a "$LOG_FILE" fi } -# ============================= # Log begrenzen -# ============================= - if [ -f "$LOG_FILE" ]; then line_count=$(wc -l < "$LOG_FILE") if [ "$line_count" -gt 1000 ]; then @@ -66,16 +60,16 @@ is_stack_excluded() { get_image() { local svc="$1" - docker compose config | awk "/^ $svc:/,/image:/" | grep image | head -n1 | awk '{print $2}' + + echo "$compose_json" \ + | jq -r --arg svc "$svc" '.services[$svc].image // empty' } get_container_image_id() { local svc="$1" local cid - cid=$(docker compose ps -q "$svc" 2>/dev/null || true) [ -z "$cid" ] && echo "" && return - docker inspect -f '{{.Image}}' "$cid" 2>/dev/null || echo "" } @@ -84,33 +78,16 @@ get_local_image_id() { docker image inspect -f '{{.Id}}' "$image" 2>/dev/null || echo "" } -get_container_image_ref() { - local svc="$1" - local cid - - cid=$(docker compose ps -q "$svc" 2>/dev/null || true) - [ -z "$cid" ] && echo "none" && return - - docker inspect -f '{{.Config.Image}}' "$cid" 2>/dev/null || echo "unknown" -} - -get_local_image_digest() { - local image="$1" - docker inspect --format='{{index .RepoDigests 0}}' "$image" 2>/dev/null || echo "unknown" -} - is_running() { local svc="$1" local cid - cid=$(docker compose ps -q "$svc" 2>/dev/null || true) [ -z "$cid" ] && return 1 - docker inspect -f '{{.State.Running}}' "$cid" 2>/dev/null | grep -q true } run_cmd() { - if [ "$DRY_RUN" = true ]; then + if [ "$UPDATE_DRY_RUN" = true ]; then log DEBUG "[DRY RUN] $*" else eval "$@" @@ -122,33 +99,21 @@ send_ntfy() { local prio="$2" local image_url="${NTFY_IMAGE_URL:-}" - if [ -n "$image_url" ]; then - curl -s \ - -H "Authorization: Bearer $NTFY_TOKEN" \ - -H "Title: $NTFY_TITLE" \ - -H "Priority: $prio" \ - -H "Tags: $NTFY_TAGS" \ - -H "Icon: $image_url" \ - -d "$msg" \ - "$NTFY_URL" > /dev/null || true - else - curl -s \ - -H "Authorization: Bearer $NTFY_TOKEN" \ - -H "Title: $NTFY_TITLE" \ - -H "Priority: $prio" \ - -H "Tags: $NTFY_TAGS" \ - -d "$msg" \ - "$NTFY_URL" > /dev/null || true - fi + curl -s \ + -H "Authorization: Bearer $NTFY_TOKEN" \ + -H "Title: $NTFY_TITLE" \ + -H "Priority: $prio" \ + -H "Tags: $NTFY_TAGS" \ + ${image_url:+-H "Icon: $image_url"} \ + -d "$msg" \ + "$NTFY_URL" > /dev/null || true } get_docker_disk_usage() { local total=0 - while read -r size; do num="${size//[!0-9.]/}" num="${num:-0}" - if [[ "$size" == *GB ]]; then total=$(bc <<< "$total + ($num * 1024)") elif [[ "$size" == *MB ]]; then @@ -161,17 +126,133 @@ get_docker_disk_usage() { LC_NUMERIC=C printf "%.0f\n" "$total" } +wait_for_healthy() { + local timeout="$1" + shift + local services=("$@") + + local start + start=$(date +%s) + + # 👉 Containerliste bestimmen + if [ ${#services[@]} -gt 0 ]; then + mapfile -t cids < <(docker compose ps -q "${services[@]}") + else + mapfile -t cids < <(docker compose ps -q) + fi + + # 👉 keine Container → nichts zu tun + if [ ${#cids[@]} -eq 0 ]; then + log INFO " ℹ️ Keine Container gefunden → überspringe Healthcheck" + return 0 + fi + + # 👉 prüfen ob überhaupt Healthchecks existieren + local has_healthcheck=false + + for cid in "${cids[@]}"; do + health=$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{end}}' "$cid" 2>/dev/null) + + if [ -n "$health" ]; then + has_healthcheck=true + break + fi + done + + # 👉 wenn keiner vorhanden → direkt raus + if [ "$has_healthcheck" = false ]; then + log INFO " ℹ️ Keine Healthchecks definiert → überspringe warten" + return 0 + fi + + log INFO " ⏳ Warte auf healthy Container (max ${timeout}s)" + + while true; do + local all_ok=true + + for cid in "${cids[@]}"; do + status=$(docker inspect -f '{{.State.Health.Status}}' "$cid" 2>/dev/null || echo "none") + + if [ "$status" = "starting" ] || [ "$status" = "unhealthy" ]; then + all_ok=false + break + fi + done + + if [ "$all_ok" = true ]; then + log INFO " ✔️ Alle Container healthy" + return 0 + fi + + local now + now=$(date +%s) + + if [ $((now - start)) -ge "$timeout" ]; then + log WARN " ⚠️ Healthcheck Timeout erreicht" + return 1 + fi + + sleep 2 + done +} + +get_remote_digest() { + local image="$1" + + docker manifest inspect "$image" 2>/dev/null \ + | grep -m1 '"digest"' \ + | awk -F '"' '{print $4}' \ + || true +} + +get_local_digest() { + local image="$1" + docker inspect --format='{{index .RepoDigests 0}}' "$image" 2>/dev/null \ + | cut -d'@' -f2 +} + +pull_with_retry() { + local image="$1" + + local retries=3 + local delay=3 + + for ((i=1; i<=retries; i++)); do + + if docker pull "$image" >/dev/null 2>&1; then + return 0 + fi + + if [ "$i" -lt "$retries" ]; then + log WARN " ⚠️ Pull fehlgeschlagen – Versuch $i/$retries, retry in ${delay}s" + sleep "$delay" + fi + done + + return 1 +} + +rate_limit() { + sleep 0.2 +} + + # ============================= # Start # ============================= log INFO "==== Docker Compose Update gestartet ====" +script_start=$(date +%s) + notify_stacks_updated=() notify_excluded_updates=() error_flag=false -cd "$COMPOSE_DIR" +declare -A stack_tree +stack_tree=() + +cd "$PATH_COMPOSE_DIR" while IFS= read -r -d '' file; do @@ -186,29 +267,30 @@ while IFS= read -r -d '' file; do log INFO "" log INFO "→ Prüfe Stack: $stack" + stack_start=$(date +%s) + cd "$dir" - mapfile -t services < <(docker compose config --services) + compose_json=$(docker compose config --format json) + mapfile -t services < <(docker compose config --services) total_services=${#services[@]} current_index=0 - # Running State merken declare -A was_running for svc in "${services[@]}"; do - if is_running "$svc"; then - was_running["$svc"]=1 - else - was_running["$svc"]=0 - fi + is_running "$svc" && was_running[$svc]=1 || was_running[$svc]=0 done stack_updated=false changed_services=() - version_report=() + stack_block="" + update_lines=() for svc in "${services[@]}"; do + update_needed=false + current_index=$((current_index + 1)) if [ "$current_index" -eq "$total_services" ]; then @@ -225,7 +307,7 @@ while IFS= read -r -d '' file; do fi if is_excluded "$svc"; then - docker pull "$image" >/dev/null 2>&1 || true + pull_with_retry "$image" || true notify_excluded_updates+=("$stack/$svc") log INFO " $prefix $svc (excluded)" continue @@ -234,48 +316,87 @@ while IFS= read -r -d '' file; do log INFO " $prefix $svc ($image)" before_id=$(get_container_image_id "$svc") - before_digest=$(docker inspect -f '{{.Image}}' "$(docker compose ps -q "$svc")" 2>/dev/null || echo "none") - if ! docker pull "$image" >/dev/null 2>&1; then - log ERROR " $prefix ❌ Pull fehlgeschlagen" - error_flag=true + # 👉 Skip wenn kein Container und nicht erlaubt + if [ -z "$before_id" ] && [ "$UPDATE_INCLUDE_STOPPED" = false ]; then + log INFO " ⏭️ übersprungen (kein Container vorhanden)" continue fi - after_id=$(get_local_image_id "$image") - after_digest=$(get_local_image_digest "$image") + # ============================= + # Digest Check + # ============================= + + local_digest=$(get_local_digest "$image") + + if [ -z "$local_digest" ]; then + update_needed=true + + elif [ -n "$before_id" ]; then + # 👉 nur bei laufenden Containern → teurer Check + remote_digest=$(get_remote_digest "$image" || true) + rate_limit + + if [ -z "$remote_digest" ]; then + log DEBUG " ⚠️ kein Remote Digest → fallback auf pull" + update_needed=true + elif [ "$remote_digest" != "$local_digest" ]; then + update_needed=true + else + update_needed=false + fi + + else + # 👉 gestoppter Container → KEIN manifest check + update_needed=true + fi + + # ============================= + # Pull + echte Prüfung + # ============================= + + if [ "$update_needed" = true ]; then + + if ! pull_with_retry "$image"; then + log ERROR " ❌ Pull fehlgeschlagen" + error_flag=true + continue + fi + + after_id=$(get_local_image_id "$image") + + # 👉 Nur vergleichen wenn Container existiert + if [ -n "$before_id" ]; then + if [ "$before_id" != "$after_id" ]; then + update_needed=true + else + update_needed=false + fi + else + # 👉 Kein Container → kein echtes "Update" + update_needed=false + fi + fi + + # ============================= + # Update erkannt + # ============================= + + if [ "$update_needed" = true ]; then - if [ -n "$before_id" ] && [ "$before_id" != "$after_id" ]; then stack_updated=true changed_services+=("$svc") - if [ "$SHOW_VERSIONS" = true ]; then - log INFO " ⬆️ UPDATE" + log INFO " ⬆️ UPDATE" + log INFO " alt: ${image}@${before_id}" + log INFO " neu: ${image}@${after_id}" - # Log (vollständig, für Debugging) - if [ -n "$before_id" ]; then - log INFO " alt: ${image}@${before_id}" - else - log INFO " alt: none" - fi - log INFO " neu: ${image}@${after_id}" + short_before="${before_id#sha256:}" + short_before="${short_before:0:6}" + short_after="${after_id#sha256:}" + short_after="${short_after:0:6}" - # Kurzversion für ntfy - if [ -n "$before_id" ]; then - short_before="${before_id#sha256:}" - short_before="${short_before:0:6}" - else - short_before="new" - fi - - short_after="${after_id#sha256:}" - short_after="${short_after:0:6}" - - version_report+=("$svc: $short_before → $short_after") - - else - log INFO " ⬆️ UPDATE" - fi + update_lines+=("$svc|$short_before|$short_after") fi done @@ -287,65 +408,89 @@ while IFS= read -r -d '' file; do if [ "$total_services" -eq 1 ]; then svc="${services[0]}" - log INFO " 🔄 Einzelcontainer-Update: $svc" if [ "${was_running[$svc]}" = 1 ]; then - if ! run_cmd docker compose up -d "$svc" --remove-orphans --no-color >/dev/null 2>&1; then - log ERROR " ❌ Update fehlgeschlagen für $svc" - error_flag=true - else - log INFO " ✔️ Container $svc aktualisiert" - fi + run_cmd docker compose up -d "$svc" --remove-orphans else - if ! run_cmd docker compose create "$svc" >/dev/null 2>&1; then - log ERROR " ❌ Create fehlgeschlagen für $svc" - error_flag=true + if [ "$UPDATE_START_STOPPED" = true ]; then + run_cmd docker compose up -d "$svc" --remove-orphans else - log INFO " ✔️ Container $svc aktualisiert (gestoppt)" + run_cmd docker compose create "$svc" fi fi - notify_stacks_updated+=("$stack ($svc)") + log INFO " ✔️ Container $svc aktualisiert" + + if [ "$REDEPLOY_WAIT_HEALTHY" = true ]; then + wait_for_healthy "$REDEPLOY_TIMEOUT" "$svc" + fi else log INFO " 🔄 Stack wird neu deployt (Trigger: ${changed_services[*]})" - log INFO " ⏳ Deploy läuft..." - if ! run_cmd docker compose up -d --remove-orphans --no-color >/dev/null 2>&1; then + + if ! run_cmd docker compose up -d --remove-orphans >/dev/null 2>&1; then log ERROR " ❌ Stack Update fehlgeschlagen" error_flag=true else log INFO " ✔️ Stack erfolgreich aktualisiert" - # vorher gestoppte wieder stoppen + if [ "$REDEPLOY_WAIT_HEALTHY" = true ]; then + wait_for_healthy "$REDEPLOY_TIMEOUT" "${changed_services[@]}" + fi + for svc in "${services[@]}"; do if [ "${was_running[$svc]}" = 0 ]; then log INFO " ⏹️ Stoppe $svc (war vorher gestoppt)" run_cmd docker compose stop "$svc" >/dev/null 2>&1 || true fi done - - notify_stacks_updated+=("$stack (${changed_services[*]})") fi fi - else - log DEBUG " ✔️ Keine Updates" + # ============================= + # Baum für ntfy bauen + # ============================= + + if [ ${#update_lines[@]} -gt 0 ]; then + stack_block="" + + for i in "${!update_lines[@]}"; do + IFS="|" read -r svc before after <<< "${update_lines[$i]}" + + if [ "$i" -eq $((${#update_lines[@]} - 1)) ]; then + prefix="└─" + else + prefix="├─" + fi + + if [ "$NTFY_SHOW_VERSIONS" = true ]; then + stack_block+=$'\n'"$prefix $svc ($before → $after)" + else + stack_block+=$'\n'"$prefix $svc" + fi + done + + stack_tree["$stack"]="$stack_block" + notify_stacks_updated+=("$stack") + fi fi - cd "$COMPOSE_DIR" + cd "$PATH_COMPOSE_DIR" -done < <(find . -name "$COMPOSE_PATTERN" -print0 | sort -z) + stack_end=$(date +%s) + log INFO " ⏱ Dauer: $((stack_end - stack_start))s" +done < <(find . -name "$PATH_COMPOSE_PATTERN" -print0 | sort -z) # ============================= -# Cleanup (mit Statistik) +# Cleanup # ============================= freed_space="0" -if [ "$ENABLE_CLEANUP" = true ]; then +if [ "$CLEANUP_ENABLED" = true ]; then if [ "$CLEANUP_ONLY_ON_UPDATE" = true ] && \ [ ${#notify_stacks_updated[@]} -eq 0 ]; then @@ -353,42 +498,31 @@ if [ "$ENABLE_CLEANUP" = true ]; then else before_size=$(get_docker_disk_usage) - log INFO "🧹 Docker Cleanup läuft..." - if [ "$CLEANUP_IMAGES" = true ]; then + if [ "$CLEANUP_IMAGES_ENABLED" = true ]; then case "$CLEANUP_IMAGES_MODE" in - unused) - log INFO " → Entferne ungenutzte Images" - run_cmd docker image prune -a -f >/dev/null 2>&1 - ;; - dangling) - log INFO " → Entferne dangling Images" - run_cmd docker image prune -f >/dev/null 2>&1 - ;; + unused) run_cmd docker image prune -a -f >/dev/null 2>&1 ;; + dangling) run_cmd docker image prune -f >/dev/null 2>&1 ;; esac fi - if [ "$CLEANUP_CONTAINERS" = true ]; then - log INFO " → Entferne gestoppte Container" + if [ "$CLEANUP_CONTAINERS_ENABLED" = true ]; then run_cmd docker container prune -f >/dev/null 2>&1 fi - if [ "$CLEANUP_VOLUMES" = true ]; then - log WARN " → Entferne ungenutzte Volumes" + if [ "$CLEANUP_VOLUMES_ENABLED" = true ]; then run_cmd docker volume prune -f >/dev/null 2>&1 fi - if [ "$CLEANUP_NETWORKS" = true ]; then - log INFO " → Entferne ungenutzte Netzwerke" + if [ "$CLEANUP_NETWORKS_ENABLED" = true ]; then run_cmd docker network prune -f >/dev/null 2>&1 fi after_size=$(get_docker_disk_usage) + freed_space=$((after_size < before_size ? before_size - after_size : 0)) - freed_space=$(awk "BEGIN {print $before_size - $after_size}") - - log INFO "✔️ Cleanup abgeschlossen (freigegeben: ${freed_space} MB)" + log INFO "✔️ Cleanup abgeschlossen (${freed_space} MB freigegeben)" fi fi @@ -398,21 +532,16 @@ fi if [ "$NTFY_ENABLED" = true ]; then - msg="Docker Compose Update Report" + msg="" - if [ ${#notify_stacks_updated[@]} -gt 0 ]; then - msg+=$'\n\n🔄 Aktualisierte Stacks' - for s in "${notify_stacks_updated[@]}"; do - msg+=$'\n - '"$s" + if [ ${#stack_tree[@]} -gt 0 ]; then + msg+=$'\n🔄 Stack Updates\n' + + for stack in $(printf "%s\n" "${!stack_tree[@]}" | sort); do + msg+=$'\n'"$stack" + msg+="${stack_tree[$stack]}" + msg+=$'\n' # Leerzeile nach jedem Stack done - - # 👉 Versionen ergänzen (nur wenn aktiviert und vorhanden) - if [ "$SHOW_VERSIONS" = true ] && [ ${#version_report[@]} -gt 0 ]; then - msg+=$'\n\n📦 Versionen' - for v in "${version_report[@]}"; do - msg+=$'\n '"$v" - done - fi fi if [ ${#notify_excluded_updates[@]} -gt 0 ]; then @@ -422,16 +551,7 @@ if [ "$NTFY_ENABLED" = true ]; then done fi - if [ "$error_flag" = true ]; then - msg+=$'\n\n❗ Fehler sind aufgetreten – Logs prüfen' - PRIORITY=5 - elif [ ${#notify_stacks_updated[@]} -gt 0 ]; then - PRIORITY=3 - else - PRIORITY=1 - fi - - if [ ${#notify_stacks_updated[@]} -eq 0 ] && \ + if [ ${#stack_tree[@]} -eq 0 ] && \ [ ${#notify_excluded_updates[@]} -eq 0 ]; then msg+=$'\n\n✔️ Alles aktuell' fi @@ -440,16 +560,18 @@ if [ "$NTFY_ENABLED" = true ]; then msg+=$'\n\n🧹 Cleanup: '"${freed_space} MB freigegeben" fi - if [ "$NTFY_ONLY_ON_CHANGES" = false ] || \ - [ ${#notify_stacks_updated[@]} -gt 0 ] || \ - [ ${#notify_excluded_updates[@]} -gt 0 ] || \ - [ "$error_flag" = true ]; then - - send_ntfy "$msg" "$PRIORITY" - log INFO "ntfy Nachricht gesendet (prio=$PRIORITY)" + if [ "$error_flag" = true ]; then + PRIORITY=5 + elif [ ${#stack_tree[@]} -gt 0 ]; then + PRIORITY=3 else - log INFO "keine Änderungen, keine ntfy Nachricht" + PRIORITY=1 fi + + send_ntfy "$msg" "$PRIORITY" + log INFO "ntfy Nachricht gesendet (prio=$PRIORITY)" fi +script_end=$(date +%s) +log INFO "⏱ Gesamtzeit: $((script_end - script_start))s" log INFO "==== Update beendet ====" \ No newline at end of file