Put your app and its PostgreSQL database on two private servers
Run your database on its own server, reachable only from your app server over the private network, with a password, TLS and a firewall rule.
Tested on Ubuntu 24.04 LTS
Before you start
- Two Ubuntu 24.04 servers on your private network (the same host)
- Root or a user with sudo on both
- PostgreSQL installed on the database server
To give your app its own database server, put both servers on your private network, make PostgreSQL listen on the database server's private address, allow only the app server's private address in pg_hba.conf (over TLS only) and in the firewall, and connect from the app with sslmode=require. The database never listens on the public internet, and its traffic doesn't count toward your monthly transfer.
Each of the four layers (where PostgreSQL listens, its access rules, the firewall, the password) stops a different mistake, so this tutorial sets them all and then checks each one: the app gets in over TLS, an unencrypted login is refused, your other servers don't get in, and the public address doesn't answer at all.
What we tested: on 11 November 2026 we ran these steps with Ubuntu 24.04 and PostgreSQL 16 on three machines sharing a 10.240.0.0/16 network, the same range as Rack4's private network: an app server at 10.240.0.10, the database at 10.240.0.11, and a third server at 10.240.0.12. The machines were containers on one host rather than Rack4 servers, so the private network card itself wasn't part of the test; the private network guide covers checking that. On 12 November 2026 we ran steps 1 to 3 and 5 again in the same way (Ubuntu 24.04 containers, Ubuntu's PostgreSQL 16.15 package, the same addresses) to test the hostssl rules, the refused unencrypted connection, the rule order, \password, and reload and restart. The firewall wasn't part of that second run. Every result quoted below is from these runs.
The examples use these names. Replace them with yours:
| What | In this tutorial |
|---|---|
| App server's private address | 10.240.0.10 |
| Database server's private address | 10.240.0.11 |
| Database | appdb |
| Database user | app |
Before you start
- Both servers are on your private network, and each Private network page lists the other. Order the database server with Join my private network ticked, or join both from their Network tabs.
- From the app server,
ping -c 3 10.240.0.11answers. If it doesn't, If it doesn't connect goes through why. - PostgreSQL is installed on the database server: Install PostgreSQL on Ubuntu.
1. Create the database and its user
On the database server:
sudo -u postgres psql -c "CREATE ROLE app LOGIN" -c "CREATE DATABASE appdb OWNER app"The role is created without a password, so none is typed on the command line. Generate one (openssl rand -base64 24 works), then set it at psql's prompt:
sudo -u postgres psqlpostgres=# \password app
Enter new password for user "app":
Enter it again:
postgres=# \q\password reads the password without echoing it and sends PostgreSQL only its SCRAM hash, so the password never appears in your shell history, the process list or the server log. In our test the stored value began SCRAM-SHA-256$, and the log didn't contain the password. Keep it in the app's configuration, not in the repository.
2. Listen on the private address
PostgreSQL listens only on localhost out of the box. Add the private address, and only that:
sudo sed -i "s/^#\?listen_addresses.*/listen_addresses = 'localhost,10.240.0.11'/" /etc/postgresql/16/main/postgresql.conf(16 is the version Ubuntu 24.04 installs; ls /etc/postgresql shows yours.) Don't use '*': that would also listen on the public address, and only the firewall would stand in the way.
3. Allow only the app server, and only over TLS
Add these two lines to the end of /etc/postgresql/16/main/pg_hba.conf:
hostssl appdb app 10.240.0.10/32 scram-sha-256
hostnossl all all 10.240.0.0/16 reject- The
hostsslline lets the userappintoappdbfrom the app server's private address only, with a password, and only over TLS. A plainhostline would also accept the same login unencrypted. - The
hostnossl ... rejectline refuses every unencrypted connection from the private network, and says so in the error. Without it, an unencrypted connection would still be refused (no line would match), but this line makes that explicit.
PostgreSQL reads pg_hba.conf from the top and uses the first line that matches the connection type, database, user and address; later lines are never consulted. Ubuntu's default lines above yours are local ... peer (admin through sudo -u postgres psql) and host lines for 127.0.0.1 and ::1 only, so they don't match private-network traffic and local administration keeps working. Check the file for any earlier line that is broader, such as host all all 0.0.0.0/0, host all all 10.240.0.0/16 or host all all all. In our test, adding host all all 10.240.0.0/16 scram-sha-256 above the two lines let sslmode=disable back in (ssl was f). Remove such a line, or put your two lines above it. To see the rules PostgreSQL loaded, in order, run sudo -u postgres psql -c "select line_number, type, database, user_name, address, auth_method from pg_hba_file_rules".
Then restart (needed for listen_addresses; later pg_hba.conf edits need only sudo systemctl reload postgresql) and check where PostgreSQL listens:
sudo systemctl restart postgresql
sudo ss -ltn | grep 5432You should see 127.0.0.1:5432 and 10.240.0.11:5432, and no 0.0.0.0 or public address.
4. Firewall: allow the app server's address
With ufw on the database server (keep SSH open first, or you'll lock yourself out):
sudo ufw allow 22/tcp
sudo ufw allow from 10.240.0.10 to any port 5432 proto tcp
sudo ufw default deny incoming
sudo ufw enableThe panel firewall doesn't cover the private network, so this rule on the server is what limits which of your servers can reach the database.
5. Connect from the app
On the app server (sudo apt install postgresql-client for psql):
psql "host=10.240.0.11 dbname=appdb user=app sslmode=require" -c "select current_user, ssl from pg_stat_ssl where pid = pg_backend_pid();"In our test this answered app | t: logged in as app, over TLS. Now force an unencrypted connection from the same server, which should fail:
psql "host=10.240.0.11 dbname=appdb user=app sslmode=disable" -c "select 1;"FATAL: pg_hba.conf rejects connection for host "10.240.0.10", user "app", database "appdb", no encryptionThat shows the server requires TLS, whatever the client asks for. The private network itself isn't encrypted, so this matters.
Ubuntu's PostgreSQL turns TLS on with a self-signed certificate (ssl = on and the ssl-cert-snakeoil files in postgresql.conf), and the two client settings differ:
sslmode=requireencrypts the connection but doesn't check which server answered. It works with the self-signed certificate, and it's what this tutorial uses.sslmode=verify-fullalso checks that the server's certificate was signed by a CA you trust and that it names the host you connected to. It needs a certificate from your own CA on the server, issued for the name or address the app connects to, and the CA certificate on the app server (sslrootcert=).
Your app's connection string is the same thing in URL form:
postgresql://app:<password>@10.240.0.11:5432/appdb?sslmode=require6. Check each layer
These are the results from our test runs:
- The app server connects with
sslmode=require:connected as app, ssl=true. - The app server with
sslmode=disable:FATAL: pg_hba.conf rejects connection for host "10.240.0.10", user "app", database "appdb", no encryption. - Another of your servers, with no firewall rule:
connection to server at "10.240.0.11", port 5432 failed: timeout expired. The firewall dropped it. - The same server with a firewall rule but no
pg_hba.confline:FATAL: no pg_hba.conf entry for host "10.240.0.12", user "app", database "appdb", SSL encryption. PostgreSQL's own rules refused it, and withsslmode=disabletherejectline refused it. - The database server's public address:
timeout expired. PostgreSQL doesn't listen there, and the firewall drops it before that. (Without the firewall, the answer wasConnection refused.) - Local administration:
sudo -u postgres psqlstill logs in aspostgres. - After
systemctl reload postgresqland after a restart: the app connects over TLS, andsslmode=disableis still refused. The settings are in the configuration files, not just in memory.
Run the first four checks from your own servers after any change to the database server's configuration.
Backups and moves
Back the database up from the database server itself: Backups you should run has pg_dump schedules, and Restore a PostgreSQL database covers moving it to a new server. A server restored from a backup comes back without its private network card: join it again, check its private address with ip -br addr, and update the app's connection string and pg_hba.conf if it changed.
What we didn't test
The Rack4 private network card itself (covered in the private network guide), MariaDB or MySQL (the same layers apply: bind-address, user host rules, the firewall), and certificate checking with verify-full.