The hobby tiers of managed Postgres work well until you cross the free-tier line. Then RDS, Supabase Pro, and Neon Scale all start at $25–70 per month per project for features you don't use. One app can absorb that. Across a five-project portfolio it compounds into a four-figure annual bill for one Postgres server.
The "self-host vs. managed" debate skips a middle ground: one cheap VPS running Postgres for many projects, with real S3 backups, point-in-time recovery, a pilot-light disaster-recovery plan, and Grafana monitoring, all as code, for about $7 a month total.
This is not zero-touch. The first setup takes two to four hours, and you need comfort with Ansible and a terminal. In exchange you get something that costs an order of magnitude less than managed equivalents at the same feature parity, rebuilds on any Ubuntu 24.04 host from the same config, and survives a provider failure with a documented runbook.
What you get
- PostgreSQL 16 with pgvector, fronted by PgBouncer in transaction-pooling mode. Pooling ships built in.
- Multiple isolated project databases on the same server. Each gets its own role, schema isolation, and mTLS client cert. Ansible signs the certs and pushes them to AWS Secrets Manager.
- S3 backups with point-in-time recovery via pgBackRest. Encrypted, versioned, WAL-shipping plus nightly full. You can restore to any second within the retention window.
- Pilot-light disaster recovery on AWS EC2. The standby lives as a CloudFormation template that costs nothing until it runs. You run one command to provision it, restore from S3, and repoint DNS. A second command tears it back down.
- Grafana Cloud monitoring. The repo ships dashboards and alert rules. Free-tier-compatible.
- mTLS everywhere. Apps connect with client certs. No public-internet password auth.
- All as code. Ansible configures the machines, CloudFormation provisions the AWS pieces, Docker Compose runs the workload. You don't SSH and edit files by hand.
What it costs
The math, for a five-project hobby portfolio with similar features (backups, DR posture, monitoring):
The pgfleet column breaks down as:
- VPS: ~$5/mo for a baseline Ubuntu 24.04 host. OVH, Hetzner, DigitalOcean all work.
- AWS idle: ~$2/mo for S3 backup storage plus a few rounding-error line items (Route 53 hosted zone, Secrets Manager).
- AWS DR: a few cents per hour, charged during a failover or drill. Zero the rest of the time.
- Grafana Cloud: free tier covers a small-to-mid deployment.
Total steady-state: ~$7/mo for the whole fleet, regardless of project count (within VPS capacity). The cost grows with traffic, not with how many projects you add.
When to skip this recipe
Four profiles this recipe serves badly:
- You don't enjoy a terminal. Supabase or Neon work better. The two-to-four-hour setup is real, and you handle Postgres major-version upgrades every one to two years.
- You need a 99.99% SLA with auto-failover guarantees. RDS Multi-AZ has that built in. pgfleet's DR is on-demand and takes minutes from your trigger to ready.
- You're past the cost-saving point. At >1 TB or sustained millions of QPS, you pay for compute and IOPS regardless of provider, and the convenience of a managed service is worth the premium.
- You don't want to own the OS. Kernel patches, unattended-upgrades, the rare reboot. Ansible automates most of it. You handle the rest.
Prerequisites
- An Ubuntu 24.04 VPS. Any provider, any region. Note the IP after you order it.
- An AWS account with billing enabled. The recipe uses S3, Secrets Manager, CloudFormation, Route 53, and EC2 during DR.
- A domain in a Route 53 hosted zone. DNS-based failover needs this. A subdomain of your main domain works.
- Local tools: Ansible 2.16+ with
ansible-vault, AWS CLI v2 configured,make, and basic Linux comfort. - ~2–4 hours for the first setup, then a few minutes per project after that.
Architecture in one diagram
Apps connect through PgBouncer (transaction pooling on port 6432) to Postgres, all on a single VPS. pgBackRest ships WAL non-stop to S3 and writes a nightly full to the same bucket. The DR EC2 doesn't exist until you need it. When you do, a CloudFormation stack provisions it from S3 and repoints DNS. After recovery you tear it back down.
The recipe
Every step below is one make command from the repo root. The Makefile is the API; Ansible and CloudFormation underneath are the implementation.
1. Get the repo + install local prereqs
git clone https://github.com/danielsemerjya/pgfleet.git && cd pgfleetInstall the Ansible collections the playbooks need:
ansible-galaxy collection install -r ansible/requirements.ymlcommunity.aws also wants boto3 for pushing client certs to Secrets Manager:
pip install boto3 botocore2. Order a VPS
Any Ubuntu 24.04 LTS host works. OVH, Hetzner, DigitalOcean, Linode. The reference deployment uses OVH because it's among the cheapest in the EU. The playbook stays provider-agnostic.
Note the IP. You'll need it in two configs in step 3.
3. Configure (vault, vars, hosts)
Every personal value lives in gitignored files. Copy the templates, fill them in, encrypt the secrets file:
cp ansible/group_vars/all/vault.example.yml ansible/group_vars/all/vault.ymlansible-vault encrypt ansible/group_vars/all/vault.ymlThen the non-secret config (domain, region, project list), the inventory, and the per-host config the Makefile reads:
cp ansible/group_vars/all/vars.example.yml ansible/group_vars/all/vars.ymlcp ansible/inventory/hosts.example.yml ansible/inventory/hosts.ymlmkdir -p private && cp config.example.mk private/config.mk4. Provision the AWS backup infra (once)
This is a CloudFormation stack: the S3 bucket, a scoped IAM user for backups, and the db.<your-domain> DNS record.
make backup-infraAfter it succeeds, copy the stack outputs into your config and vault:
make outputs5. Build the database
One command runs the full convergence: host prep, TLS and CA, Postgres and PgBouncer, per-project roles and databases, backups, monitoring, client-cert issuance.
make siteConfirm DNS resolves to your VPS:
make dnsTo re-run one phase, target a single Ansible tag, e.g. make tags T=backups.
6. Wire your first app
For local Prisma / Next.js development. This writes the client cert, key, and a ready-to-use .env.local:
make dev-env PROJECT=<your-project>For DBeaver (mTLS, PKCS#8 key). Same output, plus the connection fields you paste into DBeaver:
make dbeaver PROJECT=<your-project>For Amplify, Lambda, or any serverless host, follow docs/amplify-nextjs-setup.md in the repo. The pattern: pull the client cert from Secrets Manager at runtime, hand it to Prisma 7 via the connection string.
7. Disaster-recovery drill
Do this once before you need it. Fill in private/dr.env, then:
make dr-failoverThat provisions the DR EC2 from the CloudFormation template, restores the latest S3 backup, and repoints db.<your-domain> to the new instance. Apps reconnect on their own. After verification, tear it back down:
make dr-teardownTeardown deletes the DR EC2 and points DNS back to the primary. The DR bill goes back to $0. Schedule this drill once a quarter.
8. Monitoring
Grafana Cloud (free tier) hosts two artefacts the repo ships: dashboards (Postgres, PgBouncer, node) and alert rules (replication lag, disk pressure, backup freshness, certificate expiry). The setup lives in grafana/SETUP.md. Import the dashboards once. The alerts page you the moment a nightly backup misses or a cert is <30 days from expiry.
What you don't do manually
- mTLS client-cert issuance. Ansible signs them with the internal CA, hands them to Secrets Manager, and re-issues on a schedule. You don't reach for
openssl. - Per-project database creation. Add the project to the
projects:list invars.yml, runmake site, done. - Backup verification. pgBackRest checks WAL integrity on every push. Grafana alerts on missed backups.
- DR EC2 provisioning.A CloudFormation template handles it. You don't click anything in the AWS console.
- DNS for failover. The failover script updates Route 53. No manual editing.
Maintenance reality
Things that need your attention:
- OS patches. Unattended-upgrades runs by default, but kernel updates need a reboot. Plan a short maintenance window every few weeks.
- Postgres major-version upgrades. Once every one to two years. The pattern (dump and restore via pgBackRest) lives in the docs, and you run it.
- Quarterly DR drill. Run
make dr-failoverandmake dr-teardownon a calendar reminder. Twenty minutes of work, peace of mind for the other ninety days. - Cert renewal. Grafana alerts at 30 days; Ansible re-issues with one playbook run.
Things that don't:
- Backup health. Grafana alerts on it.
- Config drift. Ansible reconverges every time you run it.
- Monitoring drift. The dashboards live in git.
- The DR EC2. It doesn't exist until you summon it.
When this is the right choice
This recipe fits one specific profile: a small team or solo builder shipping two to fifteen small-to-mid Postgres projects, who values per-month cost and per-incident control, and who will trade two-to-four hours of setup time for years of low recurring spend.
RDS and Supabase serve a different audience. pgfleet sits at a different point on the price-vs-effort curve, and if you sit on that part of the curve too, this recipe was written for you.
