G.STANCUTA
Pubblicato · 2026 · 03 · 228 min di lettura

Il tuo database sul tuo server

  • postgres
  • self-hosting
  • devops
  • infrastructure

I database gestiti ti fanno pagare una comodità di cui potresti non aver bisogno. Un Postgres self-hosted su un server bare-metal riduce la bolletta, elimina la latenza di rete e ti consegna la strategia di backup che avresti dovuto avere fin dall'inizio.

Ho spostato l'ultima istanza RDS gestita fuori dal mio stack quattordici mesi fa. La bolletta è scesa di oltre la metà. La latenza delle query è passata da pochi millisecondi a microsecondi. E dormo tranquillo, perché la pipeline di backup è qualcosa che posso leggere, testare e di cui mi fido.

Questo articolo spiega perché il Postgres self-hosted ha senso per la maggior parte dei carichi di lavoro in produzione al di sotto di una certa scala, come ragionare sui costi e come eseguire il backup in modo da sopravvivere a una perdita totale del server.

Il vero costo del gestito

I servizi di database gestiti come RDS, Cloud SQL e Neon ti vendono una promessa: non pensarci. Quella promessa ha un prezzo adeguato. Paghi per l'istanza, paghi un sovrapprezzo sullo storage, paghi per lo standby multi-AZ e poi, in silenzio, paghi per l'egress. Ogni gigabyte di dati che la tua applicazione legge dal servizio gestito e invia altrove costa denaro. Su RDS in us-east-1 sono circa $0,09/GB. Con un carico di analytics intenso, la cifra cresce prima che tu te ne accorga.

  • Sovrapprezzo sull'istanza: i servizi gestiti addebitano tipicamente da 2x a 3x il costo grezzo di calcolo rispetto a un VPS con specifiche equivalenti.
  • Premio sullo storage: i volumi RDS su EBS costano di più per GB rispetto a un dispositivo a blocchi dedicato su Hetzner o OVH.
  • Overhead per connessione: RDS limita le connessioni a livello di motore e ti spinge verso PgBouncer o RDS Proxy, che è un componente aggiuntivo a pagamento.
  • Costi di egress: il trasferimento di rete in uscita da un servizio gestito è a consumo; un server self-hosted sulla stessa LAN della tua app non paga nulla.
  • Prezzi degli snapshot: i backup automatici consumano storage S3 a prezzo pieno, fatturato separatamente.

Confronta tutto ciò con un Hetzner AX41 (AMD Ryzen 5, 64 GB RAM, 2x 512 GB NVMe in RAID-1) a circa €45/mese. Ospiti Postgres, la tua app e un reverse proxy sulla stessa macchina. Il traffico verso il database non esce mai dall'interfaccia di loopback.

Diagramma isometrico che confronta i costi di un database gestito con quelli di un server self-hosted
Il gestito aggiunge un sovrapprezzo a ogni livello. Il self-hosted li collassa.

Latenza su localhost vs un salto di rete

Quando la tua app e Postgres girano sulla stessa macchina, puoi connetterti tramite un socket Unix. Un socket Unix bypassa completamente lo stack TCP. Una query round-trip che richiede 1,2 ms su una connessione TCP in loopback impiega meno di 0,1 ms su un socket Unix. Non è un miglioramento marginale su un percorso critico con cinquanta query per richiesta.

Anche se esegui la tua app su un server separato e ti connetti tramite una LAN privata, batti comunque un servizio gestito che si trova in una zona di disponibilità diversa di almeno un salto di rete completo. AWS garantisce latenza intra-AZ inferiore a 1 ms, il che suona bene finché non realizzi che la tua app probabilmente colpisce il database centinaia di volte per ogni caricamento di pagina.

Il vendor lock-in e l'argomento della portabilità

I database gestiti si allontanano dallo standard. AWS Aurora è Postgres-compatibile, non Postgres. Ha il suo protocollo di replica, le sue peculiarità nel motore di storage, i suoi limiti sulle estensioni. Quando hai bisogno di pg_cron, timescaledb o un FDW personalizzato, il livello gestito ti blocca o ti fa pagare di più per una classe di istanza superiore che lo supporta.

Un Postgres 17 self-hosted è esattamente Postgres 17. Installi ciò che vuoi. Aggiorni quando decidi tu. Migri su un host diverso tramite dump e restore, che richiede pochi minuti per database inferiori a 50 GB.

L'obiezione sui backup, risposta concreta

Il rifiuto più comune: "i servizi gestiti gestiscono i backup automaticamente." Vero. Ma i backup automatici che non hai mai ripristinato non sono backup. Sono un conforto. La risposta giusta all'obiezione sui backup non è discutere di filosofia, ma mostrare la pipeline reale.

Ecco come appare un vero backup notturno. Comprime il dump con zstd (veloce, ottimo rapporto), lo cifra con age (semplice, basato su chiavi, senza cerimonie GPG) e lo invia a un bucket S3-compatibile offsite. Il ripristino è una singola pipeline che va nella direzione opposta.

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"

La pipeline di ripristino è l'inverso. Decifra, decomprimi, fai il pipe in psql. Dovresti eseguire questa operazione su un database di staging almeno una volta al mese. Non è opzionale.

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."

Lasciare che un agente AI gestisca il database

Un agente AI che lavora sul tuo stack deve conoscere il database come farebbe un nuovo ingegnere: dove si trova, come connettersi, qual è il programma di backup e quali sono i comandi sicuri. La differenza è che un agente dimentica tra una sessione e l'altra, a meno che tu non gli fornisca una memoria persistente.

La soluzione pratica è un breve file markdown committato nel repository. L'agente lo legge all'inizio di ogni sessione e ha il quadro operativo completo. Nessuna rispiegazione del percorso del socket, nessuna supposizione sul nome del bucket di backup, nessuna query distruttiva eseguita accidentalmente in produzione perché l'agente ha assunto il DATABASE_URL sbagliato.

Schema di un agente AI che legge un file markdown di memoria per gestire un database self-hosted
Un file di contesto markdown fornisce all'agente una memoria operativa persistente.

Ecco come appare nella pratica quel file per una configurazione 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;

Con questo file nel repository e referenziato nel tuo CLAUDE.md o AGENTS.md principale, ogni sessione agente inizia con un quadro operativo completo. Conosce il percorso del socket, i permessi utente, il programma di backup, lo strumento di migrazione e le insidie. Può eseguire migrazioni, controllare lo stato dei backup o indagare una query lenta senza che tu debba rispiegare la configurazione ogni volta.

Il file funge anche da documentazione di onboarding per gli ingegneri umani. Scrivilo una volta, mantienilo accurato e entrambi i tipi di utenti ne beneficiano.

Cosa ti serve davvero per farlo girare

Il carico operativo del Postgres self-hosted è reale ma contenuto. Devi gestire gli aggiornamenti del sistema operativo, gli upgrade delle versioni di Postgres e il monitoraggio. Niente di tutto ciò è esotico.

  • Monitoraggio: pg_stat_statements più un sidecar Prometheus postgres_exporter. La dashboard Grafana richiede venti minuti per essere configurata.
  • Connection pooling: PgBouncer in modalità transazione. Un file di configurazione, si avvia in pochi minuti, gestisce migliaia di connessioni concorrenti.
  • Replica: la replica streaming verso un secondo server è semplice da Postgres 10 in poi. Per la maggior parte dei carichi di lavoro, il tuo backup offsite è sufficiente e più semplice.
  • Sicurezza: l'autenticazione peer di pg_hba.conf sui socket Unix significa nessuna esposizione di rete per le connessioni locali. Blocca la porta 5432 con ufw o il firewall del tuo VPS.
  • Upgrade: pg_upgrade gestisce gli upgrade delle versioni principali in-place. Testa prima su un clone. Pianifica una finestra di manutenzione di meno di un'ora.

La complessità del self-hosting di Postgres è un pomeriggio di configurazione e un file markdown che mantiene il sistema leggibile. La complessità dei database gestiti è una dashboard di fatturazione con dodici voci che controlli solo quando qualcosa va storto.

Gestisci il tuo database. Leggi i log una volta. Costruisci la pipeline di backup, testa il ripristino, committa l'AGENTS.md. Dopo di che, gira e basta.

Portfolio · Cartiglio
Disegnato da
G. STANCUTA
Disciplina
AI & AUTOMATION
Luogo
MORTER · SÜDTIROL
Stato
Disponibile
Lingue
IT · EN · RO · DE+
Stack
PLOI · HETZNER
Revisione
REV 2026.A
2026

© 2026 Gabriel Stancuta · jumpinotech.com — Progettato con l'AI, costruito per funzionare da solo.