uptime · 1413 days · 24 posts published · last deploy 1 day, 15 hours ago build:passing rss
~ / homelab / postgres-backup-self-hosted-pg-dump-bis-pitr.md
Homelab · 11. Juni 2026 · ~14min · 5c0282f

Postgres-Backup für Self-Hosted Apps: pg_dump bis PITR

Logische Dumps, WAL-Archiving, Off-Site-Verschlüsselung & echte Restore-Tests

>
devmaker.net
author · 5c0282f · 2026-06-11
Postgres-Backup Hero.jpg 1024×1024
Postgres-Backup Hero
Postgres-Backup im Self-Hosted-Setup: Dump, Off-Site, PITR
Ein Self-Hosted-Setup steht und fällt mit dem Backup – genauer: mit dem, das sich auch zurückspielen lässt. Wer seine App auf einem Postgres im Docker-Container betreibt, hat schnell einen pg_dump-Cronjob gebaut und fühlt sich sicher. Bis der erste echte Restore ansteht. Dieser Artikel zeigt mein Postgres-Backup-Setup aus dem laufenden Homelab: vom simplen logischen Dump über inkrementelles WAL-Archiving bis zu Point-in-Time-Recovery, verschlüsselt und off-site. Du brauchst Docker- und Basis-Linux-Kenntnisse – am Ende hast du ein Backup, dem du vertraust, weil du den Restore automatisiert testest.

Warum pg_dump allein nicht reicht

Fast jedes Self-Hosted-Setup beginnt beim Backup gleich: ein pg_dump in einem nächtlichen Cronjob, das Ergebnis irgendwo auf der gleichen Platte. Das ist besser als nichts – aber es deckt nur den einfachsten Fehlerfall ab. Drei Probleme bleiben offen:

  • Granularität: Ein täglicher Dump heißt im schlimmsten Fall 24 Stunden Datenverlust. Für eine Wissensdatenbank okay, für eine App mit echten Nutzereingaben nicht.
  • Off-Site: Liegt das Backup auf derselben Maschine, ist es bei Plattendefekt, Ransomware oder einem versehentlichen docker volume rm genauso weg wie die Datenbank.
  • Vertrauen: Ein Backup, das nie zurückgespielt wurde, ist eine Vermutung, kein Backup.

Ich zeige hier ein dreistufiges Setup, das genau diese drei Lücken schließt – bewusst pragmatisch, nicht überengineered.

Mein Setup

Eine Django-App, Postgres als postgres:16-alpine im Docker-Container, Daten in einem benannten Volume. Nichts Exotisches – genau das Setup, das die meisten Homelab-Apps fahren:

services:
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: appdb
      POSTGRES_USER: app
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./wal_archive:/wal_archive
    restart: unless-stopped

volumes:
  pgdata:

Das ./wal_archive-Bind-Mount brauchen wir erst für Stufe 3 – ich nehme es gleich mit auf, damit der Container dafür nicht neu gestartet werden muss.

Stufe 1: Logischer Dump mit pg_dump

Der logische Dump ist die Basis: portabel, versionsunabhängig genug und ideal für den Normalfall „ich brauche die Datenbank von gestern Nacht zurück“. Das Skript dumpt aus dem Container, komprimiert direkt und räumt alte lokale Kopien weg:

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

STAMP=$(date +%F_%H%M)
OUT="/srv/backups/appdb_${STAMP}.sql.gz"

docker exec -t db \
  pg_dump --no-owner --no-privileges -U app appdb \
  | gzip -9 > "$OUT"

# nur die letzten 7 Tage lokal vorhalten
find /srv/backups -name 'appdb_*.sql.gz' -mtime +7 -delete

--no-owner und --no-privileges erleichtern später den Restore in eine frische DB, in der die Rollen anders heißen können. Dann ab in die Crontab:

30 2 * * *  /srv/scripts/pg_backup.sh >> /var/log/pg_backup.log 2>&1

Stufe 2: Off-Site & verschlüsselt mit restic

Lokale Dumps retten dich beim Bedienfehler, nicht beim Hardware- oder Standort-Totalschaden. restic schiebt die Dumps verschlüsselt zu einem entfernten Ziel (S3-kompatibler Bucket, anderer Server, externe Platte) und dedupliziert dabei, sodass du viele Versionen für wenig Speicher vorhältst:

export RESTIC_REPOSITORY="s3:https://s3.example.com/mein-backup-bucket"
export RESTIC_PASSWORD_FILE="/etc/restic/password"
# Bucket-Credentials kommen aus der Umgebung / einem Secrets-File,
# niemals ins Skript hardcoden.

restic backup /srv/backups

# Retention: 7 täglich, 5 wöchentlich, 12 monatlich – Rest wird gelöscht
restic forget \
  --keep-daily 7 --keep-weekly 5 --keep-monthly 12 \
  --prune
Verschlüsselung ist nicht optional

restic verschlüsselt das Repository immer. Aber das Passwort liegt damit zwischen dir und deinen Daten: Geht es verloren, ist das Backup wertlos. Sichere es getrennt vom Backup-Ziel – z. B. im Passwortmanager.

Stufe 3: WAL-Archiving für Point-in-Time-Recovery

Hier wird es interessant. Mit Write-Ahead-Log-(WAL-)Archiving sicherst du nicht nur Snapshots, sondern jede einzelne Transaktion. Damit kannst du auf jeden Zeitpunkt zwischen zwei Base-Backups zurückspielen – etwa „eine Minute, bevor das fehlerhafte Migrationsskript lief“. In der postgresql.conf (bzw. als Command-Args im Container):

wal_level = replica
archive_mode = on
archive_command = 'test ! -f /wal_archive/%f && cp %p /wal_archive/%f'
archive_timeout = 300   # spätestens alle 5 Min ein WAL-Segment schreiben

Dazu regelmäßig ein Base-Backup als Ausgangspunkt – pg_basebackup zieht einen konsistenten Stand des gesamten Clusters:

docker exec -t db \
  pg_basebackup -U app -D - -Ft -X fetch \
  | gzip > /srv/backups/base_$(date +%F).tar.gz

Restore-Logik: das Base-Backup auspacken, das wal_archive als restore_command eintragen, ein recovery.signal setzen – Postgres spielt dann beim Start die WALs bis zum gewünschten recovery_target_time ein. Den genauen Ablauf gibt es im nächsten Abschnitt.

Brauchst du PITR wirklich?

WAL-Archiving lohnt sich, wenn Datenverlust in Minuten schmerzt. Für ein Blog oder eine Wissensbasis reichen Stufe 1+2 oft völlig. Ehrlich abwägen statt aus Prinzip alles aufzusetzen – PITR will auch betrieben und getestet werden.

Restore: der Teil, den alle überspringen

Der häufigste Restore ist der einfachste: einen logischen Dump in eine frische Datenbank zurückspielen. Erst eine leere DB anlegen, dann den Dump einlesen:

docker exec -i db psql -U app -c 'CREATE DATABASE appdb_restore;'

gunzip -c /srv/backups/appdb_2026-06-11_0230.sql.gz \
  | docker exec -i db psql -U app -d appdb_restore

Wichtig: in eine neue DB restoren, nicht über die laufende bügeln. So kannst du den wiederhergestellten Stand erst prüfen und erst danach umschalten. Erst wenn der Restore-Pfad einmal sauber durchgelaufen ist, weißt du, dass dein Dump überhaupt vollständig war.

Der Restore-Test als Cronjob

Das ist der Schritt, der aus „ich habe Backups“ ein „ich habe ein Backup, dem ich vertraue“ macht. Ein Wegwerf-Container zieht den neuesten Dump, spielt ihn ein und prüft mit einem Sanity-Check, ob überhaupt Daten drin sind. Fällt der Test durch, soll es laut sein:

#!/usr/bin/env bash
set -euo pipefail
LATEST=$(ls -t /srv/backups/appdb_*.sql.gz | head -1)

# Wegwerf-Postgres, isoliert vom Produktiv-Container
docker run --rm -d --name pg_verify \
  -e POSTGRES_PASSWORD=verify postgres:16-alpine
sleep 10

docker exec -i pg_verify psql -U postgres -c 'CREATE DATABASE verify;'
gunzip -c "$LATEST" | docker exec -i pg_verify psql -U postgres -d verify

# Sanity-Check: hat die wichtigste Tabelle Zeilen?
ROWS=$(docker exec -t pg_verify psql -U postgres -d verify -tAc \
  'SELECT count(*) FROM auth_user;')
docker stop pg_verify

if [ "${ROWS//[[:space:]]/}" -gt 0 ]; then
  echo "Restore-Test OK ($ROWS Zeilen in auth_user)"
else
  echo "Restore-Test FEHLGESCHLAGEN – Backup prüfen!" >&2
  exit 1
fi

Dieses Skript wöchentlich per Cron, das Ergebnis in dein Monitoring – und du erfährst von einem kaputten Backup, bevor du es brauchst, nicht mittendrin im Notfall.

Monitoring: weiß ich, wenn's klemmt?

Ein Backup-Job, der still scheitert, ist gefährlicher als gar keiner – weil du dich in Sicherheit wähnst. Die billigste Lösung ist ein Dead-Man-Switch: Das Skript pingt am Ende einen Healthcheck-Dienst an; bleibt der Ping aus, schlägt der Dienst Alarm. Eine Zeile am Skriptende reicht:

# nur bei Erfolg pingen (am Ende des Backup-Skripts, set -e sorgt für Abbruch davor)
curl -fsS -m 10 --retry 3 https://hc.example.com/ping/DEIN-CHECK-UUID > /dev/null

Was ich weggelassen habe

  • Streaming-Replikation / Hot-Standby: Hochverfügbarkeit ist ein anderes Problem als Backup. Für ein Homelab ist eine zweite Live-Instanz Overkill – und ersetzt Backups ohnehin nicht (ein DROP TABLE repliziert mit).
  • pgBackRest / barman: Mächtige Tools mit eingebautem PITR und Retention. Wer mehrere Cluster betreibt, sollte sie ansehen. Für eine App war mir die Kombi aus pg_dump + restic transparenter und leichter zu debuggen.
  • Backup der Secrets: Das beste DB-Backup nützt nichts, wenn .env, restic-Passwort und Compose-Files nicht ebenfalls (getrennt!) gesichert sind. Das gehört in einen eigenen, ehrlichen Artikel.

Fazit

Drei Stufen, sauber gestaffelt: pg_dump für den Alltag, restic für off-site und verschlüsselt, WAL-Archiving, wenn dir Minuten wehtun. Der eigentliche Gewinn steckt aber nicht im Backup, sondern im automatisierten Restore-Test – er ist der Unterschied zwischen Hoffnung und Gewissheit. Im nächsten Artikel nehme ich mir die Secrets- und Volume-Sicherung vor, damit nicht nur die Datenbank, sondern das ganze Setup reproduzierbar zurückkommt.

// responses (0)
> echo "your thoughts" >> postgres-backup-self-hosted-pg-dump-bis-pitr.responses

Schreibe einen Kommentar

Wird für die Bestätigung benötigt