G.STANCUTA
Publicat · 2026 · 03 · 228 min de citit

Baza ta de date pe propriul server

  • postgres
  • self-hosting
  • devops
  • infrastructure

Bazele de date gestionate te fac să plătești pentru o comoditate de care poate nu ai nevoie. Un Postgres self-hosted pe un server bare-metal îți reduce factura, elimină latența de rețea și îți oferă strategia de backup pe care ar fi trebuit să o ai oricum.

Am mutat ultima instanță RDS gestionată din stack-ul meu acum paisprezece luni. Factura a scăzut cu mai mult de jumătate. Latența interogărilor a trecut de la câteva milisecunde la microsecunde. Și dorm liniștit, pentru că pipeline-ul de backup este ceva pe care îl pot citi, testa și în care am încredere.

Acest articol explică de ce Postgres self-hosted are sens pentru majoritatea sarcinilor de lucru în producție sub o anumită scară, cum să gândești calculul costurilor și exact cum să faci backup într-un mod care supraviețuiește unei pierderi totale a serverului.

Costul real al gestionat

Serviciile de baze de date gestionate precum RDS, Cloud SQL și Neon îți vând o promisiune: nu te gândi la asta. Acea promisiune are un preț pe măsură. Plătești pentru instanță, plătești un adaos la stocare, plătești pentru standby multi-AZ și apoi, în liniște, plătești pentru egress. Fiecare gigaoctet de date pe care aplicația ta îl citește din serviciul gestionat și îl trimite în altă parte costă bani. Pe RDS în us-east-1, asta înseamnă aproximativ $0,09/GB. La un workload de analytics intens, suma se acumulează înainte să o observi.

  • Adaos la instanță: serviciile gestionate percep de obicei de 2x până la 3x costul brut de calcul față de un VPS cu specificații echivalente.
  • Adaos la stocare: volumele RDS pe EBS costă mai mult per GB decât un dispozitiv de bloc dedicat pe Hetzner sau OVH.
  • Overhead per conexiune: RDS limitează conexiunile la nivel de motor și te împinge spre PgBouncer sau RDS Proxy, care este un add-on cu plată.
  • Taxe de egress: transferul de rețea din afara unui serviciu gestionat este măsurat; un server self-hosted în aceeași rețea LAN cu aplicația ta nu plătește nimic.
  • Prețuri snapshot: backup-urile automate consumă stocare S3 la prețul de listă, facturate separat.

Compară asta cu un Hetzner AX41 (AMD Ryzen 5, 64 GB RAM, 2x 512 GB NVMe în RAID-1) la aproximativ €45/lună. Găzduiești Postgres, aplicația și un reverse proxy pe aceeași mașină. Traficul bazei de date nu iese niciodată din interfața de loopback.

Diagramă izometrică ce compară costurile bazelor de date gestionate cu costurile serverului self-hosted
Gestionatul adaugă adaosuri la fiecare nivel. Self-hosted le combină.

Latența localhost vs un salt de rețea

Când aplicația ta și Postgres rulează pe aceeași mașină, te poți conecta printr-un socket Unix. Un socket Unix ocolește complet stiva TCP. O interogare round-trip care durează 1,2 ms printr-o conexiune TCP în loopback durează sub 0,1 ms printr-un socket Unix. Nu este o îmbunătățire marginală pe un drum critic cu cincizeci de interogări per cerere.

Chiar dacă rulezi aplicația pe un server separat și te conectezi printr-un LAN privat, tot bați un serviciu gestionat dintr-o zonă de disponibilitate diferită cu cel puțin un salt de rețea complet. AWS garantează latență intra-AZ sub 1 ms, ceea ce sună bine până când realizezi că aplicația ta lovește probabil baza de date de sute de ori la fiecare încărcare de pagină.

Blocajul furnizorului și argumentul portabilității

Bazele de date gestionate se abat de la standard. AWS Aurora este compatibil cu Postgres, nu este Postgres. Are propriul protocol de replicare, propriile particularități ale motorului de stocare, propriile limitări la extensii. Când ai nevoie de pg_cron, timescaledb sau un FDW personalizat, nivelul gestionat fie te blochează, fie îți percepe extra pentru o clasă de instanță superioară care îl suportă.

Un Postgres 17 self-hosted este exact Postgres 17. Instalezi ce vrei. Faci upgrade când decizi tu. Migrezi pe un alt host prin dump și restore, ceea ce durează câteva minute pentru baze de date sub 50 GB.

Obiecția privind backup-urile, răspuns concret

Cel mai frecvent contraargument: "serviciile gestionate gestionează backup-urile automat." Adevărat. Dar backup-urile automate pe care nu le-ai restaurat niciodată nu sunt backup-uri. Sunt un confort. Răspunsul corect la obiecția privind backup-urile nu este să dezbați filozofie, ci să arăți pipeline-ul real.

Iată cum arată un backup nocturn real. Comprimă dump-ul cu zstd (rapid, raport excelent), îl criptează cu age (simplu, bazat pe chei, fără ceremonia GPG) și îl trimite într-un bucket S3-compatibil extern. Restaurarea este un singur pipeline în direcția opusă.

bash
#!/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"

Pipeline-ul de restaurare este inversul. Decriptează, decomprimă, trimite prin pipe în psql. Ar trebui să rulezi asta pe o bază de date de staging cel puțin o dată pe lună. Nu este opțional.

bash
#!/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."

Să lași un agent AI să opereze baza de date

Un agent AI de coding care lucrează pe stack-ul tău trebuie să știe despre baza de date la fel cum o face un nou inginer: unde se află, cum să se conecteze, care este programul de backup și care sunt comenzile sigure. Diferența este că un agent uită între sesiuni, dacă nu îi oferi memorie persistentă.

Soluția practică este un fișier markdown scurt committit în repository. Agentul îl citește la începutul fiecărei sesiuni și are imaginea operațională completă. Nicio re-explicare a căii socket-ului, nicio ghicire a numelui bucket-ului de backup, nicio interogare distructivă rulată accidental în producție pentru că agentul a presupus DATABASE_URL-ul greșit.

Schemă a unui agent AI care citește un fișier markdown de memorie pentru a opera o bază de date self-hosted
Un fișier de context markdown oferă agentului memorie operațională persistentă.

Iată cum arată acel fișier în practică pentru o configurație Postgres self-hosted:

md
# 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;

Cu acest fișier în repository și referit în CLAUDE.md sau AGENTS.md principal, orice sesiune agent începe cu o imagine operațională completă. Cunoaște calea socket-ului, permisiunile utilizatorilor, programul de backup, instrumentul de migrare și capcanele. Poate rula migrări, verifica starea backup-urilor sau investiga o interogare lentă fără să reexplici configurația de fiecare dată.

Fișierul servește și ca documentație de onboarding pentru inginerii umani. Scrie-l o dată, menține-l precis, și ambele categorii de utilizatori beneficiază.

De ce ai nevoie cu adevărat pentru a rula asta

Costul operațional al Postgres self-hosted este real dar mic. Trebuie să gestionezi actualizările sistemului de operare, upgrade-urile versiunilor Postgres și monitorizarea. Nimic din toate astea nu este exotic.

  • Monitorizare: pg_stat_statements plus un sidecar Prometheus postgres_exporter. Dashboard-ul Grafana durează douăzeci de minute să fie configurat.
  • Connection pooling: PgBouncer în modul tranzacție. Un fișier de configurare, pornește în câteva minute, gestionează mii de conexiuni concurente.
  • Replicare: replicarea în flux către un al doilea server este simplă începând cu Postgres 10. Pentru majoritatea sarcinilor de lucru, backup-ul tău extern este suficient și mai simplu.
  • Securitate: autentificarea peer din pg_hba.conf pe socket-uri Unix înseamnă nicio expunere de rețea pentru conexiunile locale. Blochează portul 5432 cu ufw sau firewall-ul VPS-ului tău.
  • Upgrade-uri: pg_upgrade gestionează upgrade-urile de versiune majoră in-place. Testează mai întâi pe o clonă. Planifică o fereastră de mentenanță de sub o oră.

Complexitatea self-hosting-ului Postgres este o după-amiază de configurare și un fișier markdown care menține sistemul lizibil. Complexitatea bazelor de date gestionate este un dashboard de facturare cu doisprezece poziții pe care îl verifici doar când ceva nu merge.

Rulează propria bază de date. Citește log-urile o dată. Construiește pipeline-ul de backup, testează restaurarea, committează AGENTS.md. După aceea, rulează și gata.

Portofoliu · Indicator
Desenat de
G. STANCUTA
Disciplină
AI & AUTOMATION
Locație
MORTER · SÜDTIROL
Stare
Disponibil
Limbi
IT · EN · RO · DE+
Stack
PLOI · HETZNER
Revizie
REV 2026.A
2026

© 2026 Gabriel Stancuta · jumpinotech.com — Proiectat cu AI, construit să funcționeze singur.