← Blog

Automatische Postgres-Backups, die du wirklich wiederherstellen kannst

Railyard

15. Mai 2026 · 11 Min. Lesezeit

Folge dem auf deinem eigenen Server. Kostenlos starten, keine Karte nötig

Ein Cron-Job, der jede Nacht pg_dump ausführt und irgendwo eine Datei schreibt, ist kein Backup-System. Es wird an dem Tag zu einem Backup-System, an dem jemand wirklich daraus wiederherstellen muss – und keinen Tag früher. Viele Teams lernen das auf die harte Art: Der Cron-Eintrag ist vor drei Monaten wegen einer PATH-Änderung, die niemand bemerkt hat, stillschweigend ausgefallen, oder der nächtliche Dump schreibt seit Wochen eine 40-Byte-Fehlermeldung in eine .dump-Datei statt eine Datenbank, oder die Backups liegen direkt neben der Datenbank, die sie eigentlich schützen sollen, und beide verschwinden beim selben Serverausfall. Eine Datei auf der Festplatte, die noch nie jemand wiederhergestellt hat, ist eine Vermutung, kein Backup.

Am Ende dieses Tutorials hast du ein echtes System: ein geskriptetes pg_dump im richtigen Format, einen Cron-Zeitplan, der es wirklich ausführt, den Dump, der an einen Speicherort außerhalb des Servers geschickt wird, eine Aufbewahrungsrichtlinie, die alte Kopien bereinigt, ohne dich bei null Backups zurückzulassen, und – am wichtigsten – einen geplanten Job, der dein neuestes Backup in eine Wegwerf-Datenbank wiederherstellt und prüft, ob es tatsächlich gut ist. Dieser letzte Teil ist der Unterschied zwischen einem Backup und einer Hoffnung.

Was du brauchst

  • Einen Postgres-Server, den du kontrollierst: einen VPS, eine dedizierte Maschine oder einen beliebigen selbst gehosteten Server mit Postgres, auf dem pg_dump, pg_restore, psql und createdb/dropdb verfügbar sind.
  • Ein Ziel für die Dumps außerhalb des Servers: S3-kompatiblen Objektspeicher (AWS S3, DigitalOcean Spaces, Backblaze B2, Cloudflare R2 oder ähnlich) sowie entweder die AWS CLI oder rclone, konfiguriert mit den passenden Zugangsdaten. Der ganze Sinn dieser Übung ist, dass das Backup nicht auf derselben Festplatte liegt wie die Datenbank – diesen Schritt zu überspringen, macht das ganze System zunichte.
  • Shell-Zugriff, um Cron-Jobs auf dem Postgres-Server (oder wo auch immer du geplante Aufgaben ausführst) einzuplanen.
  • Genug freien Festplattenplatz auf dem Server, damit ein Dump lokal landen kann, bevor er verschickt wird, und noch einmal denselben Spielraum für eine Wiederherstellungs-Testdatenbank im Verifizierungsschritt.

Ein Domainname ist dabei nicht nötig. Du sicherst Daten, nicht Traffic.

Schritt 1: Den Dump skripten

Verwende das benutzerdefinierte Format von pg_dump (-Fc, entspricht --format=custom). Es ist standardmäßig komprimiert und, anders als ein reiner SQL-Dump, genau das, was pg_restore erwartet – das bedeutet selektive Wiederherstellungen und ein Format, das pg_restore später bei Bedarf parallelisieren kann.

Füge --no-owner und --no-acl hinzu, damit der Dump keine ALTER OWNER- und GRANT/REVOKE-Anweisungen mitschleppt, die an Rollen gebunden sind, die dort, wo du letztlich wiederherstellst (einschließlich deiner Verifizierungsdatenbank in Schritt 5), möglicherweise nicht existieren oder andere Namen haben.

#!/usr/bin/env bash
set -euo pipefail

DB_NAME="myapp_production"
DB_USER="myapp"
BACKUP_DIR="/var/backups/postgres"
TIMESTAMP="$(date +%Y%m%d%H%M%S)"
DUMP_FILE="${BACKUP_DIR}/${DB_NAME}_${TIMESTAMP}.dump"

mkdir -p "${BACKUP_DIR}"

pg_dump -Fc --no-owner --no-acl \
  -U "${DB_USER}" -d "${DB_NAME}" \
  -f "${DUMP_FILE}"

if [ ! -s "${DUMP_FILE}" ]; then
  echo "pg_dump produced an empty file, aborting" >&2
  exit 1
fi

echo "Backed up ${DB_NAME} to ${DUMP_FILE}"

set -euo pipefail ist hier entscheidend: Ohne es kann ein fehlgeschlagenes pg_dump das Skript trotzdem weiterlaufen und Erfolg melden lassen. Die Prüfung [ ! -s "${DUMP_FILE}" ] fängt genau den Fall ab, in dem pg_dump sauber beendet wird, die geschriebene Datei aber leer ist – was häufiger passiert, als man denkt, wenn die Zielfestplatte voll ist.

Du solltest eine neue Datei in /var/backups/postgres sehen, deren Größe ungefähr der tatsächlichen Größe deiner Datenbank entspricht (ein Dump im custom-Format ist komprimiert, erwarte also keine 1:1-Übereinstimmung, aber wenn eine 500-MB-Datenbank eine 2-KB-Datei erzeugt, ist das ein Zeichen, dass etwas nicht stimmt).

Schritt 2: Mit cron planen

Die Crontab-Felder stehen in dieser Reihenfolge: Minute, Stunde, Tag des Monats, Monat, Wochentag. Ein nächtlicher Lauf um 2:00 Uhr sieht so aus:

0 2 * * * /usr/local/bin/pg_backup.sh >> /var/log/pg_backup.log 2>&1

Zwei Details, die für die meisten “meine Backups sind lautlos gestoppt”-Vorfälle verantwortlich sind:

  • cron läuft mit einer minimalen Umgebung, nicht mit der deiner interaktiven Shell. Wenn dein Skript pg_dump, aws oder rclone namentlich aufruft und sich auf den PATH deiner Shell verlässt, um sie zu finden, enthält der abgespeckte PATH von cron das Verzeichnis, in dem sie liegen, möglicherweise nicht. Verwende absolute Pfade im Skript (/usr/bin/pg_dump, /usr/local/bin/aws) oder setze PATH oben im Skript explizit.
  • Leite sowohl stdout als auch stderr um in eine Log-Datei (das >> ... 2>&1 oben). Ohne das mailt cron Fehlschläge an ein Postfach, das niemand liest, oder verwirft sie je nach Mail-Konfiguration deines Systems lautlos – und du erfährst es erst, wenn eine Wiederherstellung fehlschlägt.

Du solltest jede Nacht eine neue Zeile in /var/log/pg_backup.log sehen und eine neue .dump-Datei in /var/backups/postgres mit einem frischen Zeitstempel. Wenn ein Tag vergeht ohne neue Datei und ohne neue Log-Zeile, hat cron den Job gar nicht ausgeführt – das ist ein anderer Fehlerfall als ein Job, der läuft und fehlschlägt.

Schritt 3: Den Dump vom Server wegschicken

Ein Dump, der auf derselben Festplatte wie die Datenbank liegt, die er sichert, schützt dich nicht vor dem Fehlerfall, der am wichtigsten ist: dem Verlust des Servers. Schiebe ihn sofort nach dem Schreiben irgendwohin anders.

Mit der AWS CLI, gegen S3 oder einen S3-kompatiblen Endpunkt:

aws s3 cp "${DUMP_FILE}" "s3://my-backups-bucket/postgres/" --only-show-errors

Für einen Nicht-AWS S3-kompatiblen Anbieter füge --endpoint-url hinzu:

aws s3 cp "${DUMP_FILE}" "s3://my-backups-bucket/postgres/" \
  --endpoint-url "https://nyc3.digitaloceanspaces.com" \
  --only-show-errors

Oder mit rclone, nachdem du einmal ein Remote mit rclone config eingerichtet hast:

rclone copy "${DUMP_FILE}" "myremote:my-backups-bucket/postgres/"

Hänge welchen Befehl auch immer ans Ende deines Backup-Skripts und prüfe seinen Exit-Code genauso, wie du den von pg_dump geprüft hast:

if ! aws s3 cp "${DUMP_FILE}" "s3://my-backups-bucket/postgres/" --only-show-errors; then
  echo "upload to S3 failed for ${DUMP_FILE}" >&2
  exit 1
fi

Du solltest das Objekt in deinem Bucket sehen (aws s3 ls s3://my-backups-bucket/postgres/ oder das Äquivalent im Dashboard deines Anbieters), kurz nachdem der Cron-Job fertig ist, mit einer Größe, die der lokalen Datei entspricht.

Schritt 4: Alte Backups bereinigen

Ohne eine Aufbewahrungsrichtlinie wachsen dein Backup-Verzeichnis und dein Bucket unendlich weiter. Mit einer zu aggressiven Richtlinie kannst du dich genau dann auf null Backups herunterbereinigen, wenn du eines brauchst. Die sicherere Regel hat zwei Teile: behalte mindestens eine Mindestanzahl aktueller Backups unabhängig von ihrem Alter, und lösche separat alles, was älter als N Tage ist und nicht zu den behaltenen gehört.

Lokal:

#!/usr/bin/env bash
set -euo pipefail

BACKUP_DIR="/var/backups/postgres"
KEEP_DAYS=30
KEEP_MIN=7

mapfile -t ALL_DUMPS < <(ls -1t "${BACKUP_DIR}"/*.dump 2>/dev/null)

for i in "${!ALL_DUMPS[@]}"; do
  if [ "$i" -lt "${KEEP_MIN}" ]; then
    continue
  fi
  file="${ALL_DUMPS[$i]}"
  if [ -n "$(find "${file}" -mtime "+${KEEP_DAYS}")" ]; then
    rm -f "${file}"
    echo "pruned ${file}"
  fi
done

ls -1t listet Dumps neueste zuerst auf, sodass die Schleife die aktuellsten KEEP_MIN-Dateien unbedingt überspringt und nur ältere löscht, die auch die Altersprüfung nicht bestehen. Das bedeutet: Eine ruhige Woche, in der nichts Neues entsteht, lässt dich nie mit weniger als KEEP_MIN Backups zurück.

Für die entfernte Kopie unterstützen die meisten S3-kompatiblen Anbieter eine Bucket-Lifecycle-Regel, die Objekte nach N Tagen abläuft lässt – das ist einfacher, als das Bereinigen remote selbst zu skripten; setze den Wert mindestens so hoch wie KEEP_DAYS, damit du nie die einzige Kopie außerhalb des Servers wegbereinigst.

Führe das als zweiten Cron-Eintrag aus, getrennt vom Backup-Skript, damit ein Bereinigungsfehler nicht versehentlich innerhalb desselben Skripts läuft, das gerade versucht, ein frisches Backup zu erstellen:

30 2 * * * /usr/local/bin/pg_prune.sh >> /var/log/pg_backup.log 2>&1

Du solltest sehen, wie alte .dump-Dateien mit der Zeit aus /var/backups/postgres verschwinden, während mindestens KEEP_MIN aktuelle immer bestehen bleiben, und eine “pruned …”-Zeile im Log, wann immer eine entfernt wird.

Schritt 5: Wiederherstellen und verifizieren, nach Zeitplan

Das ist der Schritt, den fast jeder überspringt, und genau der, der wirklich wichtig ist. Eine existierende Dump-Datei mit einer Größe größer null sagt dir, dass pg_dump gelaufen ist. Sie sagt dir nichts darüber, ob die Daten darin intakt sind, ob das Format gültig ist oder ob eine Wiederherstellung tatsächlich funktionieren würde, wenn du sie brauchst. Der einzige Weg, zu wissen, dass ein Backup gut ist, ist es wiederherzustellen und zu prüfen, was zurückkommt.

Automatisiere das gegen eine Wegwerf-Datenbank auf demselben Server (oder einem separaten, falls du lieber keine Wiederherstellungslast auf die Produktion legen willst):

#!/usr/bin/env bash
set -euo pipefail

BACKUP_DIR="/var/backups/postgres"
LATEST_DUMP="$(ls -1t "${BACKUP_DIR}"/*.dump | head -n1)"
VERIFY_DB="restore_check_$(date +%Y%m%d)"
MIN_EXPECTED_ROWS=1

createdb "${VERIFY_DB}"

pg_restore --no-owner --no-acl -d "${VERIFY_DB}" "${LATEST_DUMP}"

ROW_COUNT="$(psql -d "${VERIFY_DB}" -tAc "SELECT count(*) FROM users;")"

dropdb "${VERIFY_DB}"

if [ "${ROW_COUNT}" -lt "${MIN_EXPECTED_ROWS}" ]; then
  echo "restore verification failed: ${ROW_COUNT} rows in users table" >&2
  exit 1
fi

echo "restore verified: ${LATEST_DUMP}, ${ROW_COUNT} rows in users table"

Tausche users gegen eine Tabelle, für die du realistisch eine Mindestzeilenzahl behaupten kannst; der Sinn der Abfrage ist nicht, erschöpfend zu sein, sondern zu belegen, dass die Wiederherstellung eine funktionierende Datenbank mit echten Daten erzeugt hat, nicht nur eine Datenbank, die existiert. pg_restore selbst schlägt auch laut bei einem wirklich beschädigten oder abgeschnittenen Dump fehl, sodass schon ein von null abweichender Exit-Code aus dem Wiederherstellungsschritt allein ein nützliches Signal ist, getrennt von der Zeilenzahl-Prüfung.

Plane es nach deinem Aufbewahrungsjob ein, und seltener als das Backup selbst ist sinnvoll – wöchentlich ist für die meisten Teams ein vernünftiger Ausgangspunkt:

0 3 * * 0 /usr/local/bin/pg_restore_check.sh >> /var/log/pg_backup.log 2>&1

Du solltest jede Woche eine Zeile “restore verified: …” im Log sehen, mit einer Zeilenzahl, die für deine Daten plausibel aussieht. Wenn diese Zeile aufhört zu erscheinen oder anfängt, eine verdächtig niedrige Zeilenzahl zu melden, hast du ein schlechtes Backup erkannt, während du noch ältere gute als Rückfallebene hast, statt es erst während eines echten Ausfalls herauszufinden.

Die Abkürzung: mit Railyard

Alles oben ist die ehrliche From-Scratch-Variante. Railyard führt dieselbe Schleife für dich aus, für jede Postgres-Datenbank, die es verwaltet, ohne die Skripte.

Wenn du eine Datenbank auf einem von Railyard verwalteten Server provisionierst, plant es automatisch eigene Backups in einem Intervall, das du pro Datenbank anpassen kannst (ein Tag ist für die meisten Apps ein vernünftiger Standard). Jedes Backup wird von Railyard tatsächlich durch Wiederherstellung und Prüfung verifiziert, nicht nur durch die Kontrolle, dass eine Datei existiert – du siehst also auf einen Blick, ob dein jüngstes Backup gut ist, statt es anzunehmen. Wiederherstellen ist ein Klick von der Seite der Datenbank aus, kein Skript zum Merken und keine Wegwerf-Datenbank, die du danach händisch aufräumen musst. Das Bereinigen wird genau so gehandhabt, wie du es selbst haben wolltest: Railyard behält eine konfigurierbare Anzahl deiner jüngsten erfolgreichen Backups und entfernt alles, was älter als eine konfigurierbare Anzahl Tage ist und nicht zu den behaltenen gehört – du bist also nie eine einzige schlechte Bereinigung von nichts entfernt.

Speziell für Postgres kannst du auch Point-in-Time-Recovery pro Server auf der Serverseite aktivieren, was geplante Dumps ergänzt: statt nur zum Moment deines letzten Backups wiederherstellen zu können, kannst du zu jedem Moment innerhalb des Wiederherstellungsfensters zurückkehren. Mehr dazu, einschließlich wie Datenbanken provisioniert und verbunden werden, findest du unter Datenbanken und Backups.

Wenn du eine neue App startest statt Backups nachträglich in eine bestehende einzubauen, decken der Quickstart und Deployen aus einem Repository ab, wie du die App und ihre Datenbank überhaupt erst auf einem Server zum Laufen bringst.

Problembehandlung

Der Cron-Job lief früher und jetzt nicht mehr, ohne jeden Fehler. Das ist fast immer das PATH-Problem aus Schritt 2: Ein Skript, das beim interaktiven Testen einwandfrei funktionierte, schlägt unter der minimalen Umgebung von cron lautlos fehl, weil pg_dump, aws oder rclone nicht dort liegen, wo cron sie erwartet. Verwende für jedes vom Skript aufgerufene Binary absolute Pfade, oder setze PATH oben im Skript explizit, und bestätige, dass der Job wirklich läuft, indem du den Zeitstempel der Log-Datei prüfst – nicht nur, dass kein Fehler aufgetaucht ist.

Die Dump-Datei existiert, ist aber leer oder abgeschnitten. Die häufigste Ursache ist eine volllaufende Zielfestplatte mitten im Schreibvorgang, besonders wenn das Backup-Verzeichnis sich eine Festplatte mit dem eigenen Datenverzeichnis der Datenbank oder einer wachsenden Log-Datei teilt. pg_dump kann in diesem Fall mit einem von null abweichenden Status beenden, weshalb das Skript genau deshalb den Exit-Code und die Dateigröße separat prüft, statt nur einem der beiden zu vertrauen.

Niemand hat je eine Wiederherstellung getestet, und die Backups waren monatelang lautlos defekt. Das ist genau das Szenario, das Schritt 5 verhindern soll. Ein beschädigter oder unvollständiger Dump kann in deinem Bucket genauso aussehen wie ein gesunder, solange niemand versucht, ihn zu benutzen. Den Wiederherstellungs-und-Verifizierungs-Job einzuplanen, statt ihn nur als etwas zu dokumentieren, das man irgendwann manuell machen sollte, schließt diese Lücke tatsächlich.

Zugangsdaten für das Ziel außerhalb des Servers sind abgelaufen oder wurden rotiert. Ein S3-Zugriffsschlüssel oder die Zugangsdaten eines rclone-Remotes können unabhängig von allem auf dem Postgres-Server ablaufen oder widerrufen werden, und der Upload-Schritt beginnt zu scheitern, während der lokale Dump weiter erfolgreich bleibt. Den Exit-Code des Upload-Befehls zu prüfen (Schritt 3) fängt das sofort ab, statt erst Wochen später, wenn du nach einer Kopie außerhalb des Servers suchst, die nie tatsächlich geschrieben wurde.

FAQ

Wie oft sollten Backups laufen? Das hängt davon ab, wie viele Daten du dir leisten kannst zu verlieren – dein Recovery Point Objective. Täglich ist für die meisten Anwendungen ein vernünftiger Standard. Wenn sich deine Daten schnell genug ändern, dass der Verlust von bis zu einem Tag ein echtes Problem wäre, führe pg_dump entweder häufiger aus oder füge WAL-Archivierung für Point-in-Time-Recovery zwischen den Dumps hinzu.

Reicht pg_dump, oder brauche ich WAL-Archivierung und Point-in-Time-Recovery? pg_dump gibt dir einen sauberen, konsistenten Snapshot genau zum Zeitpunkt der Ausführung. Kontinuierliche WAL-Archivierung (archive_mode, archive_command und periodische Basis-Backups mit pg_basebackup) lässt dich Änderungen mit recovery_target_time bis zu jedem beliebigen Moment vorwärts abspielen, sodass du bis kurz vor eine fehlerhafte Migration oder ein versehentliches Löschen wiederherstellen kannst, statt nur bis zum Dump der letzten Nacht. Für die meisten Teams deckt geplantes pg_dump-Backup den Normalfall ab (Serververlust, Beschädigung), und Point-in-Time-Recovery lohnt sich erst, wenn schon der Verlust einiger Stunden Daten zwischen Dumps wirklich wehtun würde.

Wie weiß ich, dass ein Backup wirklich gut ist? Nur eine erfolgreiche Wiederherstellung beweist es. Dass eine Datei existiert, eine plausible Größe hat und pg_dump mit Status 0 beendet, ist jeweils notwendig, aber nicht hinreichend; der einzige echte Test ist, es irgendwo wiederherzustellen und zu prüfen, dass die zurückkommenden Daten dem entsprechen, was du erwartest – genau das automatisiert Schritt 5.

Wo sollte die Kopie außerhalb des Servers eigentlich liegen? Irgendwo, das nicht dieselbe Festplatte ist, idealerweise nicht einmal derselbe Anbieter oder dieselbe Region wie der Datenbankserver, damit ein einzelner Anbieterausfall oder Account-Problem nicht gleichzeitig deine Datenbank und ihre Backups lahmlegen kann.

Muss ich die Dump-Dateien verschlüsseln? Wenn dein Objektspeicher serverseitige Verschlüsselung im Ruhezustand unterstützt, kostet das Aktivieren nichts und lohnt sich. Wenn du Verschlüsselung unabhängig vom Speicheranbieter willst, kannst du den Dump vor dem Versand durch gpg leiten, was allerdings einen Schlüssel hinzufügt, den du nun auch sicher sichern musst – ein eigenes Risiko, über das es sich lohnt nachzudenken, bevor du es hinzufügst.

Ein Backup, das du nie wiederhergestellt hast, ist ein Glaube, keine Tatsache. Die fünf Schritte hier machen aus diesem Glauben etwas, das du tatsächlich nach Zeitplan verifizierst, damit das erste Mal, dass du von einem Backup wiederherstellst, nicht auch das erste Mal ist, dass du erfährst, ob es funktioniert.

Warum Leute sich dafür Railyard entscheiden

  • Dein eigenes Cloud-Konto. Deploye auf DigitalOcean, Hetzner, Vultr, AWS, OVHcloud oder jedem Linux-Server per SSH, direkt von deinem Anbieter abgerechnet, ohne Aufschlag.
  • Ein fester Preis pro Team. Free ist für immer kostenlos: ein Server plus ein Datenbankserver, drei Apps, keine Karte. Pro kostet 19 $ im Monat für bis zu fünf Server und unbegrenzte Apps. Team kostet 49 $ im Monat mit unbegrenzten Servern. Keine Kosten pro App, pro Deployment, pro Sitz oder pro Gigabyte.
  • Templates mit einem Klick. Deploye Open-Source-Apps, wie Datenbanken, Analytics und Dev-Tools, mit einem Klick auf deinem eigenen Server.
  • Verwaltete Datenbanken. Postgres, MySQL und Redis mit geplanten, wiederherstellungsgeprüften Backups und Point-in-Time-Recovery.
  • Push-to-Deploy. Deploye von GitHub, GitLab oder der CLI, mit automatischem HTTPS und Releases ohne Ausfallzeit.

Lass Railyard die gesamte Backup-Schleife übernehmen.

Hänge einen Server an, und jede Postgres-Datenbank darauf bekommt geplante, durch Wiederherstellung verifizierte Backups mit Ein-Klick-Recovery – keine Skripte zu warten.

Kostenloser Plan, keine Kreditkarte, und 14 Tage Pro für jedes neue Team. Ein fester Preis pro Team.