PostgreSQL Backup: pg_dump, pg_basebackup und PITR im Praxistest (2026)

PostgreSQL Backup: pg_dump, pg_basebackup und PITR im Praxistest (2026)

Ein PostgreSQL Backup erstellst du in den allermeisten Fällen mit einem einzigen Befehl: pg_dump -Fc -f datenbank.dump meinedb. Das erzeugt eine komprimierte, in sich konsistente Sicherung einer Datenbank, auch während Besucher weiter schreiben. Wiederhergestellt wird sie mit pg_restore. Für größere Server oder wenn du auf eine bestimmte Sekunde zurückspringen willst, kommt die zweite Methode dazu: eine physische Kopie mit pg_basebackup plus archivierte WAL-Dateien, also Point-in-Time-Recovery.

Das ist die kurze Antwort. Die lange ist wichtiger, denn bei Datenbank-Backups gehen die meisten Dinge nicht laut schief, sondern leise. Ein Backup-Skript meldet Erfolg und hat eine 20 Byte große Datei geschrieben. Ein Restore läuft durch, obwohl zehn Fehler darin stecken. Die WAL-Archivierung scheitert seit Stunden, und die Anwendung merkt nichts davon. Wir haben das am 3. November 2026 auf unserem eigenen Server mit PostgreSQL 18.6 nachgestellt, mit einer 1,5 GB großen Testdatenbank und zehn Millionen Zeilen. Jede dieser Fallen ließ sich in wenigen Minuten auslösen.

Dieser Artikel zeigt dir, welche Backup-Methode wofür taugt, wie du sie einrichtest, wie du ein PostgreSQL Backup wiederherstellst und wie du sicher sein kannst, dass es im Ernstfall auch funktioniert. Alle Zahlen stammen aus unseren eigenen Läufen.

Video: Deployn zeigt automatische PostgreSQL-Backups in Docker samt 3-2-1-Regel (August 2026). Die Messungen und Fallen weiter unten ergänzen das Video.

PostgreSQL Backup: die drei Methoden im Überblick

PostgreSQL bringt drei grundsätzlich verschiedene Wege mit, eine Datenbank zu sichern. Sie schließen sich nicht aus, im Gegenteil: Ein ordentlicher Produktionsserver nutzt meistens zwei davon.

1. Logisches Backup mit pg_dump. pg_dump liest die Datenbank wie ein normaler Client und schreibt alles heraus, was nötig ist, um sie wieder aufzubauen: Tabellendefinitionen, Daten, Indizes, Rechte. Das Ergebnis ist entweder eine SQL-Datei oder ein komprimiertes Archiv. Es funktioniert für eine einzelne Datenbank, lässt sich auf einer anderen PostgreSQL-Version einspielen und ist das, was die meisten Leute meinen, wenn sie „PostgreSQL Backup“ sagen.

2. Physisches Backup mit pg_basebackup. pg_basebackup kopiert das komplette Datenverzeichnis des Servers, Byte für Byte, über das Replikationsprotokoll. Das Ergebnis ist ein startfähiger Klon des ganzen Clusters mit allen Datenbanken. Er läuft nur mit derselben Hauptversion und derselben Architektur.

3. Kontinuierliche Archivierung (WAL) und Point-in-Time-Recovery. PostgreSQL schreibt jede Änderung zuerst ins Write-Ahead-Log, kurz WAL. Wenn du diese WAL-Dateien laufend an einen sicheren Ort kopierst, kannst du ein physisches Backup nehmen und es bis zu einer beliebigen Sekunde danach „vorspulen“. So kommst du zum Beispiel auf den Stand von 14:31:59 zurück, eine Sekunde bevor jemand versehentlich DELETE FROM bestellungen ohne WHERE ausgeführt hat.

Daneben gibt es noch die Variante, das Datenverzeichnis bei gestopptem Server einfach zu kopieren oder einen Dateisystem-Snapshot zu ziehen. Das geht, ist aber fehleranfällig und für die meisten Leser keine gute Idee. Bei laufendem Server ist eine normale Dateikopie des Datenverzeichnisses kein Backup, sondern ein Zufallsprodukt. Dasselbe Problem haben wir übrigens auch bei SQLite gemessen, dort enthielt die Kopie im laufenden Betrieb 0 statt 1.000 Zeilen, nachzulesen in SQLite vs. PostgreSQL.

Links ein Stapel Papierbögen als logischer Export, rechts eine komplette Kopie des Datenbankzylinders als physisches Backup

Unser Testaufbau

Damit die Zahlen einordbar sind: Getestet haben wir mit PostgreSQL 18.6 im offiziellen Docker-Image auf unserem eigenen Server (12 vCPUs, NVMe-SSD). Die Testdatenbank haben wir mit pgbench -i -s 100 erzeugt. Das ergibt eine 1.503 MB große Datenbank mit 10 Millionen Zeilen in der Haupttabelle. WAL-Archivierung war eingeschaltet, summarize_wal ebenfalls, damit wir auch inkrementelle Backups testen konnten.

Nach den Tests haben wir die Container, das Image und alle Testdaten wieder gelöscht, und zwar gezielt nur unsere eigenen. Mit docker volume prune oder ähnlichen Rundumschlägen räumt man sonst auch fremde Daten weg, das haben wir an anderer Stelle schon schmerzhaft gelernt.

Eine Einschränkung vorweg: pgbench-Daten sind sehr gleichförmig und lassen sich deshalb außergewöhnlich gut komprimieren. Bei echten Daten mit Texten, JSON und Zeitstempeln werden deine Backups im Verhältnis größer. Die Relationen zwischen den Methoden bleiben aber gleich.

pg_dump: das Standard-Backup für eine Datenbank

pg_dump kennt vier Ausgabeformate. Wir haben alle mit derselben Datenbank gemessen:

VarianteBefehl (gekürzt)DauerGröße
Plain SQLpg_dump -f shop.sql shop2,9 s933 MB
Plain SQL + gzippg_dump shop | gzip > shop.sql.gz4,3 s27 MB
Custom (Standard-Kompression)pg_dump -Fc -f shop.dump shop3,6 s27 MB
Custom mit zstdpg_dump -Fc -Z zstd:3 -f shop.dump shop2,9 s8 MB
Directory, 4 parallele Jobspg_dump -Fd -j 4 -f shop.dir shop3,2 s27 MB

Was du daraus mitnehmen kannst:

Nimm das Custom-Format (-Fc). Es ist komprimiert, du kannst beim Wiederherstellen einzelne Tabellen auswählen, und pg_restore kann es parallel einspielen. Eine Plain-SQL-Datei kannst du zwar mit jedem Texteditor öffnen, aber du kannst sie nur komplett und nur mit psql einspielen.

zstd lohnt sich. Seit PostgreSQL 16 kann pg_dump mit zstd komprimieren. Bei uns war das Backup damit gleichzeitig schneller (2,9 statt 3,6 Sekunden) und um den Faktor drei kleiner (8 statt 27 MB) als mit der Standard-Kompression. Bei realistischen Daten wird der Abstand kleiner, aber zstd ist fast immer die bessere Wahl.

Parallel dumpen bringt nur bei vielen Tabellen etwas. Das Directory-Format mit -j 4 war bei uns nicht schneller, weil pgbench im Wesentlichen aus einer großen Tabelle besteht. pg_dump parallelisiert pro Tabelle, nicht innerhalb einer Tabelle. Hat deine Anwendung Dutzende ähnlich große Tabellen, sieht das anders aus.

Ist pg_dump im laufenden Betrieb konsistent?

Ja, und das haben wir ausdrücklich geprüft. pg_dump arbeitet in einer einzigen Transaktion mit einem festen Snapshot. Alles, was nach dem Start des Dumps geschrieben wird, sieht es nicht.

Unser Test: Wir haben pgbench mit vier Clients laufen lassen (rund 6.500 Transaktionen pro Sekunde, jede verändert ein Konto, eine Filiale und schreibt eine Historienzeile) und mittendrin einen Dump gezogen. Danach haben wir den Dump in eine frische Datenbank eingespielt und die drei Summen verglichen. Wenn der Dump inkonsistent wäre, also einen Teil einer Transaktion enthielte, würden sie auseinanderlaufen. Ergebnis: Alle drei Summen waren exakt gleich, jeweils −741.192. Du musst deine Anwendung für ein pg_dump also nicht anhalten.

Was pg_dump trotzdem blockiert

Konsistent heißt nicht folgenlos. pg_dump hält während der ganzen Laufzeit eine Sperre auf jeder Tabelle, die es sichert. Normale Lese- und Schreibzugriffe stört das nicht, aber Schemaänderungen schon. Wir haben während eines laufenden Dumps ein ALTER TABLE … ADD COLUMN abgeschickt: Es hing 4,5 Sekunden, bis der Dump fertig war.

Bei uns sind das ein paar Sekunden. Bei einer 200-GB-Datenbank dauert der Dump eine Stunde, und dann wartet dein Deployment mit einer Migration eine Stunde. Schlimmer noch: Hinter dem wartenden ALTER TABLE stauen sich alle weiteren Zugriffe auf diese Tabelle. Leg nächtliche Dumps deshalb nicht in das Zeitfenster, in dem deine Deployments laufen.

pg_dumpall: alle Datenbanken und die Rollen

pg_dump sichert genau eine Datenbank. Benutzer, Rollen, Passwörter und Tablespaces gehören aber nicht zu einer Datenbank, sondern zum ganzen Server, und landen deshalb nicht im Dump. Das ist eine der häufigsten Ursachen für Ärger beim Wiederherstellen.

Wir haben das nachgestellt: eine Rolle shopapp angelegt, ihr Leserechte auf die Tabellen gegeben, die Datenbank mit pg_dump -Fc gesichert und dann auf einem frischen PostgreSQL-Server eingespielt, auf dem es diese Rolle nicht gab. pg_restore meldete:

pg_restore: error: could not execute query: ERROR:  role "shopapp" does not exist
Command was: GRANT SELECT ON TABLE public.pgbench_accounts TO shopapp;
...
pg_restore: warning: errors ignored on restore: 14

Die Daten waren danach vollständig da, alle 10 Millionen Zeilen. Aber die Rechte fehlten, und deine Anwendung, die sich als shopapp anmeldet, könnte nicht darauf zugreifen. Der Exit-Code war 1, ein Skript mit set -e hätte also immerhin abgebrochen.

Zwei Lösungen, je nach Situation:

# Rollen und Passwörter separat sichern (einmal pro Server)
pg_dumpall --globals-only > globals.sql

# Beim Wiederherstellen zuerst die Rollen einspielen
psql -f globals.sql postgres
pg_restore -d shop shop.dump

Oder, wenn du bewusst auf einem anderen Server mit anderen Benutzern landen willst:

pg_restore --no-owner --no-privileges -d shop shop.dump

Damit lief der Restore bei uns ohne einen einzigen Fehler durch, Exit-Code 0. Alle Objekte gehören dann dem Benutzer, der den Restore ausführt, und Rechte musst du neu vergeben.

pg_dumpall ohne Option sichert alle Datenbanken plus Rollen in eine einzige SQL-Datei. Das ist bequem, hat aber zwei Nachteile: Es gibt nur das Plain-Format, also keine Kompression und keine Auswahl beim Restore, und die Datenbanken werden nacheinander und nicht zu einem gemeinsamen Zeitpunkt gesichert. Unsere Empfehlung: pg_dumpall --globals-only für die Rollen plus ein pg_dump -Fc pro Datenbank.

PostgreSQL Backup wiederherstellen

Ein Backup ist nur so gut wie der Restore. Wir haben unsere drei Dump-Varianten jeweils in eine leere Datenbank eingespielt:

RestoreBefehlDauer
Plain SQLpsql -d r1 -f shop.sql12,2 s
Custompg_restore -d r2 shop.dump11,7 s
Directory, 4 Jobspg_restore -j 4 -d r3 shop.dir11,5 s

Alle drei Datenbanken hatten danach exakt 10.000.000 Zeilen. Zwei Dinge fallen auf.

Erstens: Der Restore dauert rund viermal so lange wie das Backup. Beim Sichern liest PostgreSQL nur, beim Wiederherstellen muss es alle Daten schreiben, Indizes neu bauen und Fremdschlüssel prüfen. Bei uns waren es 3 Sekunden gegen 12. Bei einer 100-GB-Datenbank wird aus einem Backup von zehn Minuten ein Restore von einer Dreiviertelstunde oder mehr. Das ist die Zahl, die du kennen musst, wenn jemand fragt, wie lange der Shop im Ernstfall offline ist.

Zweitens: Paralleles Wiederherstellen brachte bei einer großen Tabelle fast nichts, aus demselben Grund wie beim Dump.

So stellst du ein PostgreSQL Backup aus dem Custom-Format wieder her:

# Neue, leere Datenbank anlegen
createdb shop_restore

# Einspielen, mit Abbruch beim ersten Fehler
pg_restore --exit-on-error -d shop_restore shop.dump

# Nur eine einzelne Tabelle zurückholen
pg_restore -d shop_restore -t bestellungen shop.dump

# Bestehende Objekte vorher löschen (Vorsicht!)
pg_restore --clean --if-exists -d shop shop.dump

Falle: psql spielt trotz Fehlern weiter ein

Hier die wohl gemeinste Falle beim Plain-Format. Wir haben denselben SQL-Dump ein zweites Mal in dieselbe Datenbank eingespielt, also in eine, in der die Tabellen schon existierten. psql meldete zehn Fehler wie relation "pgbench_accounts" already exists, lief aber weiter bis zum Ende. Exit-Code: 0.

Ein Restore-Skript, das nur auf den Exit-Code schaut, hält das für einen Erfolg. Die Lösung ist eine einzige Option:

psql -v ON_ERROR_STOP=1 -d shop -f shop.sql

Damit bricht psql beim ersten Fehler ab und liefert Exit-Code 3. Wenn du Plain-SQL-Dumps einspielst, gehört diese Option in jedes Skript. Noch besser ist zusätzlich --single-transaction, dann wird bei einem Fehler alles zurückgerollt, statt eine halb gefüllte Datenbank zu hinterlassen.

Falle: ältere Version kann neueren Dump nicht lesen

pg_dump kann ältere Server sichern, und pg_restore kann ältere Dumps lesen, aber nicht umgekehrt. Wir haben unseren mit PostgreSQL 18 erzeugten Custom-Dump mit pg_restore aus PostgreSQL 16 geöffnet:

pg_restore: error: unsupported version (1.16) in file header

Das passiert typischerweise, wenn du auf dem Backup-Server ein älteres Client-Paket installiert hast als auf dem Datenbankserver, oder wenn du nach einem Upgrade zurück auf die alte Version willst. Faustregel: Für Backup und Restore immer die Client-Werkzeuge der neuesten beteiligten Version verwenden.

pg_basebackup: der physische Klon

pg_basebackup kopiert das ganze Datenverzeichnis, inklusive aller Datenbanken, Rollen und Einstellungen. Unsere Messung mit derselben 1,5-GB-Datenbank:

VarianteBefehlDauerGröße
Verzeichnis, unkomprimiertpg_basebackup -D /bk/base -X stream -c fast2,6 s1,6 GB
Tar mit gzippg_basebackup -D /bk/base -Ft -z -X stream -c fast13,7 s86 MB

Unkomprimiert ist pg_basebackup das schnellste Verfahren überhaupt, weil es nur Dateien kopiert und nichts umwandeln muss. Mit gzip wird es dagegen fünfmal langsamer, weil die Kompression in einem einzigen Prozess läuft. Seit PostgreSQL 15 kannst du mit --compress=server-zstd oder --compress=lz4 schnellere Verfahren wählen und die Arbeit auf den Server verlagern; das haben wir in diesem Lauf nicht gemessen.

Wichtige Optionen, die du kennen solltest:

  • -X stream nimmt die WAL-Dateien, die während des Backups entstehen, gleich mit. Ohne sie ist das Backup nicht startfähig.
  • -c fast erzwingt sofort einen Checkpoint. Ohne diese Option wartet pg_basebackup auf den nächsten regulären Checkpoint, was bis zu mehreren Minuten dauern kann, ohne dass etwas zu passieren scheint.
  • -P zeigt den Fortschritt an.

Voraussetzung ist, dass der Benutzer Replikationsrechte hat und in der pg_hba.conf eine Zeile für replication existiert. Bei den offiziellen Docker-Images ist das für lokale Verbindungen bereits der Fall.

Wann pg_basebackup statt pg_dump? Wenn die Datenbank so groß ist, dass ein Restore aus einem Dump zu lange dauert (ein physisches Backup ist sofort startfähig, es müssen keine Indizes neu gebaut werden), wenn du Point-in-Time-Recovery willst, oder wenn du einen Replikationsserver aufsetzen möchtest. Für eine Datenbank mit ein paar Gigabyte, die auf einem einzelnen Server läuft, ist pg_dump meistens die einfachere und flexiblere Wahl.

Point-in-Time-Recovery: zurück auf eine bestimmte Sekunde

Ein nächtlicher Dump hat ein grundsätzliches Problem: Passiert der Fehler um 16 Uhr, verlierst du alles seit drei Uhr nachts. Point-in-Time-Recovery (PITR) schließt diese Lücke. Das Prinzip: ein Basis-Backup plus alle WAL-Dateien seitdem. Beim Wiederherstellen spielt PostgreSQL die WAL-Dateien ab und hört an genau dem Zeitpunkt auf, den du angibst.

Eine Zeitleiste mit vielen kleinen Log-Segmenten, einer roten Bruchstelle und einer Markierung kurz davor als Bild für Point-in-Time-Recovery

Schritt 1: WAL-Archivierung einschalten

In der postgresql.conf:

wal_level = replica
archive_mode = on
archive_command = 'test ! -f /archiv/%f && cp %p /archiv/%f'

Das test ! -f verhindert, dass eine bereits archivierte Datei überschrieben wird. Das empfiehlt auch die offizielle Dokumentation. archive_mode erfordert einen Neustart des Servers, die anderen beiden nur ein Reload. In der Praxis zeigt das Archiv nicht auf einen lokalen Ordner, sondern auf einen anderen Rechner oder einen Objektspeicher. Dafür sind Werkzeuge wie pgBackRest, Barman oder WAL-G gebaut, dazu weiter unten mehr.

Schritt 2: Basis-Backup ziehen

pg_basebackup -D /bk/base -Ft -z -X stream -c fast

Schritt 3: Der Unfall

Unser Test: In die Datenbank haben wir eine Tabelle bestellungen mit 50 Einträgen geschrieben. Dann haben wir uns die Uhrzeit notiert (08:04:47 UTC), eine Sekunde gewartet und alle Bestellungen gelöscht. Danach kam noch eine neue Bestellung über 999 dazu, damit der Unfall nicht der letzte Vorgang ist, so wie im echten Leben, wo der Fehler oft erst auffällt, wenn schon weitere Daten geschrieben wurden.

Schritt 4: Wiederherstellen bis vor den Unfall

Basis-Backup in ein leeres Datenverzeichnis entpacken, dann in der postgresql.auto.conf (oder postgresql.conf):

restore_command = 'cp /archiv/%f %p'
recovery_target_time = '2026-11-03 08:04:47.698259+00'
recovery_target_action = 'promote'

Und eine leere Datei recovery.signal im Datenverzeichnis anlegen. Dann den Server starten. Im Log stand bei uns:

LOG:  starting point-in-time recovery to 2026-11-03 08:04:47.698259+00
LOG:  recovery stopping before commit of transaction 906, time 2026-11-03 08:04:48.787762+00
LOG:  redo done at 3/253F8F70 ... elapsed: 42.75 s
LOG:  selected new timeline ID: 2
LOG:  database system is ready to accept connections

Die Abfrage danach: 50 Bestellungen, höchster Betrag 50. Die gelöschten Daten waren zurück, die spätere Bestellung über 999 war, wie gewollt, nicht dabei. PostgreSQL hat genau vor der Transaktion angehalten, die gelöscht hatte.

Bemerkenswert sind die 42,75 Sekunden. Die Datenbank selbst ist nur 1,5 GB groß, aber seit dem Basis-Backup waren durch unsere Tests rund 13 GB WAL entstanden. Die Wiederherstellungszeit hängt also nicht von der Größe der Datenbank ab, sondern davon, wie viel seit dem letzten Basis-Backup geschrieben wurde. Deshalb gehört zu PITR immer auch ein regelmäßiges neues Basis-Backup, zum Beispiel jede Nacht oder jede Woche.

Wichtig: Die nach dem Zielzeitpunkt geschriebenen Daten (unsere 999er-Bestellung) sind im wiederhergestellten Server weg. Im echten Leben stellt man deshalb meist auf einem zweiten Server wieder her und kopiert nur die verlorenen Zeilen in die laufende Datenbank zurück.

Die stille Falle: das Archiv läuft voll oder bricht ab

Zwei Dinge, die uns beim Test aufgefallen sind und in vielen Anleitungen fehlen.

Das WAL-Archiv wächst mit dem Schreibvolumen, nicht mit der Datenbank. Unsere Datenbank war 1,5 GB groß, das Archiv nach einer knappen halben Stunde Testen 17 GB. Jede geänderte Zeile, jeder Restore, jede Indexänderung erzeugt WAL. Ohne Aufräumregel läuft dir das Archiv irgendwann die Platte voll. Werkzeuge wie pgBackRest löschen WAL-Dateien, die zu keinem aufbewahrten Basis-Backup mehr gehören, automatisch. Bei einer Eigenbaulösung brauchst du pg_archivecleanup.

Wenn das Archivieren scheitert, merkt die Anwendung nichts. Wir haben dem Archiv-Ordner die Schreibrechte entzogen und 20 Sekunden lang Last erzeugt. Ergebnis: 33 fehlgeschlagene Archivierungsversuche, das pg_wal-Verzeichnis wuchs von 1,1 auf 1,2 GB, aber pgbench lief mit unveränderten rund 6.600 Transaktionen pro Sekunde weiter. Kein Fehler beim Client, keine Warnung in der Anwendung. PostgreSQL hält die nicht archivierten WAL-Dateien einfach fest und versucht es weiter. Nachdem wir die Rechte zurückgegeben hatten, holte es alles von selbst nach.

Das ist gutmütig, aber gefährlich: Bleibt das Archiv tagelang kaputt, ist erstens deine Point-in-Time-Recovery seit Tagen nicht mehr möglich, und zweitens läuft irgendwann die Platte des Datenbankservers voll. Dann steht PostgreSQL. Überwache deshalb die Sicht pg_stat_archiver:

SELECT archived_count, failed_count, last_failed_wal, last_failed_time
FROM pg_stat_archiver;

Steigt failed_count, oder liegt last_archived_time mehr als ein paar Minuten zurück, stimmt etwas nicht.

Inkrementelle Backups seit PostgreSQL 17

Seit Version 17 kann pg_basebackup inkrementelle Backups erstellen, also nur die Datenblöcke sichern, die sich seit dem letzten Backup geändert haben. Voraussetzung ist summarize_wal = on.

# Vollbackup
pg_basebackup -D /bk/voll -X stream -c fast

# Später: nur die Änderungen
pg_basebackup -D /bk/inkr1 -X stream -c fast \
  --incremental=/bk/voll/backup_manifest

# Zum Wiederherstellen zu einem vollständigen Backup zusammensetzen
pg_combinebackup -o /bk/komplett /bk/voll /bk/inkr1

Unsere Messung: Vollbackup 1,6 GB. Danach haben wir 15 Sekunden lang pgbench laufen lassen, rund 100.000 Transaktionen. Das inkrementelle Backup dauerte 1,9 Sekunden und war 840 MB groß, also gut die Hälfte des Vollbackups. Das Zusammensetzen mit pg_combinebackup dauerte 2,6 Sekunden, das Ergebnis war wieder 1,6 GB groß und ließ sich fehlerfrei verifizieren.

Warum so groß? pgbench verändert zufällig verteilte Konten in der ganzen Tabelle, und schon eine geänderte Zeile macht den ganzen 8-KB-Block „geändert“. Bei einer typischen Webanwendung, die vor allem neue Zeilen anhängt und alte selten anfasst, ist ein inkrementelles Backup deutlich kleiner. Bei Last, die wild über die ganze Datenbank verteilt schreibt, spart es weniger, als man hofft. Miss es mit deinen echten Daten, bevor du deine Speicherplanung darauf aufbaust.

Backups prüfen: pg_verifybackup und der echte Restore-Test

Für physische Backups bringt PostgreSQL ein Prüfwerkzeug mit. pg_basebackup schreibt ein backup_manifest mit Prüfsummen aller Dateien, und pg_verifybackup vergleicht dagegen:

pg_verifybackup /bk/voll
# backup successfully verified

Wir haben testweise eine einzige Datei im Backup verändert, die winzige PG_VERSION. Die Prüfung schlug sofort an:

pg_verifybackup: error: "PG_VERSION" has size 10 on disk but size 3 in the manifest

Das ist nützlich gegen Bitfäule und unvollständige Übertragungen. Aber es beweist nicht, dass sich das Backup wiederherstellen lässt und deine Anwendung damit läuft. Das beweist nur ein echter Restore.

Eine Person prüft an einer Werkbank eine aus einem Tresor geholte Datenbank mit einer Lupe, daneben ein grünes Prüflicht

Die Falle, die das alles nötig macht

Hier der Fall, der uns am meisten zu denken gegeben hat, weil er in unzähligen Anleitungen genau so steht: pg_dump meinedb | gzip > backup.sql.gz. Wir haben uns beim Datenbanknamen vertippt (shopp statt shop):

pg_dump -U postgres shopp | gzip > /bk/falsch.sql.gz
echo $?
# 0
ls -l /bk/falsch.sql.gz
# 20 Bytes

pg_dump ist mit einem Fehler abgebrochen, aber der Exit-Code einer Pipe ist der Exit-Code des letzten Befehls, und gzip hat erfolgreich nichts komprimiert. Ergebnis: ein Backup-Skript, das jede Nacht „Erfolg“ meldet und eine 20 Byte große leere Datei hinterlässt. Das fällt erst auf, wenn du es brauchst.

Die Lösung in Bash ist set -o pipefail. Damit lieferte derselbe Befehl bei uns Exit-Code 1. Wer kein gzip in einer Pipe braucht, nimmt gleich pg_dump -Fc -f datei, dann gibt es keine Pipe und kein Problem.

Aus genau diesem Grund prüft ein gutes Backup-Skript nicht nur den Exit-Code, sondern auch, ob die Datei eine plausible Größe hat und ob sie sich lesen lässt. Und mindestens einmal im Monat sollte ein echter Restore in eine Testdatenbank laufen.

Ein vollständiges Backup-Skript für den Alltag

Hier ein Skript, das die Fallen von oben berücksichtigt. Es sichert Rollen und jede Datenbank einzeln, prüft die Ergebnisse und räumt alte Backups auf:

#!/bin/bash
# /usr/local/bin/pg-backup.sh
set -euo pipefail

ZIEL=/var/backups/postgres
TAG=$(date +%F_%H%M)
mkdir -p "$ZIEL/$TAG"

# Rollen und Passwörter
sudo -u postgres pg_dumpall --globals-only > "$ZIEL/$TAG/globals.sql"

# Jede Datenbank einzeln im Custom-Format mit zstd
for DB in $(sudo -u postgres psql -tAc \
  "SELECT datname FROM pg_database WHERE NOT datistemplate AND datname <> 'postgres'"); do
  sudo -u postgres pg_dump -Fc -Z zstd:3 -f "$ZIEL/$TAG/$DB.dump" "$DB"

  # Plausibilität: Datei lesbar und nicht winzig?
  sudo -u postgres pg_restore -l "$ZIEL/$TAG/$DB.dump" > /dev/null
  GROESSE=$(stat -c %s "$ZIEL/$TAG/$DB.dump")
  if [ "$GROESSE" -lt 10000 ]; then
    echo "WARNUNG: $DB.dump ist nur $GROESSE Bytes groß" >&2
    exit 1
  fi
done

# Alles älter als 14 Tage löschen
find "$ZIEL" -mindepth 1 -maxdepth 1 -type d -mmin +$((14*24*60)) -exec rm -rf {} +

Zwei Details dazu. pg_restore -l liest nur das Inhaltsverzeichnis des Dumps, das geht in Sekundenbruchteilen und erkennt abgeschnittene oder kaputte Dateien. Und für das Löschen nehmen wir -mmin statt -mtime, weil -mtime +14 in Wirklichkeit „mindestens 15 volle Tage“ bedeutet. Warum das so ist, haben wir in der Linux-Befehle-Liste gemessen.

Die Mindestgröße von 10.000 Bytes ist eine grobe Untergrenze. Passe sie an deine Datenbanken an, oder vergleiche besser mit der Größe des letzten Backups und schlage Alarm, wenn das neue plötzlich viel kleiner ist.

Automatisch laufen lassen

Per Cronjob:

15 2 * * * flock -n /run/pg-backup.lock /usr/local/bin/pg-backup.sh >> /var/log/pg-backup.log 2>&1

Oder, unsere Empfehlung, als systemd-Timer. Dann landen Fehler im Journal, und mit OnFailure= kannst du dich bei einem Fehlschlag benachrichtigen lassen. Wichtig ist nur, dass irgendjemand von einem Fehlschlag erfährt. Ein Backup-Job, der still scheitert, ist schlimmer als gar keiner, weil er dir falsche Sicherheit gibt.

Der monatliche Restore-Test

#!/bin/bash
set -euo pipefail
LETZTES=$(ls -1d /var/backups/postgres/*/ | tail -1)
sudo -u postgres dropdb --if-exists restore_test
sudo -u postgres createdb restore_test
sudo -u postgres pg_restore --exit-on-error --no-owner -d restore_test "$LETZTES/shop.dump"
sudo -u postgres psql -tAc "SELECT count(*) FROM bestellungen" restore_test
sudo -u postgres dropdb restore_test

Die letzte Abfrage ist bewusst eine fachliche: Stimmt die Zahl der Bestellungen ungefähr mit der Produktion überein? Ein Restore, der durchläuft, aber eine leere Tabelle liefert, ist der Fall, den du finden willst. Miss dabei auch die Zeit. Das ist deine realistische Wiederherstellungszeit.

PostgreSQL Backup in Docker

Läuft PostgreSQL in einem Container, ist das Vorgehen dasselbe, nur mit docker exec davor. Zwei Varianten:

# Dump im Container erzeugen, auf dem Host speichern (ohne -t!)
docker exec meine-db pg_dump -U postgres -Fc shop > /backup/shop.dump

# Oder in ein gemountetes Verzeichnis schreiben
docker exec meine-db pg_dump -U postgres -Fc -f /backup/shop.dump shop

Bei der ersten Variante darfst du docker exec nicht mit -t aufrufen. Ein Pseudo-Terminal verändert Zeilenumbrüche, und ein binärer Dump ist danach kaputt. Bei Plain-SQL fällt das oft nicht sofort auf.

Den Ordner mit den Datenbankdateien (das Volume) im laufenden Betrieb zu kopieren, ist auch in Docker kein Backup. Mehr zur Struktur von Volumes findest du in Was ist Docker und in unseren Docker-Compose-Beispielen.

Video (englisch): PostgreSQL Mechanics geht die wichtigsten Backup-Befehle durch (Mai 2026).

pgBackRest, Barman, WAL-G: wann ein eigenes Werkzeug?

Für eine Datenbank mit einigen Gigabyte reicht das Skript oben, eventuell ergänzt um einen Upload an einen zweiten Ort. Sobald du aber Point-in-Time-Recovery willst, wird die Eigenbaulösung schnell aufwendig: WAL-Dateien an einen anderen Ort übertragen, alte aufräumen, Basis-Backups rotieren, Prüfsummen, Verschlüsselung, parallele Übertragung. Genau dafür gibt es spezialisierte Werkzeuge:

WerkzeugStärkePasst für
pgBackRestVoll-, Differenz- und Inkrement-Backups, parallele Kompression, S3/Azure/GCS, eingebaute Aufbewahrung und Prüfungdie meisten Produktionsserver, de-facto-Standard
Barmanzentraler Backup-Server für viele PostgreSQL-Instanzen, von EDB entwickeltUmgebungen mit vielen Datenbankservern
WAL-Gschlank, direkt in Objektspeicher, auch für MySQL und andereCloud- und Container-Umgebungen
pg_dump + Skripteinfach, versionsübergreifend, keine Zusatzsoftwarekleine bis mittlere Datenbanken, Entwicklung

Diese Werkzeuge haben wir in diesem Artikel nicht selbst gemessen. Unsere Empfehlung beruht auf ihrer Verbreitung und darauf, dass sie genau die Probleme lösen, die wir oben von Hand nachgestellt haben: wachsendes Archiv, still scheiternde Archivierung, fehlende Prüfung.

Die richtige Backup-Strategie für deinen Server

Welche Methode passt, hängt vor allem an zwei Fragen: Wie viele Daten darfst du im schlimmsten Fall verlieren (Recovery Point Objective), und wie lange darf die Wiederherstellung dauern (Recovery Time Objective)?

Isometrische Darstellung: ein Datenbankserver, ein nächtlicher Sicherungsbehälter, ein laufender Strom aus Log-Segmenten in ein Archiv und eine Kopie an einen entfernten zweiten Standort

Hobbyprojekt, kleine App, internes Werkzeug: Täglicher pg_dump -Fc plus pg_dumpall --globals-only, 14 Tage aufbewahren, eine Kopie auf einen anderen Server. Möglicher Verlust: bis zu 24 Stunden.

Onlineshop, SaaS mit zahlenden Kunden: Nächtliches Basis-Backup plus kontinuierliche WAL-Archivierung mit pgBackRest an einen externen Ort. Zusätzlich ein wöchentlicher pg_dump als versionsunabhängige Rückfallebene. Möglicher Verlust: Sekunden bis wenige Minuten.

Hohe Verfügbarkeit gefordert: Zusätzlich ein Replikationsserver (Streaming Replication), auf den du im Fehlerfall umschalten kannst. Aber Achtung, eine Replik ist kein Backup: Ein versehentliches DELETE wird in Millisekunden auf die Replik übertragen. Gegen Bedienfehler hilft nur ein Backup mit Point-in-Time-Recovery.

Und für alle gilt die 3-2-1-Regel: drei Kopien, zwei verschiedene Medien, eine außer Haus. Ein Backup auf derselben Platte wie die Datenbank schützt vor einem versehentlichen DROP TABLE, aber nicht vor einem Plattendefekt, einem gehackten Server oder einem Brand im Rechenzentrum. Wie du das mit restic umsetzt, zeigen wir in der 3-2-1-Backup-Strategie. Ein RAID ersetzt übrigens ebenfalls kein Backup.

Häufig gestellte Fragen

Wie erstelle ich ein PostgreSQL Backup?

Für eine einzelne Datenbank mit pg_dump -Fc -f datei.dump datenbankname. Das erzeugt ein komprimiertes Backup im Custom-Format, das konsistent ist, auch wenn die Datenbank gerade benutzt wird. Zusätzlich solltest du mit pg_dumpall --globals-only die Rollen und Passwörter sichern, weil die nicht im Dump enthalten sind.

Was ist der Unterschied zwischen pg_dump und pg_dumpall?

pg_dump sichert eine einzelne Datenbank und unterstützt mehrere Formate inklusive Kompression und selektivem Restore. pg_dumpall sichert alle Datenbanken des Servers plus Rollen, aber nur als eine große SQL-Datei. Bewährt hat sich die Kombination: pg_dumpall --globals-only für die Rollen und pg_dump -Fc pro Datenbank.

Was ist der Unterschied zwischen pg_dump und pg_basebackup?

pg_dump ist ein logisches Backup: Es exportiert den Inhalt einer Datenbank so, dass er sich auch in einer anderen PostgreSQL-Version neu aufbauen lässt. pg_basebackup ist ein physisches Backup: eine exakte Kopie des gesamten Datenverzeichnisses, nur mit derselben Hauptversion nutzbar, aber sofort startfähig und Grundlage für Point-in-Time-Recovery.

Wie stelle ich ein PostgreSQL Backup wieder her?

Ein Custom-Dump wird mit pg_restore -d zieldatenbank datei.dump eingespielt, am besten mit --exit-on-error. Ein Plain-SQL-Dump mit psql -v ON_ERROR_STOP=1 -d zieldatenbank -f datei.sql. Ohne ON_ERROR_STOP meldet psql auch bei Fehlern Erfolg, in unserem Test Exit-Code 0 trotz zehn Fehlern.

Kann ich pg_dump im laufenden Betrieb verwenden?

Ja. pg_dump arbeitet mit einem festen Snapshot und liefert einen konsistenten Stand, auch unter Last. In unserem Test mit rund 6.500 Schreibtransaktionen pro Sekunde stimmten alle Kontrollsummen im Backup exakt überein. Es blockiert aber Schemaänderungen wie ALTER TABLE, bis der Dump fertig ist.

Wie oft sollte ich PostgreSQL sichern?

So oft, wie du Datenverlust verkraften kannst. Ein täglicher Dump bedeutet, dass im schlimmsten Fall die Daten eines Tages fehlen. Wenn das für dich zu viel ist, brauchst du WAL-Archivierung mit Point-in-Time-Recovery, dann liegt der mögliche Verlust bei Sekunden bis Minuten.

Was ist Point-in-Time-Recovery?

Point-in-Time-Recovery (PITR) stellt eine Datenbank auf einen beliebigen Zeitpunkt wieder her. Dafür braucht es ein physisches Basis-Backup und alle seitdem archivierten WAL-Dateien. PostgreSQL spielt die Änderungen bis zum gewünschten Zeitpunkt ab und hört dann auf. So lässt sich etwa ein versehentliches Löschen rückgängig machen.

Kann ich ein Backup einer neueren PostgreSQL-Version in einer älteren einspielen?

Nicht direkt. pg_restore aus PostgreSQL 16 lehnte unseren Dump aus Version 18 mit „unsupported version in file header“ ab. Der Weg nach unten geht nur über einen Plain-SQL-Dump und gegebenenfalls Handarbeit. Nach oben, also von alt nach neu, funktioniert es problemlos, wenn du die Werkzeuge der neueren Version verwendest.

Wie groß wird ein PostgreSQL Backup?

Ein unkomprimierter SQL-Dump ist meist etwas kleiner als die Datenbank, weil Indizes nicht als Daten, sondern als Befehle gesichert werden. Komprimiert schrumpft er stark: Unsere 1,5-GB-Testdatenbank ergab 933 MB als SQL, 27 MB mit gzip und 8 MB mit zstd. Echte Daten komprimieren schlechter als unsere sehr gleichförmigen Testdaten.

Ist eine Replikation ein Backup?

Nein. Eine Replik übernimmt jede Änderung des Hauptservers innerhalb von Millisekunden, also auch versehentliche Löschungen oder eine Verschlüsselung durch Ransomware. Sie schützt vor Hardwareausfall, nicht vor Fehlern. Dafür brauchst du zusätzlich echte Backups, idealerweise mit Point-in-Time-Recovery.

Brauche ich pgBackRest oder reicht pg_dump?

Für kleine und mittlere Datenbanken, bei denen ein Tag Datenverlust verschmerzbar ist, reicht pg_dump mit einem sauberen Skript. Sobald du Point-in-Time-Recovery, große Datenmengen oder Backups in einen Objektspeicher brauchst, nimmt dir pgBackRest sehr viel Handarbeit und Fehlerquellen ab.

Fazit: Das Backup ist leicht, der Beweis ist die Arbeit

Ein PostgreSQL Backup zu erstellen ist ein Einzeiler. pg_dump -Fc ist schnell, konsistent und für die meisten Projekte genau richtig, mit zstd sogar besonders klein. Für größere Datenbanken und wenn du auf die Sekunde zurückspringen willst, kommen pg_basebackup und WAL-Archivierung dazu, in der Praxis am besten über pgBackRest.

Die eigentliche Arbeit liegt woanders, und das zeigen unsere Messungen deutlicher als jede Theorie: Eine Pipe, die eine leere Datei als Erfolg meldet. Ein psql, das trotz zehn Fehlern Exit-Code 0 liefert. Rollen, die im Dump fehlen. Ein WAL-Archiv, das seit Stunden nicht schreibt, ohne dass die Anwendung es merkt. Nichts davon wirft eine Fehlermeldung, die dich nachts weckt.

Deshalb: set -o pipefail, ON_ERROR_STOP=1, pg_dumpall --globals-only, pg_stat_archiver überwachen, eine Kopie außer Haus. Und vor allem: einmal im Monat einen echten Restore machen und die Zeit stoppen. Ein Backup, das du nie wiederhergestellt hast, ist kein Backup, sondern eine Hoffnung.