Deployment guide

PostgreSQL on a VPS: reliable, controlled database

Deploy on a VPS Cloud →

Tutorial

Databases

PostgreSQL on a VPS: reliable, controlled database

PostgreSQL is often the trusted database of business applications, APIs and analytics tools. On a ServOrbit VPS, you control the version, storage, backups and access instead of paying for a managed database by the GB. This guide covers the decisions that matter — persistence, users, network and restoration — as well as fine-tuning `postgresql.conf`, automated backups with restore testing, streaming high availability across two VPS, and troubleshooting the most common production failures.

9 min read11 steps
Contents· Why self-host PostgreSQL on a VPS1/10
  1. 01Why self-host PostgreSQL on a VPS
  2. 02The concrete benefits of a self-hosted PostgreSQL
  3. 03Hardware and software prerequisites
  4. 04Deploy PostgreSQL with Docker in production
  5. 05Configure postgresql.conf for production
  6. 06Automated backups and recovery strategy
  7. 07Lightweight high availability without Kubernetes
  8. 08PostgreSQL self-hosted vs RDS vs Supabase vs Neon
  9. 09Troubleshooting common production issues
  10. 10The official documentation

Why self-host PostgreSQL on a VPS

Managed databases (RDS, Cloud SQL) bill per GB stored, per IOPS and per connection, and often limit available extensions. By deploying PostgreSQL on your own VPS, you enable pg_stat_statements, PostGIS, pgvector or TimescaleDB without restriction, choose your exact version (15, 16, 17) and keep your data in a jurisdiction you control. A VPS with an SSD disk offers very low I/O latencies, ideal for the transactional workloads of a production application. You pay a fixed and predictable flat rate, and you alone decide on your strategy for replication, encryption and backup retention.

The concrete benefits of a self-hosted PostgreSQL

  • Fixed and predictable cost: no billing per GB, per IOPS or per number of active connections.
  • Free extensions: pgvector for AI, PostGIS for geospatial, TimescaleDB for time series.
  • Full control of backups: scheduled pg_dump, pg_basebackup or physical replication to a second VPS.
  • Fine tuning of postgresql.conf (shared_buffers, work_mem, WAL) impossible with most managed offerings.
  • Data sovereignty: your data stays on an infrastructure you choose.
  • Real superuser access to audit, profile and optimize without a support ticket.

Hardware and software prerequisites

For a development PostgreSQL instance or a small production project, plan for a VPS with 2 vCPU, 4 GB of RAM and 40 GB of NVMe SSD: PostgreSQL likes memory for its page cache. For a heavier load (several hundred connections or datasets of several tens of GB), aim for 4 vCPU and 8 GB of RAM. On the software side: Ubuntu 22.04 or 24.04 LTS, Docker Engine and the Docker Compose v2 plugin, a domain name pointing to the VPS (e.g. db.mydomain.com) if you expose remote access, and a ufw firewall configured to let through only the strict minimum. Also reserve a dedicated Docker volume for PGDATA to decouple the data from the container.

Deploy PostgreSQL with Docker in production

  1. Prepare the VPS and the firewall

    Connect via SSH, update the system with apt update && apt upgrade -y, install Docker via curl -fsSL https://get.docker.com | sh. Block port 5432 from the outside with ufw deny 5432: the database must never be directly exposed to the Internet.

  2. Write the docker-compose.yml

    Create a postgres:17 service, mount a named volume on /var/lib/postgresql/data, and define POSTGRES_PASSWORD, POSTGRES_DB and POSTGRES_USER via a .env file that is never committed. Add shm_size: 256mb for large sorts and a healthcheck with pg_isready.

  3. Launch and initialize the database

    Start with docker compose up -d, check the logs via docker compose logs -f postgres, then connect with docker compose exec postgres psql -U app -d mabase. Create your application roles with minimal privileges rather than using the superuser.

  4. Expose access via a tunnel or a TLS proxy

    For secure remote access, never open 5432 in the clear: use an SSH tunnel (ssh -L 5432:localhost:5432) or place pgbouncer behind a reverse proxy. Enable SSL on the PostgreSQL side with a Let's Encrypt certificate mounted in the container (ssl = on in postgresql.conf).

  5. Tune the configuration

    Adjust shared_buffers to about 25% of the RAM, effective_cache_size to 50-75%, and work_mem according to your queries. Enable pg_stat_statements to identify slow queries. Use the PGTune tool as a starting point.

  6. Automate the backups

    Schedule a daily pg_dump via a cron task on the host, compress into custom .dump format, and copy the archives to external storage. Regularly test restoration with pg_restore: an untested backup does not exist.

  7. Logging in for the first time

    pgAdmin is not published on the internet: open a tunnel from your own machine (ssh -L 5050:127.0.0.1:5050 root@<IP>), then go to http://127.0.0.1:5050 and sign in with [email protected] and the password provided. Then add your server ("Add New Server") with host db, port 5432, user postgres and that same password.

Configure postgresql.conf for production

  1. shared_buffers: PostgreSQL's memory cache

    Set shared_buffers = 25% of the VPS's total RAM. On 8 GB of RAM, that gives 2 GB. This parameter controls the size of the page cache shared between all PostgreSQL processes — it is the most important lever for repeated reads. A custom postgresql.conf file mounted via Docker (-v ./postgresql.conf:/etc/postgresql/postgresql.conf) lets you apply these settings at container startup.

  2. work_mem and max_connections

    Set work_mem based on your sort and join queries: 4 to 16 MB is reasonable for mixed workloads. Note: each connection can allocate work_mem multiple times simultaneously, so multiply by max_connections to estimate peak memory usage. For 100 application connections, max_connections = 150 (reserve for monitoring tools and migrations) is plenty; beyond that, use pgbouncer in transaction mode to multiplex connections.

  3. Logging and slow query detection

    Enable logging_collector = on, log_min_duration_statement = <seuil_ms> (ms) and log_line_prefix = '%t [%p]: [%l-1] db=%d,user=%u ' to log all queries exceeding the configured threshold. Logs land in pg_log/ inside the PGDATA volume. Pairing this with pg_stat_statements (extension to enable in shared_preload_libraries) gives you an aggregated view of the most expensive queries without scanning each log file.

  4. WAL and checkpoints

    Increase wal_buffers to 16 MB and set checkpoint_completion_target to 0.9 to spread checkpoint writes over time. On NVMe SSD, random_page_cost = 1.1 (instead of the default 4.0) encourages the planner to choose index scans over seq scans — a frequently overlooked setting that dramatically changes execution plans on large datasets.

Automated backups and recovery strategy

An untested backup is a backup that does not exist. Three complementary approaches cover different needs.

pg_dump + cron is the starting point: simple to set up, it produces a logical SQL file restorable on any compatible version. Schedule it at 3 AM, compress with --format=custom (compressed binary format, selective restore by table), and copy the archive to external storage (S3-compatible object, another VPS) with rclone or scp. Keep 7 days locally and 30 days remotely.

pgBackRest goes further: it supports incremental and differential backups, parallel compression and point-in-time restore (PITR) from WAL archives. It is the recommended solution once your database exceeds a few GB or you need to rewind to a precise minute after an accidental DELETE. It is configured via a pgbackrest.conf file and integrates with a cron job or systemd timer.

Monthly restore test: regularly run pg_restore -d test_restore <dump> on a test VPS to verify archive integrity. A weekly script that restores the latest backup and checks a table count or a control query is enough to catch silent corruption before it becomes critical.

Lightweight high availability without Kubernetes

PostgreSQL high availability on a VPS does not necessarily require Patroni or a complex Pacemaker cluster. Native streaming replication covers most needs with minimal configuration.

Manual streaming replication: on the primary VPS, enable wal_level = replica, add a dedicated replicator user in pg_hba.conf and set max_wal_senders = 3. On the secondary (standby) VPS, run pg_basebackup to initialize a physical copy, create a standby.signal file and configure primary_conninfo in postgresql.conf. Replication starts automatically on restart. With hot_standby = on, the standby accepts read-only queries — perfect for offloading reports and monitoring.

Patroni adds automatic failover: it monitors the primary via a key in etcd, Consul or ZooKeeper, and automatically promotes the standby if the primary becomes unreachable for more than 30 seconds. On two VPS, a third-party etcd (lightweight VM or managed service) for consensus is sufficient. Patroni is the recommended path when you cannot tolerate a 5-to-15-minute manual intervention during a failure.

Replication lag monitoring: SELECT pg_wal_lsn_diff(pg_current_wal_lsn(), sent_lsn) AS lag_bytes FROM pg_stat_replication on the primary gives you the real-time lag in bytes. A lag above a few MB indicates network saturation or an overloaded standby.

PostgreSQL self-hosted vs RDS vs Supabase vs Neon

Scroll the table

CriterionServOrbit VPS (self-hosted)AWS RDS PostgreSQLSupabase (cloud)Neon (serverless)
Monthly cost (small)99 DH/month (flat rate)~$35-70/month (db.t3.medium)~$25/month (Pro)~$19/month (Launch)
postgresql.conf controlFullPartial (parameter groups)LimitedMinimal
Custom extensionsAll (pgvector, PostGIS…)Restricted listpgvector, PostGISLimited
PITR (Point-in-Time Recovery)Yes with pgBackRestYes (native)Yes (Pro+)Yes (native)
High availabilityManual replication or PatroniMulti-AZ nativeNativeMulti-region native
Maintenance burdenHigh (DBA required)LowVery lowVery low
Data outside USYes (Moroccan VPS)By regionBy regionUS/EU only
Superuser / extensionsYesNoNoNo

Troubleshooting common production issues

OOM killer kills PostgreSQL: the OOM signal in /var/log/syslog (oom_kill_process) means memory is exhausted. Main causes: shared_buffers set too high, work_mem miscalibrated against connection count, or a concurrent process (Elasticsearch, Node.js) consuming all swap. Diagnosis: cat /proc/meminfo | grep -E 'MemFree|Cached|Buffers' and ps aux --sort=-%mem | head -10. Fix: reduce work_mem, enable huge_pages = try in postgresql.conf, or upgrade the VPS.

Connections exhausted (FATAL: sorry, too many clients already): max_connections is reached. Short term: SELECT count(*), state FROM pg_stat_activity GROUP BY state to identify idle connections. Long term: deploy pgbouncer in transaction mode (pool_mode = transaction) in front of PostgreSQL — it multiplexes hundreds of application connections onto a pool of 20-50 real connections.

Autovacuum saturated: tables with many UPDATE or DELETE operations accumulate dead tuples that bloat tables and slow down scans. SELECT relname, n_dead_tup, last_autovacuum FROM pg_stat_user_tables ORDER BY n_dead_tup DESC LIMIT 10 reveals the problematic tables. Speed up autovacuum by reducing autovacuum_vacuum_scale_factor (e.g. 0.01 instead of 0.2) for critical tables via ALTER TABLE ... SET (autovacuum_vacuum_scale_factor = 0.01).

Checkpoints too frequent (LOG: checkpoint occurring too frequently): WAL fills faster than checkpoint_timeout (default 5 min). Increase max_wal_size (e.g. 2-4 GB) and checkpoint_timeout to 15-30 min to space out checkpoints without risking excessive data loss on a crash.

For high availability, deploy a second VPS in physical streaming replication (primary_conninfo + replication slot). You get a warm standby ready to fail over, and you can offload your heavy read queries to it (hot_standby = on) to relieve the primary. Combined with pgBackRest for differential incremental backups, you reach an RPO of a few minutes without depending on a managed service.

The official documentation

For advanced configuration, tool-specific options and version changes, refer to the official PostgreSQL documentation. This guide covers going live on a ServOrbit VPS; the vendor documentation remains the reference for fine tuning.

Deploy your PostgreSQL in minutes

Launch a ServOrbit Cloud VPS with a preconfigured Docker template and SSD storage: the ideal environment to host PostgreSQL without per-GB billing.