Den ganzen Docker-Stack sicher aktuell halten
Update-Disziplin für deinen Docker-Stack: Versionen pinnen, mit Diun über Updates informiert werden, sicher einspielen, aufräumen – statt blindem Auto-Update.
Inhaltsverzeichnis
Ein selbstgehosteter Stack ist nie „fertig". Container-Images bekommen Sicherheitsupdates, Apps neue Funktionen – und wer nicht nachzieht, läuft irgendwann verwundbare Software. Dieses Rezept macht aus „ich sollte mal updaten" eine verlässliche Routine: informiert, kontrolliert und mit Netz.
Was bauen wir?
Kein Werkzeug, sondern eine Update-Disziplin für deinen kompletten Docker-Stack. Am Ende
hast du: gepinnte Versionen (nie wieder latest), einen Update-Melder (Diun), der dir
sagt, wann etwas Neues da ist, eine sichere Einspiel-Routine mit Backup davor und
Rollback-Plan – und einen Aufräum-Rhythmus, damit dir alte Images nicht die Platte vollmüllen.
Getestet mit Diun v4.33.0 unter Docker 29 / Compose v5.3.
Die Leitidee ist bewusst nicht „alles automatisch aktualisieren". Bei zustandsbehafteten Apps (Datenbanken, Nextcloud, Immich) kann ein unbeaufsichtigtes Update mitten in der Nacht eine Migration auslösen, die schiefgeht – und niemand ist da. Der sichere Weg ist: pinnen → benachrichtigen lassen → bewusst einspielen → prüfen. Blindes Auto-Update behandeln wir am Ende ehrlich (Schritt 6): Es hat seinen Platz, aber einen kleinen.
Voraussetzungen
- Grundwissen zu Docker Compose – Tags, Volumes und
compose.yamlsolltest du lesen können. - Ein laufender Stack, den du pflegen willst (z. B. die Apps hinter deinem Reverse Proxy).
- Eine funktionierende Backup-Strategie mit Restic – sie ist das Sicherheitsnetz, das Updates erst entspannt macht.
VPS 2000 G12
8 vCore · 16 GB RAM · 512 GB NVMe
ab 19,24 €/Monat
Ein gepflegter Multi-App-Stack samt Datenbanken fühlt sich auf dem VPS 2000 spürbar wohler.
💶 5 € Gutschein für netcup-Neukunden:36nc17844976032
(nur Neukunden, keine Domains)
Schritt für Schritt
Schritt 1: Nie wieder latest – Versionen bewusst pinnen
Der häufigste Fehler steht in unzähligen Anleitungen: image: nextcloud:latest. Das Problem:
latest ist ein bewegliches Ziel. Ein docker compose pull kann dir jederzeit ungefragt eine
neue Hauptversion samt Breaking Changes ziehen – du weißt nie, was du gerade fährst, und ein
Rollback ist kaum möglich.
Pinne stattdessen jeden Dienst auf einen festen Tag. Zwei sinnvolle Stufen:
- Major-Tag (
postgres:18,nextcloud:34-apache): bekommt automatisch Patch- und Minor-Updates derselben Hauptversion beim nächstenpull, aber nie einen Major-Sprung. Guter Kompromiss für die meisten Dienste. - Exakte Version (
vaultwarden/server:1.37.1): volle Kontrolle, nichts ändert sich ohne dein Zutun. Ideal für heikle, zustandsbehaftete Apps.
Was du gerade fährst, zeigt dir Compose pro Projekt:
cd ~/DEINE_APP && docker compose imagesCONTAINER REPOSITORY TAG PLATFORM IMAGE ID SIZE CREATED
diun-demo-whoami traefik/whoami v1.11.0 linux/amd64 200689790a0a 3.04MB 17 months agoSteht in der TAG-Spalte irgendwo latest, ist das dein erster Kandidat zum Pinnen. Trag den
konkreten Tag in die compose.yaml ein – das ist die Grundlage für alles Weitere.
Welche Version aktuell stabil ist, schlägst du an der Quelle nach, nicht im Bauchgefühl: die Tags-Übersicht auf Docker Hub (bzw. GHCR) des Images oder die Release-Notes auf GitHub. Für offizielle Images geht das auch per API:
curl -s "https://hub.docker.com/v2/repositories/library/postgres/tags/?page_size=20" | grep -oE '"name":"[0-9.]+"'Nimm die höchste stabile Version derselben Reihe, die du fahren willst – Vorabversionen
(rc, beta) bleiben außen vor.
Schritt 2: Sehen, was veraltet ist – der Update-Melder Diun
Pinnen heißt: Updates kommen nicht mehr von allein. Also brauchst du jemanden, der dir Bescheid gibt, wenn ein neues Image da ist. Genau das macht Diun (Docker Image Update Notifier) – es aktualisiert nichts, es beobachtet nur und benachrichtigt. Leg ihm einen eigenen Ordner an:
mkdir -p ~/diun && cd ~/diunDie compose.yaml:
services:
diun:
image: crazymax/diun:4.33.0
command: serve
environment:
- "TZ=Europe/Berlin"
- "DIUN_WATCH_SCHEDULE=0 8 * * *" # täglich 8 Uhr
- "DIUN_PROVIDERS_DOCKER=true"
- "DIUN_PROVIDERS_DOCKER_WATCHBYDEFAULT=false"
volumes:
- ./data:/data
- /var/run/docker.sock:/var/run/docker.sock:ro
restart: unless-stoppedDIUN_PROVIDERS_DOCKER=truelässt Diun deine laufenden Container über den Docker-Socket entdecken.WATCHBYDEFAULT=falseheißt: Diun beobachtet nur Container, die du ausdrücklich markierst – so bekommst du keine Flut zu Dingen, die dich nicht interessieren.DIUN_WATCH_SCHEDULEist ein Cron-Ausdruck; einmal täglich reicht völlig und schont die Registry-Rate-Limits.
Der Docker-Socket ist kein harmloser Lese-Zugriff
:ro, und das klingt beruhigend – ist es aber nur halb. Das :ro
verhindert bloß, dass die Datei docker.sock überschrieben wird; über die Docker-API dahinter
lässt sich trotzdem alles anstellen: Container mit --privileged starten, das Host-Dateisystem
einhängen, kurz Root auf dem Host. Wer Zugriff auf den Socket hat, ist praktisch root-äquivalent
– unabhängig vom :ro. Für einen reinen Melder wie Diun ist das ein bewusst eingegangenes Risiko;
wer es entschärfen will, hängt Diun nicht direkt an den Socket, sondern an einen vorgeschalteten
docker-socket-proxy, der nur die wenigen
Endpunkte (Container auflisten, Images lesen) durchreicht, die Diun wirklich braucht.Markiere die Container, die Diun im Auge behalten soll, per Label in deren compose.yaml:
labels:
- "diun.enable=true"
- "diun.watch_repo=true"diun.enable=true schaltet die Überwachung ein. diun.watch_repo=true lässt Diun das ganze
Repository nach neueren Tags absuchen (z. B. ob zu deinem postgres:18 schon ein 19
existiert) – ohne dieses Label prüft Diun nur, ob dein exakter Tag ein neues Image bekommen
hat. Starte Diun und sieh beim ersten Lauf zu:
docker compose up -d && docker compose logs -f diunINF Found 1 image(s) to analyze provider=docker
INF New image found image=docker.io/traefik/whoami:v1.11.0 provider=docker
INF New image found image=docker.io/traefik/whoami:v1.12.0 provider=docker
INF New image found image=docker.io/traefik/whoami:latest-arm64 provider=docker
INF Jobs completed added=97 failed=0 skipped=0 unchanged=0 updated=0Der erste Lauf ist nur die Bestandsaufnahme: Diun schreibt jeden gefundenen Tag als „New
image found" in seine Datenbank (added). Dass für einen überwachten Container 97 Einträge
herauskommen, liegt an watch_repo – Diun holt dann das Manifest für jeden Tag des
Repositories, also auch für Architektur-Varianten wie latest-arm64 oder v1.10.0-armv7.
Zwei Dinge folgen daraus, die man leicht falsch erwartet:
Der erste Lauf benachrichtigt nicht. Die Option
watch.firstCheckNotifsteht per Default auffalse, und das gilt pro Image-Tag. Alles, was Diun zum ersten Mal sieht, landet stillschweigend in der Datenbank. Gemeldet wird erst, wenn sich später etwas ändert.watch_repokostet Registry-Anfragen. 97 Manifest-Abfragen pro Lauf reißen das Docker-Hub-Limit ohne Login (100 Anfragen pro IPv4-Adresse bzw. IPv6-/64-Netz) fast alleine. Beim zweiten Lauf desselben Tages lief unser Test genau dort hinein –StatusCode: 429undfailed=36im Log. Wie viel Budget noch übrig ist – und über welches Zeitfenster die Registry gerade rechnet –, sagt sie dir selbst:TerminalTOKEN=$(curl -s "https://auth.docker.io/token?service=registry.docker.io&scope=repository:ratelimitpreview/test:pull" | cut -d'"' -f4) curl -sI -H "Authorization: Bearer $TOKEN" https://registry-1.docker.io/v2/ratelimitpreview/test/manifests/latest | grep -i "^ratelimit"Ausgaberatelimit-limit: 100;w=3600 ratelimit-remaining: 98;w=3600wist die Fensterbreite in Sekunden. Docker dokumentiert ein Sechs-Stunden-Fenster; unser Test-VPS bekam im August 2026 durchwegw=3600, also eine Stunde. Verlass dich deshalb auf den Header statt auf eine gemerkte Zahl – und beachte, dass diese Abfrage selbst eine Anfrage kostet. Grenz die Tags außerdem auf das ein, was dich wirklich interessiert – echte Release-Tags:
labels:
- "diun.enable=true"
- "diun.watch_repo=true"
- "diun.include_tags=^v1\\.1[12]\\.\\d+$$"
- "diun.sort_tags=semver"include_tags ist ein regulärer Ausdruck. Zwei Fallstricke stecken in der Schreibweise: Die
Backslashes müssen im YAML doppelt stehen, und das $ am Ende wird als $$ geschrieben –
sonst versucht Compose, eine Umgebungsvariable einzusetzen. Mit diesem Label bleiben von den 97
Einträgen genau die beiden echten Release-Tags übrig:
INF Found 1 image(s) to analyze provider=docker
INF New image found image=docker.io/traefik/whoami:v1.11.0 provider=docker
INF New image found image=docker.io/traefik/whoami:v1.12.0 provider=docker
INF Jobs completed added=2 failed=0 skipped=0 unchanged=0 updated=0Damit die Meldung nicht nur im Log steht, hängst du eine Benachrichtigung dran. Diun kann
E-Mail, Telegram, ntfy, Gotify und viele weitere. Für Telegram – den Bot hast du vielleicht
schon im Uptime-Kuma-Tutorial eingerichtet – reichen zwei
Zeilen im environment-Block:
- "DIUN_NOTIF_TELEGRAM_TOKEN=DEIN_BOT_TOKEN"
- "DIUN_NOTIF_TELEGRAM_CHATIDS=DEINE_CHAT_ID"Alle weiteren Kanäle stehen in der Diun-Dokumentation. So sieht die Meldung dann aus – hier über ntfy im Browser, sobald zu einem deiner gepinnten Images eine neuere Version erscheint:

Die Benachrichtigung testen, ohne wochenlang zu warten
DIUN_WATCH_FIRSTCHECKNOTIF=true in Diuns
environment, lösche ./data und starte neu: Dann schickt Diun auch für die Bestandsaufnahme
Benachrichtigungen – genau eine pro gefundenem Tag. Kanal geprüft, Variable danach wieder
entfernen (sonst rauscht dir der nächste watch_repo-Lauf den Posteingang voll).Schritt 3: Ein Update sicher einspielen (die Routine)
Diun meldet ein Update – jetzt kommt die eigentliche Disziplin. Nie einfach blind ziehen. Die feste Reihenfolge für jeden Dienst:
1. Release-Notes lesen. Bei einem Major-Sprung (z. B. Nextcloud 34 → 35) ist das Pflicht: Gibt es Breaking Changes, Migrationsschritte, entfernte Optionen? Zwei Minuten hier sparen Stunden Fehlersuche.
2. Vorher sichern. Ein Update ist der klassische Moment, an dem etwas kaputtgeht. Mach ein Restic-Backup der Daten (bei großen Sprüngen zusätzlich einen netcup-Snapshot). Dann ist ein Fehlschlag nur ein Rücksetzer, kein Datenverlust.
3. Tag anheben und ziehen. Setz den neuen Tag in der compose.yaml und hol das Image:
docker compose pull Image traefik/whoami:v1.12.0 Pulled4. Neu starten. Compose ersetzt nur den betroffenen Container, die Volumes bleiben:
docker compose up -d Container diun-demo-whoami Recreated
Container diun-demo-whoami Started5. Prüfen. Läuft der Container sauber (docker compose ps, docker compose logs), und tut
die App noch, was sie soll? Zustandsbehaftete Apps führen jetzt evtl. eine Datenbank-Migration
aus – wirf einen Blick ins Log, bis sie durch ist.
6. Rollback im Kopf haben. Geht etwas schief, setzt du den Tag in der compose.yaml zurück
auf die alte Version und machst erneut docker compose up -d. Weil deine Daten im Volume
liegen (nicht im Container), ist das bei den meisten Apps ein sauberer Rücksprung. Nur wenn die
neue Version die Datenbank schon migriert hat, hilft kein Tag-Rollback mehr – dann brauchst
du das Backup aus Schritt 2. Genau dafür ist es da.
Eine App nach der anderen
Schritt 4: Die Basis nicht vergessen – Host, Engine, versteckte Images
Deine Apps sind nur die halbe Miete. Genauso wichtig:
Betriebssystem & Docker-Engine. Die OS- und Sicherheitspakete von Debian spielst du am besten automatisch mit unattended-upgrades ein. Die Docker-Engine bleibt dabei bewusst außen vor: unattended-upgrades zieht per Default nur die Debian-Quellen (
origin=Debian), nicht das Docker-APT-Repo (origin=Docker,download.docker.com). Die Engine wird also nicht im Hintergrund aktualisiert und startet dir nachts nicht unerwartet alle Container neu – denn ein Engine-Update reißt laufende Container kurz mit. Diesen Moment bestimmst du besser selbst und aktualisierst die Engine gezielt von Hand, wenn es passt:Terminalsudo apt update && sudo apt upgrade docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-pluginPrüfe danach mit
docker compose ps, dass alle Stacks wieder oben sind.Ist dir ein kurzer Container-Neustart in der Nacht egal, kannst du die Engine auch mit unattended-upgrades mitziehen lassen: Erlaube in
/etc/apt/apt.conf.d/50unattended-upgradesdas Docker-Repo als zusätzliches Origins-Pattern. Wichtig istarchive=– das Docker-Repo setzt keinCodename-Feld, das oft empfohlenecodename=${distro_codename}würde ins Leere laufen und Docker bliebe trotz Eintrag außen vor:Ausgabe"origin=Docker,archive=${distro_codename}";Versteckte Images. In vielen Stacks stecken Datenbanken, Caches und Hilfsdienste (
postgres,redis,mariadb,gotenberg…), an die niemand denkt. Diun überwacht sie automatisch mit, sobald der jeweilige Container dasdiun.enable-Label trägt – gib es also allen langlebigen Diensten, nicht nur der sichtbaren Haupt-App.Erweiterungen deines Reverse Proxys. Auch gepinnte Plugin-Versionen (etwa der CrowdSec-Bouncer) oder Traefik selbst wollen bewusst nachgezogen werden.
Schritt 5: Aufräumen – Speicher zurückholen
Jedes Update lässt das alte Image liegen. Nach ein paar Monaten summiert sich das. Was du verbrauchst, zeigt:
docker system dfTYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 26 4 14.75GB 14.16GB (95%)
Containers 4 4 45.06kB 0B (0%)
Local Volumes 0 0 0B 0B
Build Cache 36 0 828.2MB 828.2MBUngenutzte, unbenannte („dangling") Images entfernt der sichere Standardbefehl:
docker image pruneAchtung: Das entfernt nur dangling Images. Eine alte, noch getaggte Version (wie
whoami:v1.11.0 nach dem Update auf v1.12.0) gilt nicht als dangling und bleibt liegen. Die
holst du dir gezielt zurück:
docker rmi traefik/whoami:v1.11.0Untagged: traefik/whoami:v1.11.0
Deleted: sha256:200689790a0a0ea48ca45992e0450bc26ccab5307375b41c84dfc4f2475937abWer radikaler aufräumen will, nimmt docker image prune -a (entfernt alle Images, die
gerade kein laufender Container nutzt). Das ist mächtig, zieht aber beim nächsten Start alles
neu – setz es bewusst ein, nicht im Cron.
Das sichere docker image prune -f (nur dangling) kannst du dagegen bedenkenlos regelmäßig
laufen lassen – etwa wöchentlich per systemd-Timer oder Cron. So bleibt zwischen deinen
Update-Terminals gar nicht erst Müll liegen, und du gewinnst den Speicher automatisch zurück.
Schritt 6: Auto-Update – wann es okay ist (und wann nicht)
Bleibt die Frage: Warum nicht alles vollautomatisch? Das bekannteste Werkzeug dafür ist Watchtower – das zieht neue Images und startet Container von selbst neu. Zwei Haken:
- Der Upstream ist verwaist. Das offizielle
containrrr/watchtowerhat seit v1.7.1 (11.11.2023) kein Release mehr bekommen – und das Repository wurde Ende 2025 (17.12.2025) archiviert, also endgültig eingemottet. Wer es nutzt, greift besser zu einem gepflegten Fork wienickfedor/watchtower. - Auto-Update ist für zustandsbehaftete Apps gefährlich. Eine Datenbank oder Nextcloud nachts unbeaufsichtigt über einen Major-Sprung zu heben, ist das Gegenteil von „sicher".
Sinnvoll ist Auto-Update höchstens für unkritische, zustandslose Dienste – und selbst dann
lieber im Monitor-Modus (WATCHTOWER_MONITOR_ONLY=true), der nur meldet statt aktualisiert.
Für alles andere schlägt die bewusste Routine aus Schritt 3 jedes Automatik-Werkzeug. Genau
deshalb ist Diun (melden) + manuelles Update hier die Empfehlung, nicht Watchtower
(handeln).
Sichere Automatisierung für Git-Nutzer: Renovate
compose.yaml-Dateien in einem Git-Repo liegen, gibt es einen dritten Weg, der
Automatik und Kontrolle verbindet: Renovate (oder Dependabot) erkennt die gepinnten
image:-Tags und öffnet automatisch einen Pull Request, der den Tag anhebt – mitsamt Link
zu den Release-Notes. Das Update passiert also nicht heimlich am Server, sondern als Änderung,
die du prüfst und mergst, bevor du sie mit docker compose up -d ausrollst. So bleibt der
sichere „bewusst einspielen"-Schritt erhalten, nur das lästige Nachschauen entfällt.Wenn es nicht funktioniert
docker compose pull zieht kein neues Image, obwohl es eine neue Version gibt. Dein Tag zeigt
auf eine feste Version (:1.37.1) oder auf einen Major-Tag (:18), unter dem es nur ein neues
Major gäbe. pull holt nur, was derselbe Tag inzwischen zeigt. Für einen Versionssprung
musst du den Tag in der compose.yaml selbst anheben.
Die App startet nach dem Update nicht mehr oder wirft Datenbank-Fehler. Meist ein Breaking
Change oder eine fehlgeschlagene Migration. Prüfe docker compose logs, gleiche mit den
Release-Notes ab. Tag auf die alte Version zurücksetzen und up -d; hat die neue Version die Daten
schon migriert, das Backup aus Schritt 3 einspielen.
Diun meldet nichts, obwohl Updates existieren. Prüfe, dass die Container das Label
diun.enable=true tragen und Diun den Socket lesen kann (DIUN_PROVIDERS_DOCKER=true, Socket
gemountet). Für neuere Versions-Tags braucht der Container zusätzlich diun.watch_repo=true –
ohne das sieht Diun nur Änderungen am exakt gepinnten Tag. Und: Was Diun beim ersten Mal
sieht, wird nur in die Datenbank geschrieben, nicht gemeldet (watch.firstCheckNotif ist
false) – zum Testen des Meldewegs einmalig DIUN_WATCH_FIRSTCHECKNOTIF=true setzen.
Die Festplatte läuft voll, obwohl du regelmäßig docker image prune machst. docker image prune räumt nur dangling Images ab, nicht die alten getaggten Versionen. Entferne sie gezielt
mit docker rmi <image>:<tag> oder – mit Bedacht – docker image prune -a. Auch der Build-Cache
(docker builder prune) kann wachsen.
Beim Ziehen kommt toomanyrequests / ein Docker-Hub-Rate-Limit. diun.watch_repo=true auf
vielen Images fragt viele Tags ab. Setz das Watch-Intervall seltener (z. B. einmal täglich) und
grenze mit diun.include_tags auf relevante Versionen ein, statt das ganze Repo abzuklopfen.
Wartung & Backups
- Der Rhythmus. Diun meldet → du liest die Release-Notes → Backup → Update → prüfen. In der Praxis ist das einmal im Monat ein überschaubarer Termin; kritische Sicherheitslücken ziehst du sofort nach. Ehrlich: Ganz ohne Handarbeit geht sicheres Selfhosting nicht – aber 15 Minuten im Monat sind der Deal.
- Schnelldreher zuerst. Manche Projekte veröffentlichen häufig und mit Breaking Changes – Immich, mailcow, CrowdSec, Grafana und Nextcloud. Die schaust du dir zuerst und öfter an, ruhige Kandidaten (Datenbanken, Redis) seltener.
- Backups sind der Kern dieses Rezepts. Ein Update ohne Backup ist ein Vabanquespiel; mit Restic im Rücken wird jedes Update entspannt. Vor großen Sprüngen zusätzlich einen Snapshot – der holt den ganzen Server in einem Rutsch zurück.
- Versioniere deine
compose.yaml-Dateien. Wer seine Compose-Dateien in einem Git-Repo hält, kann nicht nur Daten, sondern auch die Konfiguration auf jeden früheren Stand zurückrollen – und sieht in der Historie genau, welcher Tag wann angehoben wurde. - Halte auch die Werkzeuge aktuell. Diun selbst und deine Reverse-Proxy-Plugins gehören mit in die Update-Runde – sonst pflegt sich der Pfleger nicht.
Zuletzt aktualisiert: 11.08.2026
Feedback per E-Mail: feedback@serverkueche.de
Das könnte dir auch schmecken

Docker-Netzwerke verstehen: bridge, internes DNS & das proxy-Netz
Warum deklariert jedes App-Rezept ein proxy-Netz? Dieser Guide erklärt Docker-Netzwerke, das interne DNS und …

Docker-Volumes vs. Bind-Mounts: Wo deine Daten wirklich liegen
Named Volumes oder Bind-Mount? Dieser Grundlagen-Guide erklärt den Unterschied, zeigt beide an echten Beispielen und …

Die eigene Website mit Hugo hosten – wie die Serverküche selbst
Statische Website mit Hugo bauen und im Docker-Container hinter Traefik ausliefern – schnell, sicher, ohne Datenbank. …