diff --git a/README.md b/README.md index 0866b84..dc98c0d 100644 --- a/README.md +++ b/README.md @@ -14,11 +14,7 @@ Push-Notifications über ntfy bei Updates, Fehlern oder Status 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 + --- @@ -36,17 +32,25 @@ Spart Bandbreite, Zeit und unnötige Layer-Downloads 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 + - Compose-Konfiguration wird ausgewertet (`docker compose config`) + - Services und deren Images werden ermittelt - Für jedes Image: - - Remote-Digest wird aus der Registry abgefragt - - Lokaler Digest wird ermittelt - - Vergleich lokal vs. remote + - Image wird (einmal pro Stack) gepullt (`docker pull`, nutzt Cache) + - Lokale Image-ID wird ermittelt + - Image-ID des vorhandenen Containers wird ermittelt (auch für gestoppte Container) 4. Entscheidungslogik: - - ❌ Kein Unterschied → kein Pull, kein Restart - - ✅ Digest unterschiedlich → Image wird gepullt + - ❌ Container existiert nicht → kein Update (nur Definition vorhanden) + - ❌ Image-ID unverändert → kein Update + - ✅ Image-ID hat sich geändert → Update erkannt 5. Wenn mindestens ein Service ein Update hat: - - kompletter Stack wird neu deployed (docker compose up -d) + - Einzelcontainer: gezieltes Update des Services + - Mehrere Services: kompletter Stack wird neu deployed (`docker compose up -d`) +6. Sonderverhalten: + - Gestoppte Container werden ebenfalls geprüft und bei Updates berücksichtigt + - Gestoppte Container werden nach dem Update optional wieder gestoppt + - Excluded Services werden gepullt, aber **nicht** neu gestartet. + Verfügbare Updates werden erkannt und per NTFY gemeldet. + - Healthchecks können optional abgewartet werden --- @@ -176,7 +180,7 @@ NTFY_URL="https://ntfy.example.com/topic" NTFY_TOKEN="DEIN_TOKEN" # Titel mitsenden (optional) [ String ] -NTFY_TITLE="Docker Update ($(hostname))" +NTFY_TITLE="Autoupdate Report ($(hostname))" # Tags mitsenden (optional) [ String ] NTFY_TAGS="docker,update" diff --git a/config.conf b/config.conf index a1bf37b..71d75d4 100644 --- a/config.conf +++ b/config.conf @@ -81,7 +81,7 @@ NTFY_URL="https://ntfy.example.com/topic" NTFY_TOKEN="DEIN_TOKEN" # Titel mitsenden (optional) [ String ] -NTFY_TITLE="Docker Update ($(hostname))" +NTFY_TITLE="Autoupdate Report ($(hostname))" # Tags mitsenden (optional) [ String ] NTFY_TAGS="docker,update" diff --git a/composeupdater.png b/img/composeupdater.png similarity index 100% rename from composeupdater.png rename to img/composeupdater.png diff --git a/img/ntfy.png b/img/ntfy.png new file mode 100644 index 0000000..09e5191 Binary files /dev/null and b/img/ntfy.png differ diff --git a/shell_docker_compose_update.sh b/shell_docker_compose_update.sh index cddb1a7..d51ee36 100644 --- a/shell_docker_compose_update.sh +++ b/shell_docker_compose_update.sh @@ -68,7 +68,7 @@ get_image() { get_container_image_id() { local svc="$1" local cid - cid=$(docker compose ps -q "$svc" 2>/dev/null || true) + cid=$(docker compose ps -aq "$svc" | head -n1) [ -z "$cid" ] && echo "" && return docker inspect -f '{{.Image}}' "$cid" 2>/dev/null || echo "" } @@ -81,7 +81,7 @@ get_local_image_id() { is_running() { local svc="$1" local cid - cid=$(docker compose ps -q "$svc" 2>/dev/null || true) + cid=$(docker compose ps -aq "$svc" 2>/dev/null || true) [ -z "$cid" ] && return 1 docker inspect -f '{{.State.Running}}' "$cid" 2>/dev/null | grep -q true } @@ -137,9 +137,9 @@ wait_for_healthy() { # 👉 Containerliste bestimmen if [ ${#services[@]} -gt 0 ]; then - mapfile -t cids < <(docker compose ps -q "${services[@]}") + mapfile -t cids < <(docker compose ps -aq "${services[@]}") else - mapfile -t cids < <(docker compose ps -q) + mapfile -t cids < <(docker compose ps -aq) fi # 👉 keine Container → nichts zu tun @@ -197,21 +197,6 @@ wait_for_healthy() { 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" @@ -233,8 +218,9 @@ pull_with_retry() { return 1 } -rate_limit() { - sleep 0.2 +container_exists() { + local svc="$1" + docker compose ps -aq "$svc" 2>/dev/null | grep -q . } @@ -255,8 +241,11 @@ stack_tree=() cd "$PATH_COMPOSE_DIR" + while IFS= read -r -d '' file; do + declare -A pulled_images + dir=$(dirname "$file") stack=$(basename "$dir") @@ -325,58 +314,33 @@ while IFS= read -r -d '' file; do fi # ============================= - # Digest Check + # Pull + Vergleich (NEU) # ============================= - local_digest=$(get_local_digest "$image") - - if [ -z "$local_digest" ]; then - update_needed=true - - elif [ -n "$before_id" ]; then - # 👉 nur bei laufenden Containern - 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 + # 👉 Pull (nutzt Cache → schnell wenn nichts neu) + if [ -z "${pulled_images[$image]:-}" ]; then if ! pull_with_retry "$image"; then log ERROR " ❌ Pull fehlgeschlagen" error_flag=true continue fi + pulled_images[$image]=1 + else + log DEBUG " ⏩ Pull übersprungen (bereits gemacht)" + fi - after_id=$(get_local_image_id "$image") + 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 + if ! container_exists "$svc"; then + # 👉 Container existiert nicht → KEIN Update + update_needed=false + + elif [ "$before_id" != "$after_id" ]; then + update_needed=true + + else + update_needed=false fi # ============================= @@ -536,17 +500,17 @@ if [ "$NTFY_ENABLED" = true ]; then msg="" if [ ${#stack_tree[@]} -gt 0 ]; then - msg+=$'\n🔄 Stack Updates\n' + msg+=$'#### 🔄 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 + # msg+=$'\n' # Leerzeile nach jedem Stack done fi if [ ${#notify_excluded_updates[@]} -gt 0 ]; then - msg+=$'\n\n⏭️ Excluded (Update verfügbar)' + msg+=$'\n\n#### ⏭️ Excluded (Update verfügbar)' for s in "${notify_excluded_updates[@]}"; do msg+=$'\n - '"$s" done @@ -554,11 +518,11 @@ if [ "$NTFY_ENABLED" = true ]; then if [ ${#stack_tree[@]} -eq 0 ] && \ [ ${#notify_excluded_updates[@]} -eq 0 ]; then - msg+=$'\n\n✔️ Alles aktuell' + msg+=$'\n\n#### ✔️ Alles aktuell' fi if [ "$freed_space" != "0" ]; then - msg+=$'\n\n🧹 Cleanup: '"${freed_space} MB freigegeben" + msg+=$'\n\n#### 🧹 Cleanup: '"${freed_space} MB freigegeben" fi if [ "$error_flag" = true ]; then