DarkWolfCave
docker

PostgreSQL in Docker — Fallen die mich Daten gekostet haben

Wolf blickt besorgt auf einen Docker-Container mit PostgreSQL-Logo aus dem Daten herausfallen
DarkWolf selbstbewusst KI-Bild Generiert mit Gemini

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.

VIP Support
Wolf Support Avatar

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 VersionSchreibt nachVolume 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 ls ob 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:

BefehlEffekt
docker compose down -vLöscht Named + anonyme Volumes des Projekts
docker volume pruneLöscht alle nicht genutzten Volumes
docker system prune --volumesLö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 .gitignore eintragen
  • 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 ls auf neue anonyme Volumes prüfen

Regelmäßig:

  • docker volume ls -f dangling=true auf 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.

FAQ - Frequently Asked Questions DarkWolfCave
DarkWolf hilft bei FAQs

Häufig gestellte Fragen

Warum ändert PostgreSQL 18 den PGDATA-Pfad im Docker-Image?
Ab PostgreSQL 18 nutzt das offizielle Docker-Image ein pg_ctlcluster-kompatibles Layout. PGDATA zeigt auf /var/lib/postgresql/18/docker statt wie bisher auf /var/lib/postgresql/data. Das ermöglicht In-Place-Upgrades mit pg_upgrade --link.
Wie konfiguriere ich Volumes für PostgreSQL 18 in Docker richtig?
Ab PostgreSQL 18 mountest du auf /var/lib/postgresql statt auf /var/lib/postgresql/data. PGDATA bleibt beim Default (/var/lib/postgresql/18/docker). Beispiel: ./data/postgres:/var/lib/postgresql. Alternativ kannst du PGDATA auf den alten Pfad überschreiben, verlierst aber den Vorteil von pg_upgrade --link.
Was ist der Unterschied zwischen Named Volumes und Bind Mounts?
Named Volumes werden von Docker verwaltet und liegen unter /var/lib/docker/volumes/. Bind Mounts zeigen auf ein Verzeichnis auf deinem Host. Bind Mounts überleben docker compose down -v und docker volume prune, Named Volumes nicht.
Was passiert bei docker compose down -v mit meinen Daten?
Das -v Flag löscht alle Named Volumes und anonymen Volumes die zum Compose-Projekt gehören. Deine Datenbank-Daten sind danach unwiederbringlich weg, wenn sie in einem Named Volume lagen.
Wie erkenne ich anonyme Docker Volumes?
Mit docker volume ls siehst du alle Volumes. Anonyme Volumes haben einen langen Hash als Namen (z.B. cff178ac...) statt eines lesbaren Namens. Sie entstehen wenn das Docker-Image ein VOLUME deklariert, das nicht durch einen expliziten Mount abgedeckt wird.
Soll ich für PostgreSQL in Docker Bind Mounts oder Named Volumes verwenden?
Für Produktionsdaten empfehle ich Bind Mounts. Du siehst die Daten direkt im Dateisystem, kannst sie mit normalen Backup-Tools sichern, und sie überleben jedes docker compose down -v oder docker volume prune.

Kommentare

URLs werden automatisch verlinkt
Kommentare werden geladen...