commit 663a36b7daaee8970ad9d790a0fb03caff990500 Author: Thorsten Date: Sun Mar 29 12:42:13 2026 +0200 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1750e9b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +config.conf.own diff --git a/README.md b/README.md new file mode 100644 index 0000000..565522f --- /dev/null +++ b/README.md @@ -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 \ No newline at end of file diff --git a/composeupdater.png b/composeupdater.png new file mode 100644 index 0000000..2c70b93 Binary files /dev/null and b/composeupdater.png differ diff --git a/config.conf b/config.conf new file mode 100644 index 0000000..53859c4 --- /dev/null +++ b/config.conf @@ -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 + + + diff --git a/shell_docker_compose_update.sh b/shell_docker_compose_update.sh new file mode 100644 index 0000000..56f6aa8 --- /dev/null +++ b/shell_docker_compose_update.sh @@ -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 ===="