PostgreSQL in Docker — die Fallen die mich Daten gekostet haben
Ich betreibe mehrere PostgreSQL-Datenbanken in Docker auf meinen Raspberry Pis und VPS-Servern. Dabei bin ich in zwei Fallen getappt, die mich echte Daten gekostet haben. Dieser Artikel dokumentiert was passiert ist und wie du es verhinderst.
DarkWolfCave.de
PostgreSQL und Docker — was schiefgehen kann
PostgreSQL in Docker einzurichten ist schnell erledigt. Ein docker-compose.yml mit dem offiziellen Image, ein Volume für die Daten, Container starten — läuft. So habe ich das über ein Dutzend Mal gemacht, für verschiedene Projekte auf meinen Raspberry Pis und VPS-Servern.
Das Problem: Es läuft so lange, bis du etwas am Setup änderst. Ein Image-Update, ein docker compose down mit dem falschen Flag — und die Daten sind weg.
Du wirst hier einen groben Überblick finden.
Allerdings biete ich dir auch noch etwas mehr Support an:
- Du benötigst persönlichen Support
- Du möchtest von Beginn an Unterstützung bei deinem Projekt
- Du möchtest ein hier vorgestelltes Plugin durch mich installieren und einrichten lassen
- Du würdest gerne ein von mir erstelltes Script etwas mehr an deine Bedürfnisse anpassen
Für diese Punkte und noch einiges mehr habe ich einen limitierten VIP-Tarif eingerichtet.
Falls der Tarif gerade nicht verfügbar ist, kontaktiere mich auf Discord!
Der PGDATA-Pfad hat sich mit PostgreSQL 18 geändert
Im März 2026 läuft auf meinem Raspberry Pi 5 ein Projekt mit PostgreSQL 18 und Alpine als Basis-Image. Die docker-compose.yml sieht so aus, wie ich sie seit PostgreSQL 16 verwende:
services:
postgres:
image: postgres:18-alpine
volumes:
- ./data/postgres:/var/lib/postgresql/data
environment:
- POSTGRES_DB=meine_app
- POSTGRES_USER=app_user
- POSTGRES_PASSWORD=sicheres_passwort
Um zu verstehen warum das ein Problem ist, hilft es sich einen Volume-Mount als USB-Stick vorzustellen. Du steckst ihn an einer bestimmten Stelle in den Container ein. Alles was der Container an diese Stelle schreibt, landet auf dem Stick — und überlebt, wenn der Container gelöscht wird. Alles was er woanders hinschreibt, ist weg sobald der Container weg ist.
In meiner Config steckt der “Stick” bei /var/lib/postgresql/data:
Container-Dateisystem:
/var/lib/postgresql/
├── data/ ← mein Volume ist HIER eingesteckt
│ └── (leer)
Bis PostgreSQL 17 hat das funktioniert, weil PostgreSQL seine Daten genau dort abgelegt hat — in /var/lib/postgresql/data. Ab PostgreSQL 18 hat sich das geändert. Das offizielle Docker-Image schreibt jetzt nach /var/lib/postgresql/18/docker:
Container-Dateisystem:
/var/lib/postgresql/
├── data/ ← mein Volume ist hier — aber PG schreibt hier nicht mehr hin
├── 18/
│ └── docker/ ← PG 18 schreibt HIERHIN — kein Volume, kein Stick!
│ ├── base/
│ └── pg_wal/
Die Daten landen also neben meinem Volume, nicht darin. Container weg = Daten weg.
| PostgreSQL Version | Schreibt nach | Volume muss auf |
|---|---|---|
| 17 und älter | /var/lib/postgresql/data | /var/lib/postgresql/data |
| 18 und neuer | /var/lib/postgresql/18/docker | /var/lib/postgresql |
Diese Änderung ist in PR #1259 und auf der Docker Hub Seite unter “Important Change” dokumentiert. Der Grund: Das neue Layout ermöglicht schnellere Major-Upgrades mit pg_upgrade --link, weil alte und neue Daten im selben Verzeichnisbaum liegen können.
Die Information war verfügbar. Bei einem Major-Version-Upgrade einer Datenbank hätte ich die Image-Doku prüfen sollen. Habe ich nicht.
Was bei mir passiert ist
Auf meinem Raspberry Pi 5 lief mein Endlessh-Projekt mit genau dieser Config — Volume auf /var/lib/postgresql/data, aber PG 18 schrieb nach /var/lib/postgresql/18/docker. Die Daten landeten nicht in meinem Volume.
Das fiel nicht sofort auf, weil der Container lief und die Datenbank funktionierte. Erst bei docker compose down und docker compose up -d war alles weg — PostgreSQL startete mit einer leeren Datenbank.
Über mehrere Wochen und Container-Neustarts hatten sich 21 verwaiste Volumes angesammelt — bei jedem Neustart hatte Docker ein neues angelegt. Die aktiven Daten — 10.548 Bot-Einträge, 42 Achievements und 123 Treasures — waren zum Glück noch in einem davon vorhanden. Ich konnte sie retten.
⚠️ Wichtig: Falls dir das passiert ist, prüfe mit
docker volume lsob verwaiste Volumes existieren. Deine Daten könnten noch drin sein. Nicht blind löschen — erst inspizieren:docker run --rm -v VOLUME_HASH:/data alpine ls -la /data/
Der Fix
Den “Stick” eine Ebene höher einstecken — auf /var/lib/postgresql statt auf /var/lib/postgresql/data:
services:
postgres:
image: postgres:18-alpine
volumes:
# Ab PG 18: eine Ebene höher mounten
- ./data/postgres:/var/lib/postgresql
environment:
- POSTGRES_DB=meine_app
- POSTGRES_USER=app_user
- POSTGRES_PASSWORD=sicheres_passwort
Jetzt liegt alles was PG 18 nach /var/lib/postgresql/18/docker schreibt innerhalb des Mounts. Auf dem Host findest du die Daten unter ./data/postgres/18/docker/.
Container-Dateisystem:
/var/lib/postgresql/ ← Volume ist HIER eingesteckt
├── 18/
│ └── docker/ ← PG 18 schreibt hierhin
│ ├── base/ ← alles landet im Volume ✓
│ └── pg_wal/
Es gibt auch einen Workaround: du kannst mit PGDATA=/var/lib/postgresql/data den alten Pfad erzwingen. Das funktioniert, aber du verlierst den Vorteil des neuen Layouts — bei einem späteren Major-Upgrade (18 → 19) können mit dem neuen Layout alte und neue Daten nebeneinander existieren, und pg_upgrade --link spart dir einen kompletten Dump/Restore.
💡 Tipp: Du kannst den PGDATA-Pfad jedes Docker-Images prüfen, bevor du es einsetzt:
docker inspect postgres:18-alpine | grep PGDATA
Was ich daraus gelernt habe
Die PGDATA-Änderung ist auf Docker Hub dokumentiert, und die Image-Maintainer haben eine Schutzfunktion eingebaut, die den Container stoppt wenn alte Daten an den alten Pfaden liegen. Allerdings: docker compose pull zeigt keine Warnung, es gibt keinen Changelog für Docker-Images. Wer nicht aktiv die Doku liest, bekommt davon nichts mit.
Bei einem Major-Version-Upgrade einer Datenbank sollte man die Image-Dokumentation prüfen. Seitdem habe ich eine Regel: Vor jedem Datenbank-Image-Update die Docker Hub Seite des Images prüfen. Kein blindes docker compose pull && docker compose up -d mehr.
Du willst Docker nicht selbst einrichten? Ich übernehme das für dich — schau dir meine Services an
Named Volumes und das -v Flag
Die zweite Falle hat nichts mit PostgreSQL-Versionen zu tun, sondern damit wie Docker Volumes verwaltet. Bevor ich auf Bind Mounts umgestiegen bin, habe ich Named Volumes verwendet. So wie es die meisten Docker-Tutorials zeigen:
services:
postgres:
image: postgres:17
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
Sieht aufgeräumt aus. Docker verwaltet das Volume, du musst dich um nichts kümmern. Bis du docker compose down -v tippst. Dieses Problem betrifft übrigens alle PostgreSQL-Versionen — nicht nur 18.
Was docker compose down -v wirklich tut
Das -v Flag löscht alle Named Volumes und anonymen Volumes die zum Compose-Projekt gehören. Laut der offiziellen Docker-Dokumentation:
Remove named volumes declared in the “volumes” section of the Compose file and anonymous volumes attached to containers.
Deine PostgreSQL-Daten sind danach weg. Kein Papierkorb, keine Bestätigung, kein Zurück.
Mir ist das beim Debugging eines Netzwerkproblems passiert. Ich wollte einen Container sauber neu starten und habe reflexartig docker compose down -v getippt — weil ich es vorher bei einem Test-Setup so gemacht hatte. Bei einem Produktionsprojekt war das eine andere Sache.
Es gibt noch mehr Wege deine Volumes zu verlieren
docker compose down -v ist nicht die einzige Gefahr für Named Volumes:
| Befehl | Effekt |
|---|---|
docker compose down -v | Löscht Named + anonyme Volumes des Projekts |
docker volume prune | Löscht alle nicht genutzten Volumes |
docker system prune --volumes | Löscht alle nicht genutzten Volumes + Images + Container |
docker compose down (ohne -v) | Volumes bleiben erhalten |
Das Problem bei Named Volumes: du siehst sie nicht. Sie liegen unter /var/lib/docker/volumes/ in einem von Docker verwalteten Verzeichnis. Du sicherst sie nicht mit deinem normalen Backup-Script, und du merkst erst dass sie weg sind wenn PostgreSQL eine leere Datenbank initialisiert.
Bind Mounts statt Named Volumes
Inzwischen verwende ich in allen meinen Projekten Bind Mounts statt Named Volumes:
services:
postgres:
image: postgres:18
volumes:
- ./data/postgres:/var/lib/postgresql
# Kein volumes:-Block mehr nötig
Die Daten liegen jetzt unter ./data/postgres/18/docker/ auf dem Host. Ich sehe sie mit ls, ich kann sie mit Standard-Tools sichern und sie überleben jedes docker compose down -v.
Das data/-Verzeichnis kommt in die .gitignore:
echo "data/" >> .gitignore
PostgreSQL-Versionen sicher upgraden
Das Upgrade von PostgreSQL 16 oder 17 auf Version 18 erfordert wegen der PGDATA-Änderung besondere Vorsicht. So gehe ich seitdem bei meinen Projekten vor:
# 1. Breaking Changes lesen! Release Notes und Dockerfile prüfen
docker inspect postgres:18-alpine | grep PGDATA
# 2. Backup erstellen (alter Container muss noch laufen!)
docker compose exec postgres pg_dumpall -U DEIN_USER > backup_$(date +%Y%m%d).sql
# 3. Container stoppen (OHNE -v!)
docker compose down
# 4. docker-compose.yml anpassen: neues Image + Volume-Mount
# image: postgres:18-alpine
# volumes:
# - ./data/postgres:/var/lib/postgresql
# 5. Altes Datenverzeichnis umbenennen (nicht löschen!)
mv ./data/postgres ./data/postgres_backup_$(date +%Y%m%d)
# 6. Neuen Container starten (initialisiert leere DB)
docker compose up -d
# 7. Backup einspielen
cat backup_$(date +%Y%m%d).sql | docker compose exec -T postgres psql -U DEIN_USER
# 8. Prüfen ob alles da ist
docker compose exec postgres psql -U DEIN_USER -c "\dt"
Diesen Prozess habe ich bei der Migration von PostgreSQL 16 auf 18 für mein CronWolf-Projekt verwendet — 382 MB Daten, erfolgreich migriert und mit 11 Checks validiert.
Du willst Docker nicht selbst einrichten? Ich übernehme das für dich — schau dir meine Services an
Meine Checkliste für PostgreSQL in Docker
Nach diesen Erfahrungen habe ich mir eine Checkliste angelegt, die ich bei jedem neuen Projekt und jedem Image-Update durchgehe:
Beim Erstellen eines neuen Projekts:
- Bind Mount statt Named Volume verwenden
- Ab PG 18: Mount auf
/var/lib/postgresql(nicht/var/lib/postgresql/data) - PGDATA-Pfad muss innerhalb des Volume-Mounts liegen
-
data/in.gitignoreeintragen - Backup-Strategie definieren (pg_dump Cronjob)
Vor jedem Image-Update:
- Release Notes und Breaking Changes lesen
- PGDATA des neuen Images prüfen:
docker inspect IMAGE | grep PGDATA - Backup erstellen bevor der Container gestoppt wird
- Bei Major-Version-Upgrades: pg_dump → neues Volume → Restore
- Nach dem Update:
docker volume lsauf neue anonyme Volumes prüfen
Regelmäßig:
-
docker volume ls -f dangling=trueauf verwaiste Volumes prüfen - Bind-Mount-Verzeichnis in Backup-Script einschließen
Wenn du Docker auf deinem Raspberry Pi noch nicht eingerichtet hast, findest du in meinem Artikel Docker auf dem Raspberry Pi installieren eine Schritt-für-Schritt-Anleitung. Und falls du eigene Images bauen willst, schau dir Eigenen Docker Container erstellen an. Für eine Backup-Lösung, die auch deine Docker Volumes sichert, habe ich in Docker Backup für Raspberry Pi ein komplettes Script zusammengestellt.
Die docker-compose.yml die ich heute verwende
Nach all diesen Erfahrungen sieht meine Standard-Konfiguration für PostgreSQL in Docker so aus:
services:
postgres:
image: postgres:18-alpine
restart: unless-stopped
volumes:
# Bind Mount auf /var/lib/postgresql — neues PG 18 Layout
- ./data/postgres:/var/lib/postgresql
environment:
- POSTGRES_DB=meine_app
- POSTGRES_USER=app_user
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app_user -d meine_app"]
interval: 10s
timeout: 5s
retries: 5
Zwei Dinge machen den Unterschied: Bind Mount statt Named Volume und der Mount auf /var/lib/postgresql statt auf das alte /var/lib/postgresql/data. PGDATA muss nicht überschrieben werden — der Default passt zum Mount. Dazu ein Healthcheck, damit du sofort siehst wenn etwas nicht stimmt. Damit habe ich seit dem Fix kein einziges Volume-Problem mehr gehabt.
Kommentare