.
This commit is contained in:
168
README.md
168
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 <none> 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
|
||||
|
||||
---
|
||||
|
||||
|
||||
126
config.conf
126
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 <none> 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 ]
|
||||
@@ -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" \
|
||||
${image_url:+-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
|
||||
}
|
||||
|
||||
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"
|
||||
# 👉 Skip wenn kein Container und nicht erlaubt
|
||||
if [ -z "$before_id" ] && [ "$UPDATE_INCLUDE_STOPPED" = false ]; then
|
||||
log INFO " ⏭️ übersprungen (kein Container vorhanden)"
|
||||
continue
|
||||
fi
|
||||
|
||||
# =============================
|
||||
# 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")
|
||||
after_digest=$(get_local_image_digest "$image")
|
||||
|
||||
if [ -n "$before_id" ] && [ "$before_id" != "$after_id" ]; then
|
||||
# 👉 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
|
||||
|
||||
stack_updated=true
|
||||
changed_services+=("$svc")
|
||||
|
||||
if [ "$SHOW_VERSIONS" = true ]; then
|
||||
log INFO " ⬆️ UPDATE"
|
||||
|
||||
# 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}"
|
||||
|
||||
# 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
|
||||
run_cmd docker compose up -d "$svc" --remove-orphans
|
||||
else
|
||||
log INFO " ✔️ Container $svc aktualisiert"
|
||||
fi
|
||||
if [ "$UPDATE_START_STOPPED" = true ]; then
|
||||
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
|
||||
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"
|
||||
fi
|
||||
|
||||
cd "$COMPOSE_DIR"
|
||||
|
||||
done < <(find . -name "$COMPOSE_PATTERN" -print0 | sort -z)
|
||||
|
||||
|
||||
# =============================
|
||||
# Cleanup (mit Statistik)
|
||||
# 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 " ⏱ Dauer: $((stack_end - stack_start))s"
|
||||
|
||||
done < <(find . -name "$PATH_COMPOSE_PATTERN" -print0 | sort -z)
|
||||
|
||||
# =============================
|
||||
# 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,22 +532,17 @@ 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"
|
||||
done
|
||||
if [ ${#stack_tree[@]} -gt 0 ]; then
|
||||
msg+=$'\n🔄 Stack Updates\n'
|
||||
|
||||
# 👉 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"
|
||||
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
|
||||
fi
|
||||
|
||||
if [ ${#notify_excluded_updates[@]} -gt 0 ]; then
|
||||
msg+=$'\n\n⏭️ Excluded (Update verfügbar)'
|
||||
@@ -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
|
||||
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)"
|
||||
else
|
||||
log INFO "keine Änderungen, keine ntfy Nachricht"
|
||||
fi
|
||||
fi
|
||||
|
||||
script_end=$(date +%s)
|
||||
log INFO "⏱ Gesamtzeit: $((script_end - script_start))s"
|
||||
log INFO "==== Update beendet ===="
|
||||
Reference in New Issue
Block a user