Production-Ready PostgreSQL on a VPS: Automated Backups with pgBackRest, Patroni HA, and Docker
Managed database cloud services (AWS RDS, Google Cloud SQL, Supabase Cloud) charge eye-watering markups on RAM, storage IOPS, and data egress. A modest 16 GB RAM managed database cluster often costs upwards of $250–$600/month.
On high-performance NVMe cloud VPS providers (like Hetzner, Vultr, or DigitalOcean), the same hardware costs €15–$35/month—a 90%+ infrastructure discount.
However, running production PostgreSQL on your own server requires three non-negotiables:
- Automated WAL Archiving & Point-In-Time Recovery (PITR) (pgBackRest)
- Connection Pooling & Query Routing (PgBouncer)
- Automated Failover & High Availability (Patroni / Raft)
Here is the blueprint for a bulletproof, production-grade self-hosted PostgreSQL setup.
Architecture Blueprint
[ Application Traffic ]
│
▼
[ PgBouncer (Port 6432) ] ───> Connection pooling, transaction reuse
│
▼
[ PostgreSQL 17 (Port 5432) ] ───> Primary DB Instance
│
├───> [ WAL Stream (Continuous) ] ───> [ pgBackRest ] ───> [ Encrypted S3 / MinIO / Garage ]
│
└───> [ Streaming Replication ] ───> [ Standby Replica ] (Patroni Managed)
1. Automated Point-in-Time Recovery with pgBackRest
Standard pg_dump is insufficient for production databases over 50 GB because it locks tables, impacts CPU, and cannot restore to a specific second before a disastrous DROP TABLE query.
pgBackRest provides:
- Page-level differential and incremental backups
- Real-time Write-Ahead Log (WAL) archiving to S3 or object storage
- Point-In-Time-Recovery (PITR) with zero data loss
- Parallel compression (Zstandard/LZ4) and encryption at rest
Minimal Production pgbackrest.conf:
[global]
repo1-type=s3
repo1-s3-endpoint=s3.eu-central-003.backblazeb2.com
repo1-s3-bucket=my-production-pg-backups
repo1-s3-region=eu-central-003
repo1-s3-key=YOUR_S3_ACCESS_KEY
repo1-s3-key-secret=YOUR_S3_SECRET_KEY
repo1-cipher-type=aes-256-cbc
repo1-cipher-pass=YOUR_SUPER_SECURE_PASSPHRASE
repo1-retention-full=4
repo1-retention-diff=14
process-max=4
compress-type=zst
compress-level=6
log-level-console=info
log-level-file=detail
[maindb]
pg1-path=/var/lib/postgresql/data
pg1-user=postgres
pg1-port=5432
Automated Backup Cron / Systemd Schedule:
-
Full Backup: Every Sunday at 02:00 UTC (
pgbackrest --stanza=maindb --type=full backup) -
Differential Backup: Daily at 02:00 UTC (
pgbackrest --stanza=maindb --type=diff backup) -
Continuous WAL Archive: Triggered automatically via
archive_command = 'pgbackrest --stanza=maindb archive-push %p'
2. Fast Connection Pooling with PgBouncer
PostgreSQL spawns a separate OS process for each client connection (~10 MB RAM per connection). Under spike loads, hundreds of concurrent connections can exhaust memory and trigger catastrophic context-switching latency.
PgBouncer sits in front of Postgres, maintaining a pool of warm database connections and recycling them instantly in transaction mode.
Docker Compose Configuration:
version: '3.8'
services:
postgres_master:
image: postgres:17-alpine
container_name: postgres_primary
restart: unless-stopped
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=primary_db_secret_password
- POSTGRES_DB=app_production
volumes:
- ./pgdata:/var/lib/postgresql/data
- ./postgresql.conf:/etc/postgresql/postgresql.conf:ro
command: ["postgres", "-c", "config_file=/etc/postgresql/postgresql.conf"]
networks:
- db_backend
pgbouncer:
image: edoburu/pgbouncer:latest
container_name: pgbouncer_pool
restart: unless-stopped
environment:
- DB_USER=postgres
- DB_PASSWORD=primary_db_secret_password
- DB_HOST=postgres_primary
- DB_PORT=5432
- DB_NAME=app_production
- POOL_MODE=transaction
- MAX_CLIENT_CONN=1000
- DEFAULT_POOL_SIZE=50
- RESERVE_POOL_SIZE=10
ports:
- "127.0.0.1:6432:6432"
depends_on:
- postgres_master
networks:
- db_backend
networks:
db_backend:
driver: bridge
3. High Availability with Patroni
For mission-critical production workloads requiring automatic failover, Patroni uses a Distributed Consensus Store (DCS) like etcd or Raft to coordinate health checks and elect a new primary node in under 10 seconds if hardware failure occurs.
Cost Comparison: Cloud Managed DB vs Self-Hosted VPS
| Configuration | AWS RDS (db.r6g.xlarge - 32GB RAM, Multi-AZ) | DigitalOcean Managed DB (32GB RAM, Standby) | Self-Hosted on Hetzner CCX33 (8 Dedicated vCPU, 32GB RAM NVMe) |
|---|---|---|---|
| Monthly Compute | ~$520.00 | ~$360.00 | €52.40 (~$57.00) |
| Backup Storage | $0.095/GB | Included (limited) | $0.005/GB (Backblaze B2 S3) |
| Data Transfer | $0.09/GB | Standard limits | 20 TB Included Free |
| Estimated Annual Cost | $6,800+/year | $4,400+/year | ~$720/year |
| Annual Savings | — | — | Save $3,600 to $6,000+ per year (85-90%) |
Ready to Deploy Battle-Tested Infrastructure?
Explore complete hardware requirements and self-hosted database alternatives on SelfHostStack.
To get ready-to-run Docker Compose stacks, automated S3 backup scripts, pgBackRest restore playbooks, and Traefik reverse-proxy configurations:
👉 Get the Self-Hosted Starter Stack Pack ($29) — Instant download, production verified, zero ongoing subscriptions.
Top comments (0)