Wozu automatische Updates?#

Software altert bzw. die Zeit streicht an ihr vorbei, sodass sie gewartet und auf Stand gehalten werden muss.

“Software ist kein Vermögenswert, sondern eine Verbindlichkeit.” - Cory Doctorow, Jan-2026

In meinem Blog habe ich schon mehrfach von Problemen wie kritischen Schwachstellen in Gitea oder in PHP berichtet, Systemabstürze beseitigt und unabsichtlich veröffentlichten Seiten wieder eingefangen, deren Ursachen allesamt durch Updates oder Rekonfiguration behoben werden konnten.

Das Betriebssystem meines Servers und die wichtigsten darauf laufenden Programme erhalten bereits automatische Updates wie in meinem server-auto-upgrade Artikel beschrieben.

Trotzdem bin ich mit dem Thema noch nicht ganz fertig.

Docker image update mit Watchtower#

Image: Decomissioning watchtower. A construction crane disassembles a watchtower, its lighting tip on the hook.
Um meine Docker-Images kümmerte sich bis Dato das Programm Watchtower. Es gibt jedoch mehrere Gründe gegen seinen Einsatz:

  1. Es wird nicht mehr aktiv gewartet und befindet sich im Archivzustand. Allerdings gibt es Forks, die weitergeführt werden.
  2. Um zu funktionieren, benötigt es Zugriff auf den Docker Daemon per docker.sock. Dies ist gleichbedeutend mit Root-Zugriff des Containers auf mein System. Das will ich in Zukunft unbedingt vermeiden.

1:1-Ersatz für Watchtower benötigt? Linuxhandbook hat hier ein paar Lösungen aufgelistet. Allen gemein ist allerdings, dass sie entweder nur über vorhandene Updates informieren und sie nicht ausrollen, oder dass sie selbst ebenfalls den docker.sock als Volume benötigen.

Lösung per Skript#

Auf der Suche nach einem Weg ohne das Sicherheitsrisiko docker.sock komme ich schnell zur Idee, täglich ein Skript per cron ausführen zu lassen. Dies geschieht mit root-Rechten und kann daher direkt Docker-Befehle ausführen.

Kompliziert: docker pull, Abgleich mit vorhandenen Images, bei Bedarf einspielen#

Mein erstes Skript hat 164loc (Zeilen) und tut folgendes:

  1. Prüfen, ob benötigte Werkzeuge curl, docker, docker compose, docker-comppose.yml installiert sind und funktionieren
  2. Die aktuellen image digests der in der docker-compose.yml definierten Services herausfinden
  3. Die Registry für jeden Service per docker compose pull aktualisieren
  4. Per docker image inspect die “neuen” mit den “alten” image digests vergleichen
  5. Die neuen Images per docker compose up starten, Logs wegspeichern
  6. Bericht per ntfy an mich, falls Updates erfolgt sind.

Doch dieser Weg ist viel zu kompliziert gedacht. Das von mir verwendete Werkzeug docker compose bringt die benötigte Option direkt mit: docker compose up --pull always.

Einfach: Alles in einem Kommando per Option mitgeben#

Der Kern meines neuen Skriptes (61loc) sieht wie folgt aus:

compose=(docker compose --progress plain -f "$COMPOSE_FILE")

before=$("${compose[@]}" ps --all --quiet | sort)

if output=$("${compose[@]}" up -d --pull always --quiet-pull --wait \
    --wait-timeout "$WAIT_TIMEOUT" 2>&1); then
  status=0
else
  status=$?
fi

printf '[%s] docker compose up (exit %s)\n%s\n\n' \
  "$(date -Is)" "$status" "$output" >> "$CHANGELOG"

if (( status != 0 )); then
  report_failure "WARNING: Docker update failed" "warning" "$output"
  exit "$status"
fi

after=$("${compose[@]}" ps --all --quiet | sort)
if [[ "$before" != "$after" ]]; then
  notify "INFO: Docker containers updated" "white_check_mark" "$output"
fi

Im Skript erhebe ich einen Status vor dem Update, indem alle in der Compose-Datei spezifizierten Services aufliste und in einer Variable zwischenparke. Anschließend führe ich wie üblich docker compose up -d durch, allerdings nicht ohne die Flags --pull always sowie --wait wait-timeout <time[s]> zu setzen.

Mit der Pull-Option prüft Docker die Registry auf neue Images, welche gegebenenfalls heruntergeladen und gestartet werden. Der Timeout gibt dem Befehl eine bestimmte Zeit zur Ausführung bevor es mit einer Fehlermeldung abbricht, damit sich das Skript nicht aufhängen kann.

Was folgt ist nur noch Dokumentation und Monitoring: Die Ausgabe des Befehls wird in das Changelog geschrieben, im Falle eines Fehlschlags bekomme ich eine Information per Notify wie in meinem Server-Logging Post beschrieben. Hier wird der before-Status wichtig: Ich eine Benachrichtigung nur bei Fehler oder Update.

Anschließend muss ich das Skript nur noch ausführbar machen chmod + x docker-image-update.sh und an root übergeben: chown root root docker-image-update.sh.

Eintrag für cron#

Nach einem manuellen Test des Skripts ./docker-image-update.sh, Prüfung seiner Ausgabe, des Changelogs und den neuen Containerversionen, lasse ich das Skript täglich durch den Cron-Daemon aufrufen:

schallbert@machine: crontab -e
[...]
0 2 * * * /path/to/docker-image-update.sh

Welche Version wurde installiert?#

Statt mit Versionsbezeichnungen der Services arbeitet Docker mit Image digests. Dieser sha256-hash ist für jedes Image einzigartig, aber nicht besonders leserfreundlich:

sha256:94a00394bc5a8ef503fb59db0a7d0ae9e1110866e8aee8ba40cd864cea69ea1a # example Image digest

Image: Screenshot of my phone's Notify app message entry, showing an image update.
Doch Service-Versionsbezeichnungen wie Gitea v28.2.1 werden in den Image-Metadaten nicht standardmäßig unterstützt. Daher habe ich erstmal noch keine Lösung für die Ausgabe einer Version. Die Digests lasse ich in der Ausgabe weg.

Meldung bei Update#

Die Meldung ist verbesserungsfähig: Ich benötige eigentlich nur die Namen der Container Updates und die Bestätigung, dass alle Images danach wieder “healthy” sind. Zusätzlich erhalte ich aber die Zustände der Container, aus denen ich die benötigten Informationen erst extrahieren muss. Der Mehrwert einer Filterung erscheint mir aber derzeit zu gering, als dass ich meine Zeit in die Erweiterung des Skriptes stecken würde. Schließlich zeigt mir Recreate bereits an, welcher Container auf Stand gebracht wurde.

Ich bin zufrieden. Ich konnte vom Host aus mit wenigen Zeilen Code, aus Bordmitteln, und nur mit cron als zusätzlicher Abhängigkeit mein Updateproblem für die per Docker Compose verwalteten Container lösen.