Initial commit
This commit is contained in:
commit
567123a4b1
6 files changed
+1046
No files matched your search
@@ -0,0 +1 @@
|
||||
config_own.conf
|
||||
@@ -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
@@ -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 |
Binary file not shown.
|
After Width: | Height: | Size: 163 KiB |
@@ -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 ===="
|
||||
Reference in new issue
Block a user