To install PostgreSQL on a VPS running Ubuntu 24.04, run sudo apt install postgresql for Ubuntu’s PostgreSQL 16, or add the official PostgreSQL apt repository and install postgresql-18 for the current major version. Either way, the server starts listening only on localhost, so the work that matters comes next: a role and database for your app, an access path that keeps port 5432 off the open internet, memory settings sized to your plan, and a backup you have restored at least once.
Every command and setting below comes from the PostgreSQL 18 documentation, the PostgreSQL apt repository wiki and Ubuntu’s server documentation, checked on October 3, 2026. On that date the current releases were 18.6 and 16.15, and PostgreSQL 19 was in beta (Beta 4 shipped on September 24, 2026). You need a fresh Ubuntu 24.04 server and a sudo user. If you have not logged in yet, connect to your VPS over SSH first, then work through the new VPS security checklist: SSH keys, no root login, updates, and UFW with SSH allowed.
Key takeaways
- Ubuntu 24.04 ships PostgreSQL 16; the official PGDG apt repository adds PostgreSQL 18 (18.6 as of October 2026), which is supported until November 14, 2030.
- Install postgresql-18 by name from the PGDG repository, because its postgresql meta-package moves to PostgreSQL 19 when that ships (planned for October 2026).
- PostgreSQL listens only on localhost by default: use an SSH tunnel, or allow one client IP with a hostssl scram-sha-256 line plus a matching UFW rule, never 0.0.0.0/0 with md5.
- Start shared_buffers at 25% of RAM and effective_cache_size at 1/2 to 3/4 of RAM on a database-only VPS, and size work_mem against max_connections.
- A nightly pg_dump plus pg_dumpall --globals-only, copied off the server and test-restored, covers most small databases; add pg_basebackup and WAL archiving for point-in-time recovery.
Ubuntu’s PostgreSQL 16 or PostgreSQL 18 from the official repository?
Ubuntu ships one PostgreSQL major version per release and, in the words of postgresql.org’s Ubuntu download page, supports it “throughout the lifetime of that Ubuntu version”. On 24.04 that version is 16. The PostgreSQL Global Development Group (PGDG) also runs its own apt repository, which carries every supported major version (14 through 18 as of October 2026), plus the end-of-life 13 and the 19 beta. Both are official builds of the same software; the difference is which version you get, and who delivers the updates. On Debian 12 the distribution package is PostgreSQL 15, and the repository steps below work the same way there.
Ubuntu archive: postgresql | PGDG repository: postgresql-18 | |
|---|---|---|
| Major version on 24.04 | 16 (16.15, in noble-security and noble-updates since August 20, 2026) | 18 (18.6, released August 13, 2026) |
| Community support ends | November 9, 2028 | November 14, 2030 |
| Automatic security updates | Yes, through Ubuntu’s security pocket and unattended-upgrades | Not by default; you run apt upgrade (see minor updates) |
| Extra setup | None | One script, or a sources file plus a signing key |
| What 18 adds over 16 | Not available | Asynchronous I/O, uuidv7(), data checksums on by default, MD5 passwords deprecated |
| Pick it when | Your app supports 16 and you want no third-party repository | You start a new project or want the longest support window |
Tip: The project’s roadmap plans PostgreSQL 19 for October 2026. The PGDG repository’s postgresql meta-package always points at the newest release, so install the versioned package, postgresql-18, if you want to stay on the major version you chose. The repository’s wiki gives the same advice.
How to install PostgreSQL on a VPS running Ubuntu 24.04
Pick one of the two options, run it as your sudo user, then check the result. The rest of this guide uses version 18 in paths and unit names; on Ubuntu’s package, replace 18 with 16.
Option A: Ubuntu’s package (PostgreSQL 16)
sudo apt update
sudo apt install postgresql
That is the whole install, and it is the command postgresql.org lists for Ubuntu. Many guides add postgresql-contrib. On 24.04 that name only points at postgresql-16, which already contains the contrib extensions such as pg_stat_statements.
Option B: The PostgreSQL apt repository (PostgreSQL 18)
These two commands are the automated setup from postgresql.org’s Ubuntu download page. The script reads your release codename (noble), installs the repository’s signing key, adds apt.postgresql.org to your apt sources and runs apt-get update. It waits for you to press Enter before it changes anything.
sudo apt install -y postgresql-common
sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh
Then install the versioned server package:
sudo apt install postgresql-18
If you would rather see every line, this is the manual setup from the PostgreSQL wiki. It does the same thing:
sudo apt install curl ca-certificates
sudo install -d /usr/share/postgresql-common/pgdg
sudo curl -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc --fail https://www.postgresql.org/media/keys/ACCC4CF8.asc
. /etc/os-release
sudo tee /etc/apt/sources.list.d/pgdg.sources <<EOF
Types: deb deb-src
URIs: https://apt.postgresql.org/pub/repos/apt
Suites: $VERSION_CODENAME-pgdg
Architectures: $(dpkg --print-architecture)
Components: main
Signed-By: /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc
EOF
sudo apt update
sudo apt install postgresql-18
Check that the cluster is running
Debian and Ubuntu run PostgreSQL as clusters, each named by version and cluster name, and manage them with the tools from postgresql-common. pg_lsclusters should print one line for 18 main on port 5432 with the status online. Each cluster has its own systemd unit, postgresql@18-main.
pg_lsclusters
sudo systemctl status postgresql@18-main --no-pager
sudo -u postgres psql -c "SELECT version();"
sudo -u postgres psql -c "SHOW data_checksums;"
On 18, data_checksums reports on: from version 18, initdb enables page checksums by default, so PostgreSQL can detect pages that storage errors corrupted. A cluster created by Ubuntu’s 16 reports off.
| What | Path on Ubuntu (version 18) |
|---|---|
| Main configuration | /etc/ |
| Drop-in configuration files | /etc/ |
| Client authentication | /etc/ |
| Data directory | /var/ |
| Server log | /var/ |
| systemd unit | postgresql@ |
Create a database user and a database for your app
Ubuntu logs local users in through peer authentication: the Linux user postgres connects as the database superuser postgres, with no password. Keep it that way, and give each application its own login role that owns its own database:
sudo -u postgres createuser --pwprompt appuser
sudo -u postgres createdb --owner=appuser appdb
--pwprompt asks for the password twice and stores it as a SCRAM-SHA-256 hash, the default password_encryption. Use a long random password (for example from openssl rand -base64 30) and keep it in your password manager or your app’s secret store.
Making the role the database owner matters on PostgreSQL 15 and later. Since version 15, ordinary roles no longer get CREATE on the public schema; the schema belongs to the database owner instead. A role that does not own the database gets permission denied for schema public on its first CREATE TABLE.
Test a password login over TCP from the server itself. Peer authentication only applies to the Unix socket, so -h 127.0.0.1 makes psql use the host rules and the password:
psql -h 127.0.0.1 -U appuser -d appdb -c "SELECT current_user, current_database();"
An app on the same server then connects with postgresql://appuser:[email protected]:5432/appdb. Nothing has to listen on a public address for that.
How do you allow remote connections to PostgreSQL safely?
By default PostgreSQL listens only on localhost (listen_addresses = 'localhost'), so nothing outside the server can reach it. Choose the narrowest path that covers who needs access:
| Who connects | Path | Changes on the server |
|---|---|---|
| An app on the same VPS | 127. or the Unix socket | None |
| You, from a laptop (psql, pgAdmin, DBeaver) | SSH tunnel | None |
| An app on another server | A persistent SSH tunnel, or TLS with a one-IP allowlist | Tunnel: none. TLS: listen_, one hostssl line, one UFW rule |
| Anyone on the internet | Don’t | None |
Option 1: Reach PostgreSQL through an SSH tunnel
An SSH local forward carries the database connection inside your SSH session. Port 5432 never opens to the internet, and pg_hba.conf needs no new line, because the connection arrives on the server from 127.0.0.1. Run this on your own computer, with your server’s user and IP:
ssh -N -L 5433:127.0.0.1:5432 [email protected]
Leave it open and connect to local port 5433 in a second terminal. Using 5433 avoids a clash with a PostgreSQL server on your laptop:
psql "host=127.0.0.1 port=5433 dbname=appdb user=appuser"
pgAdmin and DBeaver can open the same tunnel from their connection settings. For an app server that needs the database around the clock, run the tunnel as a systemd service with a key that may only forward to 127.0.0.1:5432. Our SSH port forwarding guide shows both the service and the restricted tunnel key.
Option 2: Open port 5432 to one IP, with TLS and SCRAM
Use this when an app on another server must connect directly. The example lets only the app server, 198.51.100.7, reach appdb as appuser; your database server is 203.0.113.10.
Step 1: listen on the network. listen_addresses only changes at server start, so put it in a drop-in file and restart:
echo "listen_addresses = '*'" | sudo tee /etc/postgresql/18/main/conf.d/20-listen.conf
sudo systemctl restart postgresql@18-main
sudo ss -ltnp | grep 5432
* means every interface, IPv4 and IPv6. pg_hba.conf and the firewall decide who gets past it.
Step 2: allow exactly one client in pg_hba.conf. A hostssl line only matches TLS connections, and scram-sha-256 is, in the docs’ words, “the most secure of the currently provided methods”:
echo "hostssl appdb appuser 198.51.100.7/32 scram-sha-256" | sudo tee -a /etc/postgresql/18/main/pg_hba.conf
sudo systemctl reload postgresql@18-main
sudo -u postgres psql -c "SELECT line_number, type, database, user_name, address, auth_method, error FROM pg_hba_file_rules;"
PostgreSQL reads pg_hba.conf from the top, and the first line that matches the connection type, address, database and user decides. There is no fall-through: if that line’s authentication fails, later lines are not tried. The pg_hba_file_rules view shows how the server parsed the file; a value in the error column marks a broken line. If the app server connects over IPv6, add a second line with its /128 address.
Step 3: open the firewall for that IP only:
sudo ufw allow from 198.51.100.7 to any port 5432 proto tcp
sudo ufw status numbered
Step 4: connect from the app server with TLS required:
psql "host=203.0.113.10 port=5432 dbname=appdb user=appuser sslmode=require"
Inside that session, SELECT ssl, version FROM pg_stat_ssl WHERE pid = pg_backend_pid(); should show t and TLS 1.2 or newer, the server’s default minimum. Ubuntu’s PostgreSQL has TLS on out of the box, with a self-signed certificate that Ubuntu’s documentation calls suitable for testing.
sslmode=require encrypts the connection but does not check which server answered. For sslmode=verify-full, which the libpq documentation recommends for most security-sensitive environments, install a certificate issued for the server’s hostname (ssl_cert_file and ssl_key_file) and give the client the CA certificate. Note that the default sslmode on the client, prefer, offers no protection against an attacker in the middle.
Warning: Many tutorials add host all all 0.0.0.0/0 md5. That line invites every address on the internet to guess passwords for every database, accepts unencrypted connections (host matches TLS and plain connections alike), and relies on MD5, which PostgreSQL 18 deprecates.
If PostgreSQL runs in Docker with -p 5432:5432, UFW does not filter that port at all. See why Docker bypasses UFW and bind it to 127.0.0.1 instead.
PostgreSQL tuning on a VPS: shared_buffers, effective_cache_size and work_mem
PostgreSQL’s defaults are deliberately small: shared_buffers 128 MB, work_mem 4 MB, maintenance_work_mem 64 MB and max_connections 100. On a VPS with a known amount of RAM, five settings do most of the work. The rules come from the PostgreSQL 18 resource documentation and the PostgreSQL wiki’s tuning guide:
shared_buffers: PostgreSQL’s own page cache. For a dedicated database server with 1 GB of RAM or more, the docs suggest 25% of RAM as a starting value, and say more than 40% is unlikely to help, because PostgreSQL also relies on the operating system’s cache. Changes need a restart.effective_cache_size: not an allocation, only the planner’s estimate of how much data shared_buffers and the OS cache hold together. The wiki calls half of RAM a normal conservative setting and 3/4 “more aggressive but still reasonable”.work_mem: memory for each sort or hash step before it spills to temporary files. One query can run several such steps, and every connection can do so at once, so the docs warn the total “could be many times the value”. Hash steps may usework_memtimeshash_mem_multiplier(2.0 by default).maintenance_work_mem: memory for VACUUM, CREATE INDEX and restores. Safe to set well above work_mem, but each autovacuum worker may use this much too.max_connections: each connection is its own server process, and the work_mem worst case grows with it. The wiki points to connection pooling if you need thousands.
Starting values for each VPS size
The table applies those rules to each HourlyVPS memory size, for a VPS that runs only PostgreSQL. The last three columns are our own arithmetic, not PostgreSQL’s: max_connections times work_mem stays at or below a quarter of RAM, rounded down to a power of two and never below the 4 MB default, and maintenance_work_mem is 1/16 of RAM, capped at 1 GB.
| RAM (plans) | shared_buffers | effective_cache_size | work_mem | maintenance_work_mem | max_connections |
|---|---|---|---|---|---|
| 1 GB (Q1) | 256MB | 768MB | 4MB | 64MB | 50 |
| 2 GB (Q2) | 512MB | 1536MB | 8MB | 128MB | 50 |
| 4 GB (Q4) | 1GB | 3GB | 8MB | 256MB | 100 |
| 8 GB (Q8, C8) | 2GB | 6GB | 16MB | 512MB | 100 |
| 16 GB (Q16, C16) | 4GB | 12GB | 32MB | 1GB | 100 |
| 32 GB (C32) | 8GB | 24GB | 64MB | 1GB | 100 |
If your app runs on the same VPS, it needs part of that RAM. Our rule of thumb for a shared server: about 15% of RAM for shared_buffers, half of RAM for effective_cache_size, and only as many connections as your app’s pool opens.
Apply the settings with a drop-in file
Ubuntu’s postgresql.conf has include_dir = 'conf.d' near its end, added by postgresql-common, so every .conf file in that directory is read after the settings above it and overrides them. One small file keeps your changes in a single place you can copy to the next server. For a 4 GB VPS:
sudo tee /etc/postgresql/18/main/conf.d/10-memory.conf <<'EOF'
# Starting values for a 4 GB VPS that runs only PostgreSQL
shared_buffers = 1GB
effective_cache_size = 3GB
work_mem = 8MB
maintenance_work_mem = 256MB
max_connections = 100
EOF
sudo systemctl restart postgresql@18-main
sudo -u postgres psql -c "SHOW shared_buffers;" -c "SHOW work_mem;"
shared_buffers and max_connections only change at a restart; the other three also apply after sudo systemctl reload postgresql@18-main. If the server does not come back, the reason is in /var/log/postgresql/postgresql-18-main.log. Avoid mixing methods: ALTER SYSTEM writes to postgresql.auto.conf, which is read last and wins over your drop-in.
What about random_page_cost? The wiki notes that many admins lower it on fast storage, but lists it last: check autovacuum, statistics and memory first, and change it only when EXPLAIN shows a plan you can prove is wrong.
Tip: On 1 GB and 2 GB plans, a swap file is a cheap safety net against the out-of-memory killer during a spike. Swap is not extra RAM: if the server swaps all day, move up a size.
How do you back up PostgreSQL on a VPS? pg_dump vs pg_basebackup
The PostgreSQL docs describe three approaches: SQL dumps, file-system-level copies and continuous archiving. On a single VPS, two bundled tools cover nearly every case:
pg_dump + pg_ | pg_basebackup | |
|---|---|---|
| What it copies | One database as a logical archive; roles come from pg_dumpall | Every file of the whole cluster (physical copy) |
| Restores into a newer major version | Yes; the docs call this an important advantage | No; file-level backups are “extremely server-version-specific” |
| Point-in-time recovery | No, only the moment of the dump | Yes, together with WAL archiving |
| Restore a single table | Yes, with pg_restore | No, the whole cluster |
| Needs | Read access to what it dumps; runs while the app writes, without blocking readers or writers | A REPLICATION role or superuser and a replication line in pg_hba.conf (Ubuntu’s default allows local) |
| Fits | Most small VPS databases, nightly | Larger databases, PITR, building a replica |
The pg_dump reference adds a caveat: “except in simple cases, pg_dump is generally not the right choice for taking regular backups of production databases.” A small app database restored from last night’s dump is such a simple case. Once losing a day of writes is unacceptable, add pg_basebackup with continuous archiving for point-in-time recovery.
A nightly dump with a systemd timer
This script dumps every database in the compressed custom format plus the cluster-wide roles, and keeps a week of files on the server. It runs as the postgres user, so peer authentication logs it in without a password. First create a directory only postgres can read:
sudo install -d -o postgres -g postgres -m 700 /var/backups/postgresql
sudo tee /usr/local/bin/pg-nightly-dump <<'EOF'
#!/bin/bash
# Dump every database plus roles; keep 7 days. Runs as the postgres user.
set -euo pipefail
umask 077
dest=/var/backups/postgresql
stamp=$(date +%F-%H%M)
pg_dumpall --globals-only > "$dest/globals-$stamp.sql"
for db in $(psql -AtX -c "SELECT datname FROM pg_database WHERE datallowconn AND NOT datistemplate"); do
pg_dump -Fc -f "$dest/$db-$stamp.dump" "$db"
done
find "$dest" -type f -mtime +7 -delete
EOF
sudo chmod 755 /usr/local/bin/pg-nightly-dump
Create a service that runs it as postgres, and a timer that starts the service at 03:15 every night:
sudo tee /etc/systemd/system/pg-nightly-dump.service <<'EOF'
[Unit]
Description=Dump all PostgreSQL databases
[Service]
Type=oneshot
User=postgres
ExecStart=/usr/local/bin/pg-nightly-dump
EOF
sudo tee /etc/systemd/system/pg-nightly-dump.timer <<'EOF'
[Unit]
Description=Nightly PostgreSQL dump
[Timer]
OnCalendar=*-*-* 03:15:00
Persistent=true
[Install]
WantedBy=timers.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now pg-nightly-dump.timer
Run it once by hand, then check the files and the log:
sudo systemctl start pg-nightly-dump.service
sudo ls -lh /var/backups/postgresql
journalctl -u pg-nightly-dump.service -n 20 --no-pager
Persistent=true catches up on a run the server missed while it was off. The timer section of our systemd guide explains OnCalendar and Persistent in depth. The script assumes database names without spaces.
A dump on the same disk disappears with the server, so copy /var/backups/postgresql off the VPS every night, for example with restic to S3-compatible object storage, which adds encryption and retention. An Uptime Kuma push monitor can alert you when the nightly job stops reporting in.
A physical base backup with pg_basebackup
pg_basebackup copies the whole cluster over a replication connection. Ubuntu’s default pg_hba.conf already allows local replication connections for the postgres user, so this works as is. Give base backups their own directory, so the dump script’s seven-day cleanup does not touch them:
sudo install -d -o postgres -g postgres -m 700 /var/backups/pg-base
sudo -u postgres pg_basebackup -D /var/backups/pg-base/base-$(date +%F) -Ft -z -P
The result is a directory with base.tar.gz, pg_wal.tar.gz and a backup_manifest. It restores only into the same major version, and without WAL archiving it restores to the moment the backup finished, not to any point after it.
Prove the restore on an hourly server
A backup you have never restored is a guess. Deploy a Quartz Q2, billed by the hour at $0.02/hour, install the same PostgreSQL major version or a newer one as above, and copy last night’s globals file and dump into your home directory (with scp, or from your restic repository). Then restore:
sudo -u postgres psql -X < ~/globals-2026-10-03-0315.sql
sudo -u postgres pg_restore --create -d postgres < ~/appdb-2026-10-03-0315.dump
sudo -u postgres psql -d appdb -c "\dt"
The globals file reports that role postgres already exists; that error is harmless. pg_restore --create creates appdb and restores into it. Restore into a newer major version and you have also rehearsed your next upgrade.
A two-hour drill costs $0.04, paid from the initial credit the server is ordered with. Delete the server when you finish, rather than only stopping it. A stopped server is still billed, because its vCPU, memory, disk and IP addresses stay reserved for you; only deleting the server stops billing.
How do you upgrade PostgreSQL on Ubuntu?
Minor releases: apt, every quarter
Minor releases, such as 18.4 to 18.6 (18.5 was never released), fix bugs and security issues without changing the data format. The project aims to ship at least one each quarter, targeting the second Thursday of February, May, August and November; the next is due on November 12, 2026. Installing one means replacing the binaries and restarting, which apt does for you, so expect a brief disconnect:
sudo apt update
sudo apt upgrade
Ubuntu publishes its postgresql-16 updates to the security pocket (16.15 landed there on August 20, 2026), so unattended-upgrades installs them, because its default Allowed-Origins list holds Ubuntu’s own archives. PGDG packages are not on that list, so on Option B, run the upgrade yourself after each release date. Read the release notes first: the 18.6 notes, for example, say its first three security fixes may need configuration adjustments or data cleanups after you update.
Major upgrades: from 16 to 18 with pg_upgradecluster
A new major version changes the on-disk format, so the data must be dumped and reloaded or converted with pg_upgrade. Ubuntu’s postgresql-common wraps both in pg_upgradecluster, which copies your configuration to the new cluster and swaps the ports, so clients keep using 5432. Take a fresh dump, then:
1. Install the new version. Add the PGDG repository (Option B), then:
sudo apt install postgresql-18
pg_lsclusters
2. Remove an empty target cluster, if one appeared. postgresql-common before version 269 (Ubuntu 24.04 ships 257) creates an empty 18/main on port 5433 when postgresql-18 installs; newer versions, such as the PGDG repository’s, create one only on a server with no clusters. pg_upgradecluster stops with “target cluster 18/main already exists”, so only if pg_lsclusters lists an empty 18/main:
sudo pg_dropcluster 18 main --stop
3. Upgrade. The default method dumps and reloads, which is slow but safe:
sudo pg_upgradecluster -v 18 16 main
4. Check and test. pg_lsclusters should now show 18/main online on 5432 and 16/main down on 5433. Test your app, then refresh the planner statistics as the pg_upgrade docs recommend:
sudo -u postgres vacuumdb --all --analyze-in-stages --missing-stats-only
sudo -u postgres vacuumdb --all --analyze-only
5. Remove the old version once you are satisfied:
sudo pg_dropcluster 16 main
sudo apt purge postgresql-16 postgresql-client-16
| Method | Option | Speed | Extra disk during the upgrade | Old cluster afterwards |
|---|---|---|---|---|
| dump (default) | none | Slowest; rewrites all data and indexes | About one more copy of your data | Kept on another port, set to manual start |
| upgrade | -m upgrade | Faster; pg_upgrade copies the files | About one more copy of your data | Kept on another port, set to manual start |
| link | -m link | Fastest; hard links, no copying | Little | Unusable once the new cluster starts |
One detail for 18: with the PGDG repository’s pg_upgradecluster, the dump method gives the new cluster data checksums, the version 18 default. Upgrade and link keep the old cluster’s setting (off on a 16 cluster Ubuntu created), because pg_upgrade requires matching checksum settings. Rehearse the whole run on an hourly server first: a three-hour rehearsal on a Quartz Q4 costs $0.09.
How do you monitor PostgreSQL on a VPS?
On a small database server, watch four things: whether it runs, how many connections it holds, which queries are slow, and how full the disk is. You can check all four without a monitoring stack. From the shell:
pg_lsclusters
sudo tail -n 50 /var/log/postgresql/postgresql-18-main.log
df -h /var/lib/postgresql
Then, inside sudo -u postgres psql:
-- Connections by state (compare the total with max_connections)
SELECT state, count(*) FROM pg_stat_activity GROUP BY state;
-- Queries running for longer than a minute
SELECT pid, now() - query_start AS runtime, left(query, 60) AS query
FROM pg_stat_activity
WHERE state = 'active' AND now() - query_start > interval '1 minute'
ORDER BY runtime DESC;
-- Database sizes
SELECT datname, pg_size_pretty(pg_database_size(datname)) AS size
FROM pg_database ORDER BY pg_database_size(datname) DESC;
-- Share of reads served from shared_buffers
SELECT datname, round(100.0 * blks_hit / nullif(blks_hit + blks_read, 0), 1) AS hit_pct
FROM pg_stat_database WHERE datname IS NOT NULL;
The statistics docs note that blks_hit counts only PostgreSQL’s own buffer cache, not the operating system’s, so a lower hit_pct does not prove disk reads. Watch the trend instead: if it falls as the database grows, the hot data no longer fits in shared_buffers.
Find slow queries with pg_stat_statements
The pg_stat_statements extension ships with the server package. It needs shared memory, so load it at startup, restart, and create the extension in the database you want to inspect:
echo "shared_preload_libraries = 'pg_stat_statements'" | sudo tee /etc/postgresql/18/main/conf.d/30-pg-stat-statements.conf
sudo systemctl restart postgresql@18-main
sudo -u postgres psql -d appdb -c "CREATE EXTENSION IF NOT EXISTS pg_stat_statements;"
The five statements that cost the most total time:
SELECT calls, round(total_exec_time) AS total_ms, round(mean_exec_time::numeric, 1) AS mean_ms,
left(query, 60) AS query
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 5;
To also see slow statements in the server log, set log_min_duration_statement. With 500ms, every statement that runs that long or longer is logged with its duration, and a reload applies it:
echo "log_min_duration_statement = 500ms" | sudo tee /etc/postgresql/18/main/conf.d/40-slow-log.conf
sudo systemctl reload postgresql@18-main
What size VPS does PostgreSQL need, and what does it cost?
PostgreSQL starts with small defaults (128 MB of shared_buffers), so the server itself rarely decides the plan. Your hot data does: the tables and indexes your queries touch all the time. Keep that in RAM and most reads never reach the disk.
Our sizing reasoning, not a PostgreSQL rule: on a database-only server, the hot data should fit in roughly 3/4 of RAM (shared_buffers plus the OS cache). Disk must hold your data, up to 1 GB of WAL with the default max_wal_size, a week of local dumps, and room for a second copy of the data during a dump-based major upgrade.
| Workload | Hot data (our estimate) | Plan | Hourly | Monthly cap |
|---|---|---|---|---|
| Learning, a dev database, a restore drill | Under 0.5 GB | Quartz Q1 (1 vCPU, 1 GB, 25 GB NVMe) | $0.01/hour | $5.00 |
| App and database on one server | Up to about 1 GB | Quartz Q2 (1 vCPU, 2 GB, 50 GB NVMe) | $0.02/hour | $10.00 |
| Dedicated database for a small production app | Up to about 3 GB | Quartz Q4 (2 vCPU, 4 GB, 80 GB NVMe) | $0.03/hour | $15.00 |
| Busier app, more connections, reporting queries | Up to about 6 GB | Quartz Q8 (4 vCPU, 8 GB, 160 GB NVMe) | $0.05/hour | $25.00 |
| Heavy queries all day that need steady CPU | Up to about 6 GB | Chrono C8 (2 dedicated vCPU, 8 GB, 100 GB NVMe) | $0.07/hour | $35.00 |
| Larger working set | Up to about 12 GB | Chrono C16 (4 dedicated vCPU, 16 GB, 200 GB NVMe) | $0.13/hour | $65.00 |
To measure your own hot data, start with the database sizes from the monitoring queries: if the whole database fits in 3/4 of RAM, so does the hot part. Quartz plans share their vCPUs; Chrono plans pin dedicated vCPUs to your server, which suits a database that runs heavy queries around the clock.
A production database runs 24/7, and you do not pick a separate plan for that. The same server is billed by the hour, and its charges stop at the plan’s monthly price in each billing period (one month from your order date), which makes it a monthly VPS with no contract and no prepayment. Short jobs, such as restore drills, upgrade rehearsals and load tests, pay only for their hours. One plan across all four:
| Duration | Hours on the meter | Cost $0.03 | Note |
|---|---|---|---|
| 3 hours | 3 | $0.09 | |
| 1 day | 24 | $0.72 | |
| 7 days | 168 | $5.04 | |
| 30 days | 720 | $15.00 | Capped at the monthly price |
The hourly vs monthly guide explains why a capped hourly bill never costs more than paying by the month, and the VPS cost calculator prices any other duration. Put the database in the same city as the app: every query is a network round trip, so an app in Europe talking to a database in North America pays the transatlantic delay on each one. The VPS location guide has the latency math. HourlyVPS deploys in Istanbul today, and New York is coming soon; see locations.
How billing works: Every server is billed by the hour: the plan’s hourly rate is deducted from the server’s prepaid balance for every hour it exists, powered on or off, until you delete it. Ordering a server takes an initial credit of $5 ($9 for Chrono C32, $16 for Chrono C64). It is prepaid credit, not a fee: the server’s hourly usage is deducted from it, and when it runs out you top up. For a database that must stay up, one rule matters most: When a server’s balance is down to about 24 hours of usage, you get a top-up invoice; at zero balance the server is suspended, and a suspended server is deleted 7 days later. Account top-ups start at $5. The hourly billing guide walks through the monthly cap with worked examples, and pricing lists every plan’s hourly price and cap.
PostgreSQL connection errors on a VPS and how to fix them
| Error or symptom | Likely cause | Fix |
|---|---|---|
connection to server on socket "/var/ | The cluster is down, or runs on another port (5433 after an upgrade) | pg_, then sudo systemctl start postgresql@ and read the log |
Peer authentication failed for user "appuser" | psql used the Unix socket, where Ubuntu uses peer authentication, from a Linux user with a different name | Add -h 127. to log in with the password |
Connection refused with Is the server running on that host and accepting TCP/ | listen_addresses is still localhost, or the server was not restarted after the change | sudo ss -ltnp | grep 5432; fix the drop-in and restart |
| The connection times out | UFW, or a firewall on the client side, drops the packets | sudo ufw status; allow the client’s IP on 5432 |
no pg_ | No line matches. “no encryption” means the client did not use TLS, and your line is hostssl | Connect with sslmode=, or correct the address, database or user in the line, then reload |
password authentication failed for user "appuser" | Wrong password, or the role has none | Set it again with \password appuser in sudo -u postgres psql |
permission denied for schema public | PostgreSQL 15 and later no longer let every role create objects in public | ALTER DATABASE appdb OWNER TO appuser; or GRANT CREATE ON SCHEMA public TO appuser; |
sorry, too many clients already | max_connections is used up, often by idle connections | Check pg_; cap the app’s pool or add a pooler such as PgBouncer before raising max_connections |
| The server will not start after a config change | A typo, or a memory setting larger than the server can provide | Read the last lines of /var/, fix the drop-in, restart |
Retiring a database server later? Work through the checklist before you delete a VPS first: a final dump copied off the server, and the firewall allowlists on other machines that still trust its IP.
Deploy this setup
Run PostgreSQL 18 for your app 24/7
Quartz Q4 · 2 shared vCPU · 4 GB RAM · 80 GB NVMe · Istanbul
- Per hour$0.03/hour
- Per day (24 h)$0.72/day
- Monthly cap$15.00/monthFor this job
Starts with a $5 initial credit, which goes into the server’s balance and pays for its hours.
Billed by the hour, never more than $15.00 per billing period. Delete the server and billing stops.



