All writing
15 min read

A production-grade Postgres recipe for ~$7/mo

  • PostgreSQL
  • Self-hosting
  • Recipe
A charcoal server linked by a glowing line to a luminous cloud of storage, with a dashed wireframe standby server waiting on the right: self-hosted Postgres with backups and pilot-light DR

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):

$0$100$200$300pgfleet1 VPS + AWS, 5 projects$7/moSupabase Pro5 × $25/project$125/moAWS RDS Multi-AZ5 × db.t4g.micro$175/moNeon Scale5 × $69/project$345/mo
Approximate public list prices, mid-2026, for a five-project portfolio with comparable backup / DR / monitoring posture. RDS includes db.t4g.micro Multi-AZ + storage + backups; Neon and Supabase numbers are their published per-project Scale/Pro tiers. Your real bill will vary with traffic.

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.

AppsAmplifyLambda · Next.jsVPS · UBUNTU 24.04PgBouncertransaction pool · :6432PostgreSQL 16+ pgvectorS3pgBackRest · PITRencrypted · versionedDR EC2pilot-light~$0 idlemTLSWAL + nightlyon failoverDNS repoint (Route 53)
Solid lines run in steady state. Dashed lines fire during a failover.

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 pgfleet

Install the Ansible collections the playbooks need:

ansible-galaxy collection install -r ansible/requirements.yml

community.aws also wants boto3 for pushing client certs to Secrets Manager:

pip install boto3 botocore

2. 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.yml
ansible-vault encrypt ansible/group_vars/all/vault.yml

Then 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.yml
cp ansible/inventory/hosts.example.yml ansible/inventory/hosts.yml
mkdir -p private && cp config.example.mk private/config.mk

4. 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-infra

After it succeeds, copy the stack outputs into your config and vault:

make outputs

5. 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 site

Confirm DNS resolves to your VPS:

make dns

To 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-failover

That 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-teardown

Teardown 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 in vars.yml, run make 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-failover and make dr-teardown on 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.

Discuss

Want to talk about your version of this?

Email me