· DevOps · 7 minuten Lesezeit
Docker Volumes in Produktion mit Named Volumes, Bind Mounts und der Hetzner-Volume-Trick
Ich habe gelernt, dass ein Docker Named Volume in Produktion nicht automatisch auf einem Hetzner Volume landet. Erst wenn ich den Pfad bewusst steuere, weiß ich wirklich, wo Qdrant, Redis und Zertifikate geschrieben werden.

Inhalt
- Das Problem mit Named Volumes in Produktion
- Bind Mounts mit eigenem Pfad
- Die driver_opts Alternative
- Der env var Trick für das Beste aus beiden Welten
- Redis AOF-Persistenz
- Hetzner Volume als Fundament
- restart unless-stopped
- Das Deployment-Runbook
- Server Backups als zweite Sicherungsschicht
- Alle Artikel der Serie
Das Problem mit Named Volumes in Produktion
Named Volumes sind lokal großartig. Ich schreibe einfach qdrant_data:/qdrant/storage und Docker kümmert sich um den Rest. Für lokale Entwicklung ist das genau richtig, weil ich keinen Pfad pflegen muss und trotzdem persistente Daten bekomme.
In Produktion hat diese Bequemlichkeit aber eine blinde Stelle. Docker verwaltet das Volume intern und legt die Daten typischerweise unter /var/lib/docker/volumes/.../_data auf dem Root-Disk des Servers ab. Wenn ich auf einer Hetzner VM zusätzlich ein Hetzner Volume an /mnt/data mounte, ändert das für Docker Named Volumes erst einmal gar nichts. Sie schreiben weiter auf das lokale Root-Disk der VM.
Genau das ist der Denkfehler, den ich vermeiden will. Ein zusätzlich angehängtes Hetzner Volume schützt meine Daten nicht automatisch. Es schützt sie nur dann, wenn meine Container auch wirklich dorthin schreiben.
So sieht die bequeme lokale Konfiguration heute aus:
# docker-compose.yml
services:
qdrant:
volumes:
- qdrant_data:/qdrant/storageLokal ist das gut. In Produktion ist es mir zu intransparent.
Bind Mounts mit eigenem Pfad
Ein Bind Mount ist die ehrliche Variante. Ich schreibe den Zielpfad selbst hin und sehe damit sofort, wo die Daten landen.
services:
qdrant:
volumes:
- ./data/qdrant:/qdrant/storageOder in Produktion explizit so:
services:
qdrant:
volumes:
- /mnt/data/qdrant:/qdrant/storageDer Vorteil ist offensichtlich: keine Magie, keine Docker-interne Ablage, keine falsche Annahme über das Root-Disk. Der Nachteil ist ebenfalls klar. Wenn ich den Produktionspfad direkt in docker-compose.yml fest eintrage, passt dieselbe Datei lokal nicht mehr sauber. Dann beginne ich sehr schnell mit mehreren fast gleichen Compose-Dateien oder mit manuellen Anpassungen pro Umgebung.
Die driver_opts Alternative
Docker kennt dafür noch einen Mittelweg. Ich kann ein Named Volume definieren und ihm intern sagen, dass es eigentlich nur ein Bind Mount auf einen festen Pfad ist.
volumes:
qdrant_data:
driver: local
driver_opts:
type: none
o: bind
device: /mnt/data/qdrantTechnisch funktioniert das gut. Ich bekomme den Namen eines Volumes und trotzdem einen expliziten Pfad. Für mich bleibt es aber schwerer lesbar als nötig. Wer diese Syntax nicht regelmäßig benutzt, muss erst einmal erinnern, was type: none, o: bind und device hier genau bedeuten.
Der env var Trick für das Beste aus beiden Welten
Die sauberste Variante ist für mich deshalb ein einfacher Compose-Ausdruck mit Default-Wert:
services:
qdrant:
volumes:
- ${QDRANT_DATA_DIR:-qdrant_data}:/qdrant/storage
restart: unless-stopped
redis:
command: redis-server --appendonly yes --appendfsync everysec
volumes:
- ${REDIS_DATA_DIR:-redis_data}:/data
restart: unless-stopped
traefik:
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ${LETSENCRYPT_DATA_DIR:-letsencrypt_data}:/letsencrypt
restart: unless-stopped
loki:
volumes:
- ${LOKI_DATA_DIR:-loki_data}:/loki
restart: unless-stopped
grafana:
volumes:
- ${GRAFANA_DATA_DIR:-grafana_data}:/var/lib/grafana
restart: unless-stopped
ollama:
volumes:
- ${OLLAMA_DATA_DIR:-ollama_data}:/root/.ollama
restart: unless-stopped
volumes:
qdrant_data:
redis_data:
letsencrypt_data:
loki_data:
grafana_data:
ollama_data:Der Trick liegt in :-. Wenn QDRANT_DATA_DIR nicht gesetzt ist, nutzt Docker das Named Volume qdrant_data. Das ist ideal für lokal. Wenn ich in Produktion QDRANT_DATA_DIR=/mnt/data/qdrant setze, wird daraus automatisch ein Bind Mount auf genau diesen Pfad.
So bekomme ich beide Vorteile gleichzeitig: lokal einfache Named Volumes, in Produktion volle Pfadkontrolle. Und ich muss die Compose-Datei nicht duplizieren.
Redis AOF-Persistenz
Bei Redis ist der Fehler noch tückischer, weil er oft erst nach dem ersten Crash sichtbar wird. Redis lebt primär im RAM. Ohne aktivierte Persistenz ist ein Neustart für BullMQ so, als hätte es die Queue nie gegeben. Wartende Jobs verschwinden, aktive Jobs sind weg, und der Fehler wirkt im Nachhinein wie ein unerklärlicher Schluckauf.
Deshalb würde ich Redis in Produktion nie ohne AOF laufen lassen.
redis:
command: redis-server --appendonly yes --appendfsync everysec
volumes:
- ${REDIS_DATA_DIR:-redis_data}:/dataappendonly yes schreibt jede Änderung fortlaufend auf Disk. appendfsync everysec ist dabei ein guter Kompromiss aus Datensicherheit und I/O-Last. Die BullMQ-Queue überlebt damit Reboots und Container-Crashes sehr viel zuverlässiger.
Hetzner Volume als Fundament
Ein Hetzner Volume ist für mich die eigentliche Basis dieser Strategie. Technisch ist es detachbarer Block Storage, praktisch fühlt es sich an wie eine externe SSD für den Server. Ich mounte das Volume einmal an /mnt/data und lege dort alle persistenten Service-Daten ab.
Der entscheidende Vorteil zeigt sich nicht im Normalbetrieb, sondern im schlechten Tag. Wenn ich den Server neu aufsetzen muss, etwa wegen Größenwechsel, Betriebssystem-Neuinstallation oder Recovery nach einem größeren Defekt, dann muss ich meine Daten nicht exportieren und später wieder importieren. Ich hänge das Volume ab, hänge es an den neuen Server an und mounte wieder /mnt/data. Danach liegen Qdrant, Redis, Zertifikate und Logs sofort wieder an derselben Stelle.
Für 60 GB liegt das aktuell bei ungefähr 2,88 Euro pro Monat. Für eine Produktionsumgebung ist das aus meiner Sicht sehr günstige Ruhe.
restart unless-stopped
Stateful Services brauchen aus meiner Sicht fast immer restart: unless-stopped. Ohne diese Zeile reicht schon ein geplanter Reboot nach einem Kernel-Update, und Teile des Stacks bleiben aus. Dann ist nicht der Datenbestand kaputt, aber die Anwendung ist trotzdem offline.
restart: unless-stopped bedeutet: Container starten nach Crash oder Server-Neustart automatisch wieder hoch, außer ich habe sie bewusst selbst gestoppt. Genau dieses Verhalten will ich für Qdrant, Redis, Traefik, Grafana, Loki und ähnliche Infrastrukturbausteine.
Das Deployment-Runbook
Ein manueller Schritt bleibt trotzdem: Die Zielverzeichnisse müssen auf dem gemounteten Hetzner Volume existieren, bevor ich den Stack das erste Mal starte.
# Run once after mounting the Hetzner Volume at /mnt/data
mkdir -p /mnt/data/{qdrant,redis,letsencrypt,loki,grafana,ollama}Relative Bind Mounts kann Docker oft selbst anlegen. Bei absoluten Produktionspfaden will ich mich darauf nicht verlassen. Ich erstelle die Struktur bewusst, prüfe den Mount und starte erst dann docker-compose up -d.
Server Backups als zweite Sicherungsschicht
Ein Hetzner Volume ist starke Echtzeit-Persistenz, aber für mich nicht die einzige Sicherung. Darüber lege ich noch Hetzner Server Backups. Diese Backups sind vollständige VM-Snapshots zu einem bestimmten Zeitpunkt.
Damit habe ich zwei unabhängige Ebenen. Das Volume schützt mich im laufenden Betrieb und bei Serverwechseln. Das Server Backup schützt mich bei Bedienfehlern, korrupten Deployments oder einem Problem, das erst Stunden später bemerkt wird. Für einen CX21 liegt diese zweite Schicht ungefähr bei 1,50 Euro pro Monat zusätzlich.

Die Grafik trennt das lokale Root-Disk der VM klar vom gemounteten Hetzner Volume. Ich zeige darin, warum Docker Named Volumes standardmäßig auf dem falschen Datenträger landen können und wie der env var Pfadtrick alle stateful Services gezielt auf /mnt/data lenkt.
Für mich ist genau das die eigentliche Produktionsreife eines Docker-Setups: Nicht nur Container starten zuverlässig, sondern auch Daten landen nachweisbar auf dem Datenträger, den ich im Recovery-Fall wirklich mitnehmen kann.
Alle Artikel der Serie
- Vision und Systemübersicht: Chrome Extension, RAG-Architektur, Projekthintergrund: Artikel lesen
- RAG-System Aufbau: Qdrant, Embeddings, Cosine-Ähnlichkeit in TypeScript: Artikel lesen
- AI Provider Abstraktion: Ollama vs. OpenAI, Interface-Design, kein Vendor-Lock-in: Artikel lesen
- Chrome Extension MV3: Drei isolierte Laufzeitkontexte, Message Passing, Strategy Pattern: Artikel lesen
- Docker Compose Strategie: Override-Pattern, von lokal zu Azure: Artikel lesen
- Ollama lokal vs. Docker: Die Entscheidung und ihre Konsequenzen: Artikel lesen
- Ollama Auto-Pull Entrypoint: Automatisiertes Modell-Setup beim Container-Start: Artikel lesen
- tsconfig und Vite:
Node16vs.bundler, warum Vite eigene Regeln hat: Artikel lesen - Instagram Caption mit MutationObserver vollständig laden: Artikel lesen
- Chrome Extension Foundation mit Health-Dot und Retry-Queue: Artikel lesen
- Phase 2 Features: Shadow DOM Overlay, Tailwind v4, Duplicate Detection: Artikel lesen
- Race Condition bei der Plattformerkennung: Wie ein UI-Event die Instagram-Erkennung bricht: Artikel lesen
- PostId-Extraktion in zwei Instagram-Layouts: querySelector vs. Ancestor-Traversal: Artikel lesen
- Instagram Karussell vollständig erfassen mit MutationObserver: Lazy-Loading, Observer-before-click, Timeout-Fallback: Artikel lesen
- Notiz und Tags beim Screenshot-Speichern: Artikel lesen
- Instagram Tastatur-Shortcuts blockieren Chrome Extension Eingaben: Artikel lesen
- Lowercase-Normalisierung und Duplikat-Erkennung im Tag-Input: Artikel lesen
- Zitadel Login V2 in Docker Compose: drei versteckte Fehler: Artikel lesen
- PKCE OAuth in einer Chrome MV3 Extension: Artikel lesen
- React Frontend mit react-oidc-context und Zitadel: Artikel lesen
- Vite Build-Time-Umgebungsvariablen in Docker: Artikel lesen
- Event-Driven Ingestion mit BullMQ und Redis: Artikel lesen
- MinIO statt Azurite: S3-kompatible Objektspeicherung lokal und auf Hetzner: Artikel lesen
- access_token, id_token und der Userinfo-Endpoint: was wohin gehört: Artikel lesen
- Qdrant Multi-Tenancy: Pro Nutzer eine eigene Collection: Artikel lesen
- Wenn Backend und Frontend unterschiedliche Typen kennen: Artikel lesen
- Zitadel Bootstrap entfernt: Host-Header-Bug und manuelles Setup: Artikel lesen
- Backend Code Review: sechs Probleme vor dem Launch behoben: Artikel lesen
- Traefik statt NGINX: Reverse Proxy für einen wachsenden Docker-Compose-Stack: Artikel lesen
- Zweischichtiges Rate Limiting: Traefik und express-rate-limit mit Redis: Artikel lesen
- DSGVO Art. 17 korrekt implementieren: Promise.allSettled und Export-Batching: Artikel lesen
- Embedding-Modell-Lock-in: Warum mxbai-embed-large eine Produktionsentscheidung für immer ist: Artikel lesen
- Docker Volumes in Produktion: Named Volumes, Bind Mounts und der Hetzner-Volume-Trick: (dieser Artikel)
- Zwei Sicherheitslücken vor dem Launch: Redis ohne Auth und ein offener Qdrant-Admin-Port: Artikel lesen
Du baust gerade ein ähnliches System und überlegst, welche Entscheidungen für dein Projekt passen? Lass uns das gemeinsam einschätzen.



