Databases9 min read

Self-Hosted PostgreSQL vs Amazon RDS: ROI Comparison

The Amazon RDS bill grows as the database grows — storage, IOPS, connections, Multi-AZ: each parameter adds a line item. Self-hosted PostgreSQL on a dedicated VPS reverses the equation: fixed, predictable cost, unrestricted extensions, full root access. This guide compares both models with real numbers, covers the installation steps, and addresses the most common objection: who manages the server.

Why re-evaluate RDS in 2026

Amazon RDS for PostgreSQL is a solid managed database, but its billing model is designed so the invoice scales non-linearly with growth. Each component is billed separately: instance compute, gp3 storage per GB, provisioned IOPS, Multi-AZ replication nodes, backups beyond the included window, outbound data transfers. A db.r6g.4xlarge instance in Multi-AZ with 500 GB of gp3 storage exceeds $3,000/month on us-east-1 on-demand pricing. The same workload on a three-node Hetzner Cloud cluster runs around $835/month — a saving of $2,315/month, roughly $27,800 over twelve months (source: selfhost.dev, May 2026, HN item #48816129). This figure covers a complete high-availability cluster. For a development database, a staging environment, or a business workload without replication, a single node is enough for a fraction of that cost. The pricing landscape also shifted in 2026: Hetzner revised its rates in April then June 2026, and OVHcloud updated its VPS pricing in April 2026. These increases change the absolute numbers, not the logic — self-hosting remains structurally cheaper once the database exceeds a few dozen GB and the traffic demands a non-burstable instance. The question is no longer whether self-hosting costs less — the numbers confirm it — but whether the operational overhead is worth the savings.

What you gain by switching to a VPS

  • Fixed, predictable cost — no billing per GB, extra IOPS, or simultaneous connections; you size your VPS once and the price does not change with query volume.
  • Unrestricted extensions — PostGIS, TimescaleDB, pg_partman, pgvector, citus: no arbitrary restrictions, unlike RDS where the list is closed and unsupported extensions are absent.
  • Root access and fine-grained configurationpostgresql.conf, pg_hba.conf, huge_pages, wal_level, connection poolers: adjust every parameter without going through a cloud console and without restrictions on system tools.
  • Version control — you decide when to move from PostgreSQL 16 to 17, without imposed maintenance windows or forced deprecation by the vendor.
  • Data within your perimeter — choose the datacenter, encrypt volumes according to your internal policy, no cross-region data transfer charges on every analytical read.
  • Full portabilitypg_dump or logical replication moves your data to any other host without vendor friction and without egress fees.
  • Administration option available — if operational overhead remains the main objection, the VPS administration option covers routine tasks (updates, monitoring, backups); you keep control, not the workload.

PostgreSQL on VPS vs Amazon RDS: comparison table

CriterionPostgreSQL on VPSAmazon RDS for PostgreSQL
Monthly cost (standard workload)Fixed — proportional to the chosen serverVariable — compute + storage + IOPS + Multi-AZ + network egress
High-availability example (3 nodes / Multi-AZ)~$835/month (Hetzner CCX53 cluster, May 2026)~$3,150/month (db.r6g.4xlarge Multi-AZ, us-east-1)
PostgreSQL extensionsAll, including PostGIS, pgvector, TimescaleDB, citusRestricted list; unsupported extensions absent
Root / OS accessYes — OS choice, Docker, cron, kernel tuning, pgBouncerNo — AWS API only, restricted parameters
Version upgradesAt your own pace, without imposed maintenance windowsScheduled or forced by AWS at deprecation
Operational overheadBackups, updates, monitoring to manageAutomated by AWS: backups, patches, failover
PortabilityFull — `pg_dump` or logical replication, no egress feeTied to AWS ecosystem, data export is paid

Prerequisites before migrating

A VPS with 4 GB RAM and 2 vCPU is sufficient for most web application databases under 50 GB with moderate traffic and fewer than 50 concurrent connections. For a more demanding database, an analytical schema with many joins, or a service exposed to traffic spikes, plan for at least 8 GB RAM. SSD NVMe storage is essential: PostgreSQL's random access patterns on a rotational disk or entry-level SATA SSD significantly degrade performance, particularly on cache warm-up after a restart. Root access is required — standard on any cloud VPS. On the network side, you will need a fixed IPv4 address or an internal domain name to point your application connections to. Finally, plan a migration window where both databases run in parallel: the application points to RDS while you populate the VPS, verify consistency, then switch the connection variable. Duration depends on volume: a 5 GB dump restored in parallel takes minutes; 200 GB may take several dozen.

Install and configure PostgreSQL on a VPS

01

Install PostgreSQL from the official PGDG repository

On Ubuntu 24.04 or Debian 12, add the PostgreSQL Global Development Group repository to get the current version rather than the distribution-packaged one: curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/postgresql.gpg then echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" | sudo tee /etc/apt/sources.list.d/pgdg.list. Then: sudo apt update && sudo apt install -y postgresql-17.

02

Create a dedicated user and database

Connect as postgres: sudo -u postgres psql. Then create a dedicated application user and isolated database: CREATE USER myapp WITH PASSWORD 'strong-password'; followed by CREATE DATABASE myapp OWNER myapp;. Exit with \q. Never use the postgres role in your application connection string — a bug or injection would operate with superuser rights.

03

Configure postgresql.conf for expected load

The configuration file is at /etc/postgresql/17/main/postgresql.conf. Key parameters for a 4 GB VPS: shared_buffers = 1GB (25% of RAM), effective_cache_size = 3GB, work_mem = 16MB, maintenance_work_mem = 256MB, max_connections = 100. Restart after any change: sudo systemctl restart postgresql. For an 8 GB VPS, raise shared_buffers to 2 GB and effective_cache_size to 6 GB.

04

Restrict network connections in pg_hba.conf

By default, PostgreSQL only listens on localhost. If your application is on the same server, leave it as is. For an application on another machine, edit /etc/postgresql/17/main/pg_hba.conf and add: host myapp myapp <app-IP>/32 scram-sha-256. In postgresql.conf, set listen_addresses = 'localhost,<VPS-IP>'. Reload: sudo systemctl reload postgresql.

05

Export from RDS and import on the VPS

From a machine with access to both hosts, export using the custom format: pg_dump -h <rds-endpoint> -U <user> -Fc <database> -f dump.pgc. Then import on the VPS in parallel across 4 workers: pg_restore -h localhost -U myapp -d myapp -j 4 dump.pgc. Verify row counts on a few critical tables before switching the application connection.

06

Automate backups with a cron job

Create /usr/local/bin/pg-backup.sh: PGPASSWORD='password' pg_dump -U myapp myapp -Fc > /var/backups/pg/myapp-$(date +%Y%m%d-%H%M).pgc. Make it executable: chmod +x /usr/local/bin/pg-backup.sh. Add the crontab entry: 0 3 * * * /usr/local/bin/pg-backup.sh. Keep dumps encrypted on external storage or transfer them via rsync to a second VPS for an off-site copy.

07

Enable monitoring with pg_stat_statements

In postgresql.conf, add shared_preload_libraries = 'pg_stat_statements'. Restart PostgreSQL, then activate the extension in your database: CREATE EXTENSION pg_stat_statements;. Identify slow queries: SELECT query, mean_exec_time, calls FROM pg_stat_statements ORDER BY mean_exec_time DESC LIMIT 10;. This replaces RDS Performance Insights for daily diagnosis.

Post-install hardening: disable the system postgres account for SSH connections (sudo passwd -l postgres); enable ssl = on in postgresql.conf with a Let's Encrypt or self-signed certificate to encrypt network connections between the application and the database; enable ufw and open port 5432 only to known application IPs (sudo ufw allow from <app-IP> to any port 5432). These three steps cover the essential attack surface of a database exposed on a non-private network.

Troubleshooting: common errors after migration

The errors below appear frequently in the first hours after an RDS-to-VPS migration. Each has a precise message and a targeted remedy — read the full message before acting, as several distinct causes share the same error code.

Five common errors and their solutions

  • FATAL: password authentication failed for user "myapp" — the password sent does not match the one stored, or the authentication method differs (md5 vs scram-sha-256). Check the corresponding line in pg_hba.conf and reload: sudo systemctl reload postgresql. If you changed the password from psql, make sure the application connection string reflects the new one.
  • FATAL: no pg_hba.conf entry for host "<IP>", user "myapp", database "myapp", SSL off — the source IP of the connection is not authorised in pg_hba.conf. Add the missing entry for that IP and reload. If you are not using SSL, make sure the line uses host and not hostssl.
  • ERROR: extension "uuid-ossp" does not exist (or any extension absent on RDS) — the extension is available in PostgreSQL but not enabled in this database. Run from psql: CREATE EXTENSION IF NOT EXISTS "uuid-ossp";. If the extension is absent from the package, install it: sudo apt install postgresql-17-<extension>.
  • FATAL: remaining connection slots are reserved for non-replication superuser connectionsmax_connections is reached. Increase it in postgresql.conf and restart, or install pgBouncer to pool connections: a pool of 10 real connections can serve hundreds of application clients.
  • pg_restore: error: could not execute query: ERROR: role "rdsadmin" does not exist — RDS creates internal roles absent from any non-AWS PostgreSQL. Add --no-owner --no-privileges to pg_restore: pg_restore --no-owner --no-privileges -h localhost -U myapp -d myapp dump.pgc. Objects are imported without attempting to reassign ownership to RDS roles.

What this comparison does not cover — and what to know before deciding

Self-hosting transfers to your team the management of backups, security updates and monitoring. This is a reality, not an argument against it — evaluate it against the cost saved. For a solo developer or a small team without operational expertise, the VPS administration option covers routine tasks — updates, monitoring, verified backups — without you having to orchestrate them. RDS remains relevant in two specific situations: when multi-region resilience is a contractual requirement and you lack the resources to implement it manually, and when consumption-based billing genuinely suits a very small, rarely-accessed database (a VPS runs and is billed 24/7 even at zero load). For everything else — mid-sized database, stable workload, team with basic system skills — the numbers clearly favour self-hosting. The articles <a href="/blog/heberger-postgresql-vps">PostgreSQL on VPS: installation and best practices</a> and <a href="/blog/postgresql-fin-de-vie-planifier-montee-version">PostgreSQL end of life: planning the version upgrade</a> complement this guide on the operational and major version management side.

Deploy PostgreSQL on a dedicated-resource VPS

Root access, SSD NVMe storage, IPv4 included, no per-GB surcharge. Choose your configuration and put your database in orbit.

Need help?

Browse our help center and FAQ, or write to our team — support in French, English and Arabic.