Deine Datenbank auf deinem eigenen Server
- postgres
- self-hosting
- devops
- infrastructure
Managed-Datenbanken berechnen dir Komfort, den du vielleicht gar nicht brauchst. Ein selbst gehostetes Postgres auf einem Bare-Metal-Server senkt deine Kosten, eliminiert Netzwerklatenz und gibt dir die Backup-Strategie, die du sowieso schon hättest haben sollen.
Ich habe die letzte verwaltete RDS-Instanz vor vierzehn Monaten aus meinem Stack entfernt. Die Rechnung sank um mehr als die Hälfte. Die Query-Latenz fiel von einstelligen Millisekunden auf Mikrosekunden. Und ich schlafe gut, weil die Backup-Pipeline etwas ist, das ich lesen, testen und dem ich vertrauen kann.
Dieser Beitrag erklärt, warum selbst gehostetes Postgres für die meisten Produktions-Workloads unterhalb einer bestimmten Größenordnung sinnvoll ist, wie man die Kostenrechnung durchdenkt und wie man Backups so einrichtet, dass man einen vollständigen Serververlust übersteht.
Die wahren Kosten von Managed
Managed-Datenbankdienste wie RDS, Cloud SQL und Neon verkaufen dir ein Versprechen: Kümmere dich nicht darum. Dieses Versprechen hat seinen Preis. Du zahlst für die Instanz, du zahlst einen Aufschlag auf den Speicher, du zahlst für Multi-AZ-Standby, und dann zahlst du still und leise für Egress. Jedes Gigabyte Daten, das deine Anwendung aus dem verwalteten Dienst liest und woanders hinschickt, kostet Geld. Auf RDS in us-east-1 sind das etwa $0,09/GB. Bei einem intensiven Analytics-Workload summiert sich das, bevor du es merkst.
- Instanz-Aufschlag: Managed-Dienste berechnen typischerweise das 2- bis 3-fache der reinen Rechenkosten im Vergleich zu einem VPS mit gleichen Spezifikationen.
- Speicher-Aufschlag: EBS-basierte RDS-Volumes kosten mehr pro GB als ein dediziertes Block-Device bei Hetzner oder OVH.
- Overhead pro Verbindung: RDS drosselt Verbindungen auf Engine-Ebene und drängt dich zu PgBouncer oder RDS Proxy, das ein kostenpflichtiges Add-on ist.
- Egress-Gebühren: Netzwerktransfer aus einem verwalteten Dienst wird gemessen; ein selbst gehosteter Server im selben LAN wie deine App zahlt nichts.
- Snapshot-Preise: Automatisierte Backups verbrauchen S3-Speicher zum Listenpreis, separat abgerechnet.
Vergleiche das mit einem Hetzner AX41 (AMD Ryzen 5, 64 GB RAM, 2x 512 GB NVMe in RAID-1) für etwa €45/Monat. Du hostest Postgres, deine App und einen Reverse Proxy auf derselben Maschine. Der Datenbankverkehr verlässt niemals das Loopback-Interface.

Localhost-Latenz vs. ein Netzwerkhop
Wenn deine App und Postgres auf derselben Maschine laufen, kannst du dich über einen Unix-Socket verbinden. Ein Unix-Socket umgeht den TCP-Stack vollständig. Eine Round-Trip-Query, die über eine Loopback-TCP-Verbindung 1,2 ms dauert, braucht über einen Unix-Socket unter 0,1 ms. Das ist keine marginale Verbesserung auf einem heißen Pfad mit fünfzig Queries pro Request.
Selbst wenn du deine App auf einem separaten Server betreibst und über ein privates LAN verbindest, schlägst du immer noch einen Managed-Dienst in einer anderen Verfügbarkeitszone um mindestens einen vollständigen Netzwerkhop. AWS garantiert eine Intra-AZ-Latenz unter 1 ms, was gut klingt, bis du erkennst, dass deine App die Datenbank wahrscheinlich hunderte Male pro Seitenaufruf trifft.
Vendor-Lock-in und das Portabilitätsargument
Managed-Datenbanken weichen ab. AWS Aurora ist Postgres-kompatibel, nicht Postgres. Es hat sein eigenes Replikationsprotokoll, seine eigenen Storage-Engine-Eigenheiten, seine eigenen Einschränkungen bei Erweiterungen. Wenn du pg_cron, timescaledb oder ein benutzerdefiniertes FDW benötigst, blockiert dich der verwaltete Tier oder berechnet extra für eine höhere Instanzklasse, die es unterstützt.
Ein selbst gehostetes Postgres 17 ist genau Postgres 17. Du installierst, was du möchtest. Du upgradest, wenn du es entscheidest. Du migrierst zu einem anderen Host durch Dump und Restore, was bei Datenbanken unter 50 GB Minuten dauert.
Der Backup-Einwand, konkret beantwortet
Der häufigste Einwand: "Managed-Dienste erledigen Backups automatisch." Stimmt. Aber automatisierte Backups, die du nie wiederhergestellt hast, sind keine Backups. Sie sind ein Trost. Die richtige Antwort auf den Backup-Einwand ist nicht, Philosophie zu diskutieren, sondern die tatsächliche Pipeline zu zeigen.
So sieht ein echtes nächtliches Backup aus. Es komprimiert den Dump mit zstd (schnell, ausgezeichnetes Verhältnis), verschlüsselt ihn mit age (einfach, schlüsselbasiert, ohne GPG-Zeremonie) und schickt ihn in einen externen S3-kompatiblen Bucket. Das Restore ist eine einzelne Pipeline in die entgegengesetzte Richtung.
#!/usr/bin/env bash
# backup-pg.sh — nightly Postgres dump + compress + encrypt + upload
# Runs as the postgres system user via cron or systemd timer.
# Requires: pg_dump, zstd, age, rclone (configured with a remote called "b2")
set -euo pipefail
DB_NAME="${DB_NAME:-myapp}"
BACKUP_DIR="/var/backups/postgres"
DATE=$(date -u +%Y-%m-%dT%H%M%SZ)
FILENAME="${DB_NAME}_${DATE}.sql.zst.age"
AGE_PUBKEY="${AGE_PUBKEY:?set AGE_PUBKEY env var to recipient public key}"
mkdir -p "$BACKUP_DIR"
echo "[backup] dumping $DB_NAME..."
pg_dump \
--host=/var/run/postgresql \
--username=postgres \
--format=plain \
--no-password \
"$DB_NAME" \
| zstd -T0 -3 \
| age -r "$AGE_PUBKEY" \
> "$BACKUP_DIR/$FILENAME"
echo "[backup] uploading to offsite..."
rclone copy "$BACKUP_DIR/$FILENAME" "b2:myapp-backups/postgres/"
echo "[backup] pruning local copies older than 7 days..."
find "$BACKUP_DIR" -name "*.age" -mtime +7 -delete
echo "[backup] done: $FILENAME"Die Restore-Pipeline ist die Umkehrung. Entschlüsseln, dekomprimieren, in psql pipen. Du solltest dies mindestens einmal im Monat gegen eine Staging-Datenbank ausführen. Nicht optional.
#!/usr/bin/env bash
# restore-pg.sh — decrypt + decompress + restore a Postgres backup
# Usage: ./restore-pg.sh backup_file.sql.zst.age target_db_name
# Requires: age, zstd, psql, age identity key at ~/.config/age/key.txt
set -euo pipefail
BACKUP_FILE="${1:?usage: restore-pg.sh <backup.sql.zst.age> <target_db>}"
TARGET_DB="${2:?usage: restore-pg.sh <backup.sql.zst.age> <target_db>}"
AGE_IDENTITY="${AGE_IDENTITY:-$HOME/.config/age/key.txt}"
if [[ ! -f "$BACKUP_FILE" ]]; then
echo "error: backup file not found: $BACKUP_FILE" >&2
exit 1
fi
echo "[restore] creating database $TARGET_DB if not exists..."
psql --host=/var/run/postgresql --username=postgres \
-c "CREATE DATABASE \"$TARGET_DB\" WITH TEMPLATE template0;" || true
echo "[restore] restoring from $BACKUP_FILE into $TARGET_DB..."
age --decrypt --identity "$AGE_IDENTITY" "$BACKUP_FILE" \
| zstd --decompress --stdout \
| psql \
--host=/var/run/postgresql \
--username=postgres \
--dbname="$TARGET_DB" \
--single-transaction \
--set ON_ERROR_STOP=on
echo "[restore] done."Einen KI-Agenten die Datenbank bedienen lassen
Ein KI-Coding-Agent, der an deinem Stack arbeitet, muss die Datenbank genauso kennen wie ein neuer Ingenieur: wo sie läuft, wie man sich verbindet, wie der Backup-Zeitplan aussieht und was die sicheren Befehle sind. Der Unterschied ist, dass ein Agent zwischen Sitzungen vergisst, es sei denn, du gibst ihm persistentes Gedächtnis.
Die praktische Lösung ist eine kurze Markdown-Datei, die ins Repository committet wird. Der Agent liest sie zu Beginn jeder Sitzung und hat das vollständige Betriebsbild. Kein erneutes Erklären des Socket-Pfads, kein Raten des Backup-Bucket-Namens, kein versehentliches Ausführen einer destruktiven Query in der Produktion, weil der Agent die falsche DATABASE_URL angenommen hat.

So sieht diese Datei in der Praxis für ein selbst gehostetes Postgres-Setup aus:
# AGENTS.md — Database Operations
## Connection
- Engine: Postgres 17, running on localhost
- Socket: /var/run/postgresql (use this, not TCP)
- App user: myapp_user (limited to SELECT/INSERT/UPDATE/DELETE on myapp schema)
- Superuser: postgres (for migrations and admin only — never use in app code)
- DATABASE_URL: postgresql:///myapp?host=/var/run/postgresql (app user, Unix socket)
## Schema
- Migrations managed by Flyway: files in /db/migrations/, prefix V{version}__description.sql
- Run migrations: flyway -url="jdbc:postgresql:///myapp?socketFactory=..." migrate
- Never edit an existing migration file. Always create a new one.
## Backups
- Schedule: nightly at 02:00 UTC via systemd timer (pg-backup.timer)
- Script: /opt/scripts/backup-pg.sh
- Local retention: 7 days in /var/backups/postgres/
- Offsite: Backblaze B2 bucket myapp-backups/postgres/
- Encryption: age, recipient key in /etc/backup/age-pubkey.txt
- Restore script: /opt/scripts/restore-pg.sh <file> <target_db>
- **Test restores monthly.** Last tested: 2026-03-01, succeeded.
## Safe Commands
```bash
# Check Postgres status
systemctl status postgresql
# Tail live query log (caution: verbose)
tail -f /var/log/postgresql/postgresql-17-main.log
# Manual backup now
sudo -u postgres /opt/scripts/backup-pg.sh
# Connect as app user
psql "postgresql:///myapp?host=/var/run/postgresql" -U myapp_user
```
## Gotchas
- Do NOT run VACUUM FULL during business hours; it takes an exclusive lock.
- pg_hba.conf uses peer auth for local Unix connections; no password needed for postgres system user.
- Extensions installed: uuid-ossp, pg_stat_statements, pg_cron (cron.database_name = myapp).
- pg_cron jobs are in the myapp schema, table cron.job. List with: SELECT * FROM cron.job;
Mit dieser Datei im Repository und einem Verweis in deiner CLAUDE.md oder AGENTS.md startet jede Agentensitzung mit einem vollständigen Betriebsbild. Er kennt den Socket-Pfad, die Benutzerberechtigungen, den Backup-Zeitplan, das Migrations-Tool und die Fallstricke. Er kann Migrationen ausführen, den Backup-Status prüfen oder eine langsame Query untersuchen, ohne dass du das Setup jedes Mal neu erklären musst.
Die Datei dient auch als Onboarding-Dokumentation für menschliche Ingenieure. Schreibe sie einmal, halte sie aktuell, und beide Zielgruppen profitieren davon.
Was du wirklich brauchst, um das zu betreiben
Der Betriebsaufwand für selbst gehostetes Postgres ist real, aber gering. Du musst OS-Updates, Postgres-Versions-Upgrades und Monitoring handhaben. Nichts davon ist exotisch.
- Monitoring:
pg_stat_statementsplus ein Prometheuspostgres_exporterSidecar. Das Grafana-Dashboard ist in zwanzig Minuten eingerichtet. - Connection Pooling: PgBouncer im Transaktionsmodus. Eine Konfigurationsdatei, startet in Minuten, verarbeitet tausende gleichzeitiger Verbindungen.
- Replikation: Streaming-Replikation auf einen zweiten Server ist ab Postgres 10 unkompliziert. Für die meisten Workloads reicht dein externes Backup aus und ist einfacher.
- Sicherheit:
pg_hba.confPeer-Auth auf Unix-Sockets bedeutet keine Netzwerkexposition für lokale Verbindungen. Sperre Port 5432 mitufwoder der Firewall deines VPS. - Upgrades:
pg_upgradeerledigt Major-Version-Upgrades in-place. Teste zuerst auf einem Klon. Plane ein Wartungsfenster von unter einer Stunde.
Die Komplexität von selbst gehostetem Postgres ist ein Nachmittag Einrichtung und eine Markdown-Datei, die das System lesbar hält. Die Komplexität von Managed-Datenbanken ist ein Abrechnungs-Dashboard mit zwölf Positionen, das du nur prüfst, wenn etwas schiefläuft.
Betreibe deine eigene Datenbank. Lies die Logs einmal durch. Baue die Backup-Pipeline, teste das Restore, committe die AGENTS.md. Danach läuft es einfach.