first commit

This commit is contained in:
2026-03-29 12:42:13 +02:00
commit 663a36b7da
5 changed files with 474 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
config.conf.own

128
README.md Normal file
View File

@@ -0,0 +1,128 @@
# Docker Compose Auto-Updater
Dieses Script überprüft mehrere Docker-Compose-Stacks auf Image-Updates und aktualisiert diese automatisch.
## 🚀 Features
- 🔄 Stack-basiertes Update
- 🧪 Dry-Run Modus
- 📲 ntfy Benachrichtigungen
- ⏭️ Exclude-Liste für Services
---
## 📂 Voraussetzungen
- Docker + Docker Compose (v2)
- Bash
- Optional: ntfy Server
---
## ⚙️ Konfiguration (`config.conf`)
```bash
# Pfad zu deinen Compose-Files
COMPOSE_DIR="/pfad/zu/deinen/stacks"
# Dateimuster
COMPOSE_PATTERN="docker-compose.yml"
# Exclude Container
EXCLUDE_SERVICES=(
"example_container_1"
"example_container_2"
)
# Exclude Stack
EXCLUDE_STACKS=(
"example_stack_1"
"example_stack_2"
)
# Verhalten bei gestoppten Containern
UPDATE_STOPPED=true # Image aktualisieren
START_STOPPED=false # danach NICHT starten
# Dry Run (true/false)
DRY_RUN=false
# Logging
LOG_FILE="/pfad/zum/log/update.log"
LOG_LEVEL="INFO"
# ntfy
NTFY_ENABLED=true
NTFY_TITLE="Docker Update ($(hostname))"
NTFY_TOKEN="DEIN_TOKEN"
NTFY_URL="https://ntfy.example.com/topic"
NTFY_TAGS="docker,update"
NTFY_ONLY_ON_CHANGES=false
# Versions Nr. der Container in der NTFY Nachricht anzeigen (true/false)
SHOW_VERSIONS=true
```
---
## ▶️ Nutzung
```bash
chmod +x script.sh
./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
---
## 🔔 ntfy Prioritäten
| Zustand | Priorität |
|----------------------|----------|
| ✔️ Keine Updates | 1 |
| 🔄 Updates vorhanden | 3 |
| ❌ Fehler | 5 |
---
## 📄 Beispiel Ausgabe
```
→ Prüfe Stack: homepage
├─ dockerproxy (image)
└─ homepage (image)
→ Prüfe Stack: app
├─ db (image)
⬆️ UPDATE
alt: sha256:abc
neu: sha256:def
└─ web (image)
🔄 Stack wird neu deployt
```
---
## ⚠️ 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.
---
## 🧪 Dry Run
```bash
DRY_RUN=true
```
→ zeigt nur, was passieren würde

BIN
composeupdater.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 KiB

41
config.conf Normal file
View File

@@ -0,0 +1,41 @@
# Pfad zu deinen Compose-Files
COMPOSE_DIR="/pfad/zu/deinen/stacks"
# Dateimuster
COMPOSE_PATTERN="docker-compose.yml"
# Exclude Container
EXCLUDE_SERVICES=(
"example_container_1"
"example_container_2"
)
# Exclude Stack
EXCLUDE_STACKS=(
"example_stack_1"
"example_stack_2"
)
# Verhalten bei gestoppten Containern
UPDATE_STOPPED=true # Image aktualisieren
START_STOPPED=false # danach NICHT starten
# Dry Run (true/false)
DRY_RUN=false
# Logging
LOG_FILE="/pfad/zum/log/update.log"
LOG_LEVEL="INFO" # DEBUG=sehr detailliert, INFO=Standard, WARN=nur wichtige Hinweise/Updates, ERROR=nur Fehler
# ntfy
NTFY_ENABLED=true
NTFY_TITLE="Docker Update ($(hostname))" # ntfy Titel (frei definierbar)
NTFY_TOKEN="DEIN_TOKEN"
NTFY_URL="https://ntfy.example.com/topic"
NTFY_TAGS="docker,update"
NTFY_ONLY_ON_CHANGES=false
# Versions Nr. anzeigen (true/false)
SHOW_VERSIONS=true

View File

@@ -0,0 +1,304 @@
#!/bin/bash
set -euo pipefail
BASE_DIR="$(dirname "$0")"
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
}
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
# =============================
is_excluded() {
local svc="$1"
for ex in "${EXCLUDE_SERVICES[@]}"; do
[[ "$svc" == "$ex" ]] && return 0
done
return 1
}
is_stack_excluded() {
local stack="$1"
for ex in "${EXCLUDE_STACKS[@]}"; do
[[ "$stack" == "$ex" ]] && return 0
done
return 1
}
get_image() {
local svc="$1"
docker compose config | awk "/^ $svc:/,/image:/" | grep image | head -n1 | awk '{print $2}'
}
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 ""
}
get_local_image_id() {
local image="$1"
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"
}
run_cmd() {
if [ "$DRY_RUN" = true ]; then
log DEBUG "[DRY RUN] $*"
else
eval "$@"
fi
}
send_ntfy() {
local msg="$1"
local prio="$2"
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
}
# =============================
# Start
# =============================
log INFO "==== Docker Compose Update gestartet ===="
notify_stacks_updated=()
notify_excluded_updates=()
error_flag=false
cd "$COMPOSE_DIR"
# =============================
# Sortierung
# =============================
while IFS= read -r -d '' file; do
dir=$(dirname "$file")
stack=$(basename "$dir")
if is_stack_excluded "$stack"; then
log INFO "→ Stack $stack übersprungen (excluded)"
continue
fi
log INFO ""
log INFO "→ Prüfe Stack: $stack"
cd "$dir"
mapfile -t services < <(docker compose config --services)
total_services=${#services[@]}
current_index=0
stack_updated=false
changed_services=()
version_report=()
for svc in "${services[@]}"; do
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
if is_excluded "$svc"; then
docker pull "$image" >/dev/null 2>&1 || true
notify_excluded_updates+=("$stack/$svc")
log INFO " $prefix $svc (excluded)"
continue
fi
log INFO " $prefix $svc ($image)"
before_id=$(get_container_image_id "$svc")
before_ref=$(get_container_image_ref "$svc")
if ! docker pull "$image" >/dev/null 2>&1; then
log ERROR " $prefix ❌ Pull fehlgeschlagen"
error_flag=true
continue
fi
after_id=$(get_local_image_id "$image")
after_digest=$(get_local_image_digest "$image")
if [ "$before_id" != "$after_id" ] && [ -n "$after_id" ]; then
stack_updated=true
changed_services+=("$svc")
if [ "$SHOW_VERSIONS" = true ]; then
log WARN " ⬆️ UPDATE"
log INFO " alt: $before_ref"
log INFO " neu: $after_digest"
version_report+=("$svc: ${before_ref##*@}${after_digest##*@}")
else
log WARN " ⬆️ UPDATE"
fi
fi
done
# =============================
# Update Logik
# =============================
if [ "$stack_updated" = true ]; then
if [ "$total_services" -eq 1 ]; then
svc="${services[0]}"
log WARN " 🔄 Einzelcontainer-Update: $svc"
if ! run_cmd docker compose up -d "$svc" >/dev/null 2>&1; then
log ERROR " ❌ Update fehlgeschlagen"
error_flag=true
else
log INFO " ✔️ Container erfolgreich aktualisiert"
notify_stacks_updated+=("$stack ($svc)")
fi
else
log WARN " 🔄 Stack wird neu deployt (Trigger: ${changed_services[*]})"
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"
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)
# =============================
# Notification
# =============================
if [ "$NTFY_ENABLED" = true ]; then
msg="Docker Compose Update Report"
if [ ${#notify_stacks_updated[@]} -gt 0 ]; then
msg+=$'\n\n🔄 Aktualisierte Stacks'
for s in "${notify_stacks_updated[@]}"; do
msg+=$'\n - '"$s"
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
# Priority fix
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 ] && \
[ ${#notify_excluded_updates[@]} -eq 0 ]; then
msg+=$'\n\n✔ Alles aktuell'
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)"
else
log INFO "keine Änderungen, keine ntfy Nachricht"
fi
fi
log INFO "==== Update beendet ===="