Initial commit

This commit is contained in:
Thorsten committed 2026-05-12 15:51:05 +02:00
commit 567123a4b1
6 files changed
+1046

No files matched your search

+1
View File
@@ -0,0 +1 @@
config_own.conf
+340
View File
@@ -0,0 +1,340 @@
# Docker Compose Auto-Updater
<p align="center">
<img src="./images/composeupdater.png" alt="Logo" width="200">
</p>
---
> 🔧 Automatisches Update von Docker-Compose-Stacks mit feingranularer Steuerung per Labels
## 🚀 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**
Push-Notifications über ntfy bei Updates, Fehlern oder Status
- ⚙️ **Service-Modi** (per Label steuerbar)
Einzelne Container oder komplette Stacks gezielt vom Update ausschließen
- 🗑️ **Prune Funktion**
Entfernt nicht mehr benötigte Images/Container automatisch
---
## 📂 Voraussetzungen
- Docker + Docker Compose (v2)
- Bash
- jq
- Optional: ntfy Server
---
## 🧠 Funktionsweise
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`)
- Services und deren Images werden ermittelt
- Für jedes Image:
- Image wird bei Bedarf gepullt (maximal einmal pro Image und Stack, Cache-basiert)
- Lokale Image-ID wird ermittelt
- Image-ID des vorhandenen Containers wird ermittelt (auch für gestoppte Container)
4. Entscheidungslogik:
- ❌ 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:
- **Einzelcontainer:**
- gezieltes Update nur dieses Services (`docker compose up -d <service>`)
- **Mehrere Services:**
- kompletter Stack wird neu deployed (`docker compose up -d`)
- **Optional**
- feste Wartezeit nach dem Deploy (`REDEPLOY_WAIT`)
- anschließendes Warten auf erfolgreiche Healthchecks (`REDEPLOY_WAIT_HEALTHY`)
6. Sonderverhalten:
- Gestoppte Container werden ebenfalls geprüft und bei Updates berücksichtigt
- Gestoppte Container werden nach dem Update optional wieder gestoppt
- Service-Verhalten wird vollständig über Labels gesteuert (`composeupdater.mode`)
- Je nach Modus werden Services:
- komplett ignoriert (`ignore`)
- nur überwacht (`notify-only`)
- oder automatisch aktualisiert (`update`)
---
## ⚙️ Service-Modi (per Label steuerbar)
Du kannst das Verhalten einzelner Services oder ganzer Stacks über Labels steuern:
```yml
labels:
- composeupdater.mode=update
```
### 📊 Übersicht
| Mode | Pull | Compare | Update | ntfy |
| ----------- | ---- | ------- | ------ | ---- |
| update | ✅ | ✅ | ✅ | ✅ |
| notify-only | ✅ | ✅ | ❌ | ✅ |
| ignore | ❌ | ❌ | ❌ | ❌ |
### 🧠 Erklärung der Modi
🔄 `update` (**Standard**)
- Images werden gepullt
- Lokales Image wird mit dem Container verglichen
- Bei Änderungen wird der Service bzw. Stack aktualisiert
- ntfy-Benachrichtigung wird gesendet
🔔 `notify-only`
- Image wird gepullt (für Vergleich notwendig)
- Es wird geprüft, ob ein Update verfügbar ist
- Kein Container-Update / kein Restart
- ntfy informiert über verfügbare Updates
🚫 `ignore`
- Service wird komplett ignoriert
- Kein Pull
- Kein Vergleich
- Kein Update
- Keine Benachrichtigung
### 🧩 Beispiele
#### Service ausschließen (komplett ignorieren)
```yml
services:
db:
image: postgres:15
labels:
- composeupdater.mode=ignore
```
#### Nur Benachrichtigung, kein automatisches Update
```yml
services:
app:
image: myapp:latest
labels:
- composeupdater.mode=notify-only
```
#### Explizit Standardverhalten setzen
```yml
services:
web:
image: nginx:latest
labels:
- composeupdater.mode=update
```
---
## 🗑️ Prune / Cleanup
Nach Abschluss der Updates kann optional ein Docker Cleanup durchgeführt werden.
Dabei werden ungenutzte Ressourcen entfernt:
- `docker image prune`
→ entfernt ungenutzte („dangling“) Images
- `docker image prune -a`
→ entfernt alle Images, die von keinem Container verwendet werden
- `docker container prune`
→ entfernt gestoppte Container
- `docker volume prune`
→ entfernt ungenutzte Volumes
- `docker network prune`
→ entfernt ungenutzte Netzwerke
---
## 🔔 ntfy
| Zustand | Priorität |
|----------------------|----------|
| ✔️ Keine Updates | 1 |
| 🔄 Updates vorhanden | 3 |
| ❌ Fehler | 5 |
### Anzeigebeispiel
<p align="left">
<img src="./images/ntfy.png" alt="Logo" width="400">
</p>
---
## ⚙️ Konfiguration (`config.conf`)
```bash
# ==========================================================
# DOCKER COMPOSE UPDATER - CONFIG
# ==========================================================
# ----------------------------------------------------------
# PATH
# ----------------------------------------------------------
# Basisverzeichnis der Stacks [ String ]
PATH_COMPOSE_DIR="/pfad/zu/deinen/stacks"
# Compose-Dateiname [ String ]
PATH_COMPOSE_PATTERN="*compose*.yml"
# ----------------------------------------------------------
# LOG
# ----------------------------------------------------------
# Log-Datei [ String ]
LOG_FILE="/pfad/zum/log/update.log"
# Log-Level [ DEBUG | INFO | WARN | ERROR ]
LOG_LEVEL="INFO"
# ----------------------------------------------------------
# UPDATE
# ----------------------------------------------------------
# Nur Simulation, keine Änderungen [ true | false ]
UPDATE_DRY_RUN_ENABLED=false
# Gestoppte Container updaten [ true | false ]
UPDATE_INCLUDE_STOPPED=true
# Danach wieder starten [ true | false ]
UPDATE_START_STOPPED=false
# ----------------------------------------------------------
# REDEPLOY
# ----------------------------------------------------------
# Feste Wartezeit nach Redeploy [ Number ]
REDEPLOY_WAIT=45
# Warten bis Container healthy sind [ true | false ]
REDEPLOY_WAIT_HEALTHY=true
# Timeout in Sekunden für healthy Check [ Number ]
REDEPLOY_WAIT_HEALTHY_TIMEOUT=60
# ----------------------------------------------------------
# NTFY SETTINGS
# ----------------------------------------------------------
# NTFY Zusammenfassung senden [ true | false ]
NTFY_ENABLED=true
# Server URL [ String ]
NTFY_URL="https://ntfy.example.com/topic"
# Token [ String ]
NTFY_TOKEN="DEIN_TOKEN"
# Titel mitsenden (optional) [ String ]
NTFY_TITLE="Autoupdate Report ($(hostname))"
# Tags mitsenden (optional) [ String ]
NTFY_TAGS="docker,update"
# Icon mitsenden (optional) [ String ]
NTFY_IMAGE_URL="http://dein-server/host-icon.png"
# Nur senden wenn Updates vorhanden [ true | false ]
NTFY_ONLY_ON_CHANGES=false
# Versionsnummern anzeigen [ true | false ]
NTFY_SHOW_VERSIONS=true
# ----------------------------------------------------------
# DOCKER CLEANUP
# ----------------------------------------------------------
# Prune Befehle ausführen [ true | false ]
CLEANUP_ENABLED=true
# Nur nach erfolgten Updates ausführen [ true | false ]
CLEANUP_ONLY_ON_UPDATE=true
# Images löschen [ true | false ]
CLEANUP_IMAGES_ENABLED=true
# Image-Prune Modus [ dangling | unused ]
CLEANUP_IMAGES_MODE="unused"
# Container löschen [ true | false ]
CLEANUP_CONTAINERS_ENABLED=true
# Volumes löschen [ true | false ]
CLEANUP_VOLUMES_ENABLED=false
# Networks löschen [ true | false ]
CLEANUP_NETWORKS_ENABLED=true
```
---
## ▶️ Nutzung
```bash
chmod +x script.sh
./script.sh
```
---
## 📄 Beispiel Ausgabe
```
🔍 Prüfe Stack: rss
├─ read (phpdockerio/readability-js-server) [Mode: 🔄 update]
├─ merc (wangqiru/mercury-parser-api) [Mode: 🔄 update]
├─ full-text-rss (heussd/fivefilters-full-text-rss:latest) [Mode: 🔄 update]
├─ rss-bridge (rssbridge/rss-bridge:latest) [Mode: 🔄 update]
⬆️ UPDATE
alt: rssbridge/rss-bridge:latest@sha256:55215923cf81b2fa6fbb7ecc1bd2555405f4fc06029ae9876e91164a735c7b9d
neu: rssbridge/rss-bridge:latest@sha256:f3f0218c8b075cbc7c559c8e6852888e95fa6d68258436da6195efc5ab98b025
└─ freshrss (freshrss/freshrss:latest) [Mode: 🔄 update]
♻️ Stack wird neu deployt (Trigger: rss-bridge)
⏳ Deploy läuft...
✅ Stack erfolgreich aktualisiert
💤 Warte 60s nach Deploy
⏳ Warte auf healthy Container (max 60s)
💚 Alle Container healthy
🕒 Dauer: 78s
→ Prüfe Stack: tinymediamanager
└─ tinymediamanager [Mode: 🚫 ignore]
🕒 Dauer: 1s
```
---
## ⚠️ Hinweise
Wird in einem Stack ein Container aktualisiert, wird anschließend der gesamte Stack neu gestartet, sofern er mehr als einen Container enthält. Dadurch wird sichergestellt, dass alle Abhängigkeiten wieder gemäß der `docker-compose.yml` ausgeführt werden.
+108
View File
@@ -0,0 +1,108 @@
# ==========================================================
# DOCKER COMPOSE UPDATER - CONFIG
# ==========================================================
# ----------------------------------------------------------
# PATH
# ----------------------------------------------------------
# Basisverzeichnis der Stacks [ String ]
PATH_COMPOSE_DIR="/pfad/zu/deinen/stacks"
# Compose-Dateiname [ String ]
PATH_COMPOSE_PATTERN="*compose*.yml"
# ----------------------------------------------------------
# LOG
# ----------------------------------------------------------
# Log-Datei [ String ]
LOG_FILE="/pfad/zum/log/update.log"
# Log-Level [ DEBUG | INFO | WARN | ERROR ]
LOG_LEVEL="INFO"
# ----------------------------------------------------------
# UPDATE
# ----------------------------------------------------------
# Nur Simulation, keine Änderungen [ true | false ]
UPDATE_DRY_RUN_ENABLED=false
# Gestoppte Container updaten [ true | false ]
UPDATE_INCLUDE_STOPPED=true
# Danach wieder starten [ true | false ]
UPDATE_START_STOPPED=false
# ----------------------------------------------------------
# REDEPLOY
# ----------------------------------------------------------
# Feste Wartezeit nach Redeploy [ Number ]
REDEPLOY_WAIT=120
# Warten bis Container healthy sind [ true | false ]
REDEPLOY_WAIT_HEALTHY=true
# Timeout in Sekunden für healthy Check [ Number ]
REDEPLOY_WAIT_HEALTHY_TIMEOUT=60
# ----------------------------------------------------------
# NTFY SETTINGS
# ----------------------------------------------------------
# NTFY Zusammenfassung senden [ true | false ]
NTFY_ENABLED=true
# Server URL [ String ]
NTFY_URL="https://ntfy.example.com/topic"
# Token [ String ]
NTFY_TOKEN="DEIN_TOKEN"
# Titel mitsenden (optional) [ String ]
NTFY_TITLE="Autoupdate Report ($(hostname))"
# Tags mitsenden (optional) [ String ]
NTFY_TAGS="docker,update"
# Icon mitsenden (optional) [ String ]
NTFY_IMAGE_URL="http://dein-server/host-icon.png"
# Nur senden wenn Updates vorhanden [ true | false ]
NTFY_ONLY_ON_CHANGES=false
# Versionsnummern anzeigen [ true | false ]
NTFY_SHOW_VERSIONS=true
# ----------------------------------------------------------
# DOCKER CLEANUP
# ----------------------------------------------------------
# Prune Befehle ausführen [ true | false ]
CLEANUP_ENABLED=true
# Nur nach erfolgten Updates ausführen [ true | false ]
CLEANUP_ONLY_ON_UPDATE=true
# Images löschen [ true | false ]
CLEANUP_IMAGES_ENABLED=true
# Image-Prune Modus [ dangling | unused ]
CLEANUP_IMAGES_MODE="unused"
# Container löschen [ true | false ]
CLEANUP_CONTAINERS_ENABLED=true
# Volumes löschen [ true | false ]
CLEANUP_VOLUMES_ENABLED=false
# Networks löschen [ true | false ]
CLEANUP_NETWORKS_ENABLED=true
Binary file not shown.

After

Width:  |  Height:  |  Size: 266 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

+597
View File
@@ -0,0 +1,597 @@
#!/bin/bash
set -euo pipefail
BASE_DIR="$(cd "$(dirname "$0")" && pwd)"
source "$BASE_DIR/config.conf"
mkdir -p "$(dirname "$LOG_FILE")"
LOG_LEVEL="${LOG_LEVEL:-INFO}"
level_to_int() {
case "$1" in
DEBUG) echo 0 ;;
INFO) echo 1 ;;
WARN) echo 2 ;;
ERROR) echo 3 ;;
*) echo 1 ;;
esac
}
INDENT=" "
SUBINDENT=" "
should_log() {
[ "$(level_to_int "$1")" -ge "$(level_to_int "$LOG_LEVEL")" ]
}
log() {
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
tail -n 1000 "$LOG_FILE" > "$LOG_FILE.tmp" && mv "$LOG_FILE.tmp" "$LOG_FILE"
fi
fi
# =============================
# Helper
# =============================
get_service_mode() {
local svc="$1"
echo "$compose_json" \
| jq -r --arg svc "$svc" '
.services[$svc].labels // {}
| (if type=="array"
then map(split("=") | {(.[0]): .[1]}) | add
else .
end)
| .["composeupdater.mode"] // "update"
'
}
get_image() {
local svc="$1"
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 -aq "$svc" | head -n1)
[ -z "$cid" ] && echo "" && return
docker inspect -f '{{.Image}}' "$cid" 2>/dev/null || echo ""
}
get_local_image_id() {
local image="$1"
docker image inspect -f '{{.Id}}' "$image" 2>/dev/null || echo ""
}
is_running() {
local svc="$1"
local cid
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
}
run_cmd() {
if [ "$UPDATE_DRY_RUN_ENABLED" = true ]; then
log DEBUG "[DRY RUN] $*"
else
eval "$@"
fi
}
send_ntfy() {
local msg="$1"
local prio="$2"
local image_url="${NTFY_IMAGE_URL:-}"
curl -s \
-H "Authorization: Bearer $NTFY_TOKEN" \
-H "Title: $NTFY_TITLE" \
-H "Priority: $prio" \
-H "Tags: $NTFY_TAGS" \
-H "Markdown: yes" \
${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
total=$(bc <<< "$total + $num")
elif [[ "$size" == *kB ]]; then
total=$(bc <<< "$total + ($num / 1024)")
fi
done < <(docker system df --format '{{.Size}}' 2>/dev/null)
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 -aq "${services[@]}")
else
mapfile -t cids < <(docker compose ps -aq)
fi
# keine Container → nichts zu tun
if [ ${#cids[@]} -eq 0 ]; then
log INFO "${SUBINDENT}ℹ️ 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 "${SUBINDENT}ℹ️ Keine Healthchecks → überspringe warten"
return 0
fi
log INFO "${SUBINDENT}⏳ 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 "${SUBINDENT}💚 Alle Container healthy"
return 0
fi
local now
now=$(date +%s)
if [ $((now - start)) -ge "$timeout" ]; then
log WARN "${SUBINDENT}⚠️ Healthcheck Timeout erreicht"
return 1
fi
sleep 2
done
}
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
}
container_exists() {
local svc="$1"
docker compose ps -aq "$svc" 2>/dev/null | grep -q .
}
# =============================
# Start
# =============================
log INFO "==== Docker Compose Update gestartet ===="
script_start=$(date +%s)
notify_stacks_updated=()
notify_excluded_updates=()
error_flag=false
declare -A stack_tree
stack_tree=()
cd "$PATH_COMPOSE_DIR"
while IFS= read -r -d '' file; do
declare -A pulled_images
dir=$(dirname "$file")
stack=$(basename "$dir")
log INFO ""
log INFO "🔍 Prüfe Stack: $stack"
stack_start=$(date +%s)
cd "$dir"
compose_json=$(docker compose config --format json)
mapfile -t services < <(docker compose config --services)
total_services=${#services[@]}
current_index=0
declare -A was_running
for svc in "${services[@]}"; do
is_running "$svc" && was_running[$svc]=1 || was_running[$svc]=0
done
stack_updated=false
changed_services=()
stack_block=""
update_lines=()
for svc in "${services[@]}"; do
update_needed=false
current_index=$((current_index + 1))
if [ "$current_index" -eq "$total_services" ]; then
prefix="└─"
else
prefix="├─"
fi
image=$(get_image "$svc")
if [ -z "$image" ]; then
log WARN " $prefix $svc → kein Image"
continue
fi
# =============================
# Mode (nur Service!)
# =============================
mode=$(get_service_mode "$svc")
case "$mode" in
update) mode_label="Mode: 🔄 update" ;;
notify-only) mode_label="Mode: 🔔 notify-only" ;;
ignore) mode_label="Mode: 🚫 ignore" ;;
*)
mode_label="Mode: ❓ unknown"
mode="update"
;;
esac
case "$mode" in
ignore)
log INFO "${INDENT}$prefix $svc [$mode_label]"
continue
;;
notify-only)
log INFO "${INDENT}$prefix $svc ($image) [$mode_label]"
before_id=$(get_container_image_id "$svc")
if [ -z "$before_id" ]; then
continue
fi
if [ -z "${pulled_images[$image]:-}" ]; then
pull_with_retry "$image" || true
pulled_images[$image]=1
fi
after_id=$(get_local_image_id "$image")
if [ "$before_id" != "$after_id" ]; then
notify_excluded_updates+=("$stack/$svc")
fi
continue
;;
update)
log INFO "${INDENT}$prefix $svc ($image) [$mode_label]"
;;
esac
before_id=$(get_container_image_id "$svc")
if [ -z "$before_id" ] && [ "$UPDATE_INCLUDE_STOPPED" = false ]; then
log INFO "${SUBINDENT}⏭️ übersprungen (kein Container vorhanden)"
continue
fi
# =============================
# Pull + Vergleich
# =============================
if [ -z "${pulled_images[$image]:-}" ]; then
if ! pull_with_retry "$image"; then
log ERROR "${INDENT}❌ Pull fehlgeschlagen"
error_flag=true
continue
fi
pulled_images[$image]=1
else
log DEBUG "${SUBINDENT}⏩ Pull übersprungen (bereits gemacht)"
fi
after_id=$(get_local_image_id "$image")
if ! container_exists "$svc"; then
update_needed=false
elif [ "$before_id" != "$after_id" ]; then
update_needed=true
else
update_needed=false
fi
# =============================
# Update erkannt
# =============================
if [ "$update_needed" = true ]; then
stack_updated=true
changed_services+=("$svc")
log INFO "${SUBINDENT}⬆️ UPDATE"
log INFO "${SUBINDENT} alt: ${image}@${before_id}"
log INFO "${SUBINDENT} 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}"
update_lines+=("$svc|$short_before|$short_after")
fi
done
# =============================
# Update Logik
# =============================
if [ "$stack_updated" = true ]; then
if [ "$total_services" -eq 1 ]; then
svc="${services[0]}"
log INFO ""
log INFO "${INDENT}🔄 Einzelcontainer-Update: $svc"
if [ "${was_running[$svc]}" = 1 ]; then
run_cmd docker compose up -d "$svc" --remove-orphans >/dev/null 2>&1
else
if [ "$UPDATE_START_STOPPED" = true ]; then
run_cmd docker compose up -d "$svc" --remove-orphans >/dev/null 2>&1
else
run_cmd docker compose create "$svc"
fi
fi
log INFO "${SUBINDENT}✅ Container aktualisiert"
if [ "${REDEPLOY_WAIT:-0}" -gt 0 ]; then
log INFO "${SUBINDENT}💤 Warte ${REDEPLOY_WAIT}s nach Deploy"
sleep "$REDEPLOY_WAIT"
fi
if [ "$REDEPLOY_WAIT_HEALTHY" = true ]; then
wait_for_healthy "$REDEPLOY_WAIT_HEALTHY_TIMEOUT" "$svc"
else
log INFO "${SUBINDENT}ℹ️ Keine Healthchecks → überspringe warten"
fi
else
log INFO ""
log INFO "${INDENT}♻️ Stack wird neu deployt (Trigger: ${changed_services[*]})"
log INFO "${SUBINDENT}⏳ Deploy läuft..."
if ! run_cmd docker compose up -d --remove-orphans >/dev/null 2>&1; then
log ERROR "${SUBINDENT}❌ Stack Update fehlgeschlagen"
error_flag=true
else
log INFO "${SUBINDENT}✅ Stack erfolgreich aktualisiert"
if [ "${REDEPLOY_WAIT:-0}" -gt 0 ]; then
log INFO "${SUBINDENT}💤 Warte ${REDEPLOY_WAIT}s nach Deploy"
sleep "$REDEPLOY_WAIT"
fi
if [ "$REDEPLOY_WAIT_HEALTHY" = true ]; then
wait_for_healthy "$REDEPLOY_WAIT_HEALTHY_TIMEOUT" "${changed_services[@]}"
else
log INFO "${SUBINDENT}ℹ️ Keine Healthchecks → überspringe warten"
fi
for svc in "${services[@]}"; do
if [ "${was_running[$svc]}" = 0 ]; then
log INFO "${SUBINDENT}⏹️ Stoppe $svc (war vorher gestoppt)"
run_cmd docker compose stop "$svc" >/dev/null 2>&1 || true
fi
done
fi
fi
# =============================
# 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 "$PATH_COMPOSE_DIR"
stack_end=$(date +%s)
log INFO "${INDENT}🕒 Dauer: $((stack_end - stack_start))s"
done < <(find . -name "$PATH_COMPOSE_PATTERN" -print0 | sort -z)
# =============================
# Cleanup
# =============================
freed_space="0"
log INFO ""
if [ "$CLEANUP_ENABLED" = true ]; then
if [ "$CLEANUP_ONLY_ON_UPDATE" = true ] && \
[ ${#notify_stacks_updated[@]} -eq 0 ]; then
log INFO "🧹 Cleanup übersprungen (keine Updates)"
else
before_size=$(get_docker_disk_usage)
log INFO ""
log INFO "${INDENT}🧹 Docker Cleanup läuft..."
if [ "$CLEANUP_IMAGES_ENABLED" = true ]; then
case "$CLEANUP_IMAGES_MODE" in
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_ENABLED" = true ]; then
run_cmd docker container prune -f >/dev/null 2>&1
fi
if [ "$CLEANUP_VOLUMES_ENABLED" = true ]; then
run_cmd docker volume prune -f >/dev/null 2>&1
fi
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))
log INFO "${INDENT}✅ Cleanup abgeschlossen (${freed_space} MB freigegeben)"
fi
fi
# =============================
# Notification
# =============================
if [ "$NTFY_ENABLED" = true ]; then
msg=""
if [ ${#stack_tree[@]} -gt 0 ]; then
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
done
fi
if [ ${#notify_excluded_updates[@]} -gt 0 ]; then
msg+=$'\n\n#### ⏭️ Excluded (Update verfügbar)'
for s in "${notify_excluded_updates[@]}"; do
msg+=$'\n - '"$s"
done
fi
if [ ${#stack_tree[@]} -eq 0 ] && \
[ ${#notify_excluded_updates[@]} -eq 0 ]; then
msg+=$'\n\n#### ✔️ Alles aktuell'
fi
if [ "$freed_space" != "0" ]; then
msg+=$'\n\n#### 🧹 Cleanup: '"${freed_space} MB freigegeben"
fi
if [ "$error_flag" = true ]; then
PRIORITY=5
elif [ ${#stack_tree[@]} -gt 0 ]; then
PRIORITY=3
else
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 ===="