Wenn Docker schneller als das NAS ist

Am Morgen meldete mein Backup-Tool einen Fehler, und zwar bei allen GitHub-Repositories, die es in meine selbst gehostete Forgejo-Instanz spiegeln sollte. Im Log standen ERR pkt-line 3: EOF git=push und anschließend Exiting with status=1. Mein erster Verdacht fiel auf das Tool-Image, das ich kurz zuvor aktualisiert hatte. Plausibel, schnell gedacht und komplett daneben.

Die eigentliche Ursache lag eine Schicht tiefer. Forgejo speichert seine Repositories auf der NAS, die per NFS in den Container gemountet wird. Mein Docker-Host bootete um 04:06:14, der Forgejo-Container startete um 04:06:45. Die NFS-Mounts waren erst ab 04:07:06 aktiv, der letzte sogar erst um 04:07:16. Docker war also schneller als die NAS.

Das ist eine ungünstige Reihenfolge, weil Docker Bind-Mounts beim Containerstart auflöst und fehlende Quellverzeichnisse selbst anlegt. Existiert der Mountpunkt zu diesem Zeitpunkt nur als lokales Verzeichnis, bindet Docker genau dieses leere Verzeichnis ein. Wenn NFS später erscheint, sieht der Host zwar die echten Daten, der Container bleibt aber an seinem lokalen Schattenverzeichnis hängen.

Der falsche Verdacht

Weil der Fehler nach einem Image-Update auftrat, sah zunächst alles nach einer Regression aus. Erst der Blick auf die übrigen Dienste zeigte das Muster: Mehrere weitere Container waren betroffen, von der Mediensammlung bis zur Dokumentenablage. Ein weiterer Dienst schrieb Downloads auf die lokale Platte statt auf die NAS, und Paperless bemerkte Dateien im Eingangsordner nicht. Forgejo war nur der Dienst, der am lautesten protestierte.

In einem verdeckten lokalen Ordner lagen außerdem rund 21 GB alte Daten aus einem früheren, unbemerkten Auftreten desselben Fehlers, teilweise aus Oktober 2025. Der Vorfall war also nicht neu, nur diesmal laut genug, dass ich ihn nicht mehr als Einzelfehler wegdiskutieren konnte.

Zwei Sichten auf denselben Pfad

Die Diagnose lässt sich ohne Spezialwerkzeug nachvollziehen. Auf dem Host führt der Bind-Mount /mnt/ds920/Datengrab/git auf /data im Forgejo-Container. Die Repositories liegen auf dem Host unter einem doppelten git-Pfad, weil das der Datenordner von Forgejo ist. Im Container verschwindet der erste Teil durch den Mount. Die beiden Befehle stehen weiter unten in Schritt 2 des Guides.

Das Ergebnis war auf dem Host 71 und im Container 0. Ein konkreter Hostpfad lautet /mnt/ds920/Datengrab/git/git/repositories/codebude/anchor-cms.git, im Container wird daraus /data/git/repositories/codebude/anchor-cms.git. Genau diese zwei Sichten sind bei einem kaputten Bind-Mount entscheidend: Der Host sieht durch NFS die Wahrheit, der Container sieht eine lokale Attrappe.

Die Grundlagen zu Docker Bind-Mounts beschreibt die Docker-Dokumentation zu Bind-Mounts sehr gut. Das Verhalten ist nicht Forgejo-spezifisch, Forgejo macht es nur unangenehm sichtbar, wenn plötzlich jedes Repository verschwunden zu sein scheint.

Die schnelle Lösung

Sobald die NFS-Mounts aktiv sind, reicht ein Neustart der betroffenen Container. Docker löst den Bind-Mount beim Start erneut auf:

docker restart forgejo paperless

Forgejo und Paperless sind hier die konkreten Beispiele. Bei den übrigen betroffenen Diensten gehe ich genauso vor, ohne blind den kompletten Host neu zu starten. Danach prüfe ich die Container-Sicht erneut, die Logs und, wenn möglich, einen echten Funktionstest.

Guide: den Fehler erkennen und beheben

1. Symptome erkennen

Verdächtig sind leere Datenbestände, Dateien, die ein Dienst nicht sieht, und Anwendungen, die scheinbar erfolgreich an den falschen Ort schreiben. Wenn mehrere Container gleichzeitig ihre Daten verlieren, prüfe ich zuerst die Mounts und nicht das zuletzt aktualisierte Image.

2. Host und Container vergleichen

Ich vergleiche die Forgejo-Sicht auf dem Host und im Container mit demselben Beispiel:

# Host-Sicht über den NFS-Mount
find /mnt/ds920/Datengrab/git/git/repositories -maxdepth 2 -name '*.git' | wc -l   # 71

# Dieselbe Stelle aus dem Container
docker exec forgejo find /data/git/repositories -maxdepth 2 -name '*.git' | wc -l  # 0
Schema mit unterschiedlichen Host- und Container-Sichten auf ein NFS-Verzeichnis

Zusätzlich prüfe ich, welcher Hostpfad tatsächlich eingebunden ist:

docker inspect forgejo --format '{{range .Mounts}}{{println .Source "->" .Destination}}{{end}}'

3. Betroffene Container identifizieren

Ich suche die laufenden Container nach Bind-Mounts unterhalb des NAS-Pfads und prüfe die Liste anschließend manuell:

docker ps -q | xargs -r docker inspect --format '{{.Name}} {{range .Mounts}}{{if eq .Type "bind"}}{{println .Source "->" .Destination}}{{end}}{{end}}' | grep -i ds920

4. Akut neu starten

Wenn die Mounts sicher aktiv sind, starte ich die betroffenen Container gezielt neu. Bei einem Compose-Stack ist ein gezieltes docker compose restart meist übersichtlicher als ein Rundumschlag. Für automatisches Startverhalten nach einem Reboot ist auch die Docker-Dokumentation zu Restart-Policies hilfreich.

5. Drop-in anlegen und laden

Bei mir hatte docker.service keine Abhängigkeit zu den NFS-Mount-Units. Ohne jede Konfiguration starten Docker und die Mounts parallel. Es ist ein Wettlauf, und genau den hat Docker diesmal gewonnen.

Ich habe deshalb dieses Drop-in angelegt. RequiresMountsFor= erzeugt laut systemd.unit für die benötigten Mount-Units zwei Dinge auf einmal: eine Abhängigkeit vom Typ Requires= und eine Reihenfolgeabhängigkeit vom Typ After=. Die Mount-Units, die den angegebenen Pfad bereitstellen, müssen also zuerst abgearbeitet werden.

# /etc/systemd/system/docker.service.d/10-wait-for-nas.conf
[Unit]
RequiresMountsFor=/mnt/ds920/Datengrab /mnt/ds920/Filme /mnt/ds920/Musik /mnt/ds920/homes
After=mnt-ds920-Datengrab.mount mnt-ds920-Filme.mount mnt-ds920-Musik.mount mnt-ds920-homes.mount

Aus /mnt/ds920/Filme wird dabei mnt-ds920-Filme.mount. Die Schreibweise der Mount-Unit leitet sich aus dem Pfad ab, Slashes werden zu Bindestrichen. Die Details zu solchen Units stehen in der systemd-Dokumentation für Mount-Units.

sudo mkdir -p /etc/systemd/system/docker.service.d
sudoedit /etc/systemd/system/docker.service.d/10-wait-for-nas.conf
sudo systemctl daemon-reload
sudo systemctl restart docker

Die harte Variante bedeutet: Schlägt ein benötigter Mount fehl, startet Docker nicht. Das ist für mich die bessere Voreinstellung, weil ein sichtbar gestoppter Dienst weniger gefährlich ist als mehrere Container, die still in leere Verzeichnisse schreiben.

Wer Verfügbarkeit höher gewichtet, kann die weiche Variante verwenden. Wichtig ist, dass sie nicht dasselbe ist wie keine Konfiguration: After= nimmt den Wettlauf weg, und Wants= zieht die Mount-Units zusätzlich in dieselbe Transaktion. Docker wartet dann auf die abgearbeitete Reihenfolge, startet aber auch weiter, wenn ein Mount scheitert. Neuere systemd-Fassungen bieten dafür außerdem die Kurzform WantsMountsFor=, die wie RequiresMountsFor= aussieht, aber Wants= statt Requires= erzeugt.

[Unit]
Wants=mnt-ds920-Datengrab.mount mnt-ds920-Filme.mount mnt-ds920-Musik.mount mnt-ds920-homes.mount
After=mnt-ds920-Datengrab.mount mnt-ds920-Filme.mount mnt-ds920-Musik.mount mnt-ds920-homes.mount

Der einzige Unterschied zwischen den Varianten liegt damit im Verhalten bei einem fehlgeschlagenen Mount. Mit RequiresMountsFor= startet Docker nicht, mit Wants= startet Docker trotzdem und trägt bewusst das Risiko, auf ein leeres lokales Verzeichnis zu binden. Meine Empfehlung bleibt deshalb die harte Variante. Die weiche ist für den Fall gedacht, in dem Verfügbarkeit wichtiger ist als ein sicherer Abbruch.

Beim Neustart von Docker können Container abhängig von ihrer Restart-Policy wieder anlaufen. Deshalb prüfe ich vorab, dass die Mounts verfügbar sind, und wiederhole danach den Vergleich aus Schritt zwei.

6. Abhängigkeit prüfen

systemctl show docker.service -p RequiresMountsFor --value
systemctl show docker.service -p After --value | tr ' ' '\n' | grep mnt

In der Ausgabe müssen die Mountpfade und die zugehörigen Units auftauchen. Ein Reload allein ist kein Beweis, eine Kontrolle der gespeicherten Abhängigkeit schon eher.

Lokale Schatten sicher aufräumen

Der NFS-Mount verdeckt die lokalen Verzeichnisse an den Mountpunkten. Ein nicht-rekursiver Bind des Root-Dateisystems macht die darunterliegenden Reste sichtbar. Die NAS-Mounts werden dabei nicht mitgenommen:

sudo mkdir -p /mnt/plainroot
sudo mount --bind / /mnt/plainroot          # nicht rekursiv, NFS-Mounts fehlen darin
du -sh /mnt/plainroot/mnt/ds920/*
findmnt -n -o FSTYPE -T /mnt/plainroot/mnt/ds920/Datengrab   # muss ext4 zeigen, nicht nfs4

Vor jedem Löschen prüfe ich den konkreten Zielpfad mit findmnt. Er muss ein lokales Dateisystem wie ext4 und nicht nfs4 zeigen. Die Mountpunkt-Verzeichnisse selbst bleiben stehen.

sudo rm -rf /mnt/plainroot/mnt/ds920/Datengrab/Downloads/*
sudo umount /mnt/plainroot
sudo rmdir /mnt/plainroot

Bei den alten Daten habe ich Pfade, Zeiten und Inhalte geprüft, bevor etwas gelöscht wurde. Ein verstecktes lokales Verzeichnis ist kein Backup, auch wenn es sich beim Aufräumen kurz so tarnt.

Fazit

Der Fehler lag nicht im Backup-Tool und nicht im aktualisierten Image. Docker war einfach schneller als die NAS und band ein lokales Verzeichnis, bevor NFS bereitstand. Ein paar Zeilen systemd-Konfiguration verhindern, dass ein kleines Timingproblem dazu führt, dass ein Container unbemerkt im falschen Verzeichnis arbeitet.

Wer Docker im Alltag mit CUDA nutzt, findet außerdem den passenden Beitrag Llama.cpp in Docker unter WSL2.

Habt ihr Ähnliches erlebt?

Schreibt einen Kommentar und erzählt, wie ihr den Fehler gefunden und dauerhaft abgesichert habt.

Raffi

Seit 2011 blogge ich hier über Programmierung, meine Software, schreibe Tutorials und versuche mein Wissen, so gut es geht, mit meinen Lesern zu teilen.

Hinterlasse einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Sie dient nur dem Spamschutz.