# 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.

Source: https://rack4.io/tutorials/app-database-private-network · Updated: 2026-11-12

Difficulty: intermediate · Time: about 30 minutes · Tested on: Ubuntu 24.04 LTS

Prerequisites:

- 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](https://rack4.io/help/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](https://rack4.io/help/private-network#check-it-works) 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

1. 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.
2. From the app server, `ping -c 3 10.240.0.11` answers. If it doesn't, [If it doesn't connect](https://rack4.io/help/private-network#if-it-doesn-t-connect) goes through why.
3. PostgreSQL is installed on the database server: [Install PostgreSQL on Ubuntu](https://rack4.io/tutorials/install-postgresql-ubuntu).

## 1. Create the database and its user

On the database server:

```bash
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:

```bash
sudo -u postgres psql
```

```text
postgres=# \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:

```bash
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`:

```text
hostssl    appdb    app    10.240.0.10/32    scram-sha-256
hostnossl  all      all    10.240.0.0/16     reject
```

- The `hostssl` line lets the user `app` into `appdb` from the app server's private address only, with a password, and only over TLS. A plain `host` line would also accept the same login unencrypted.
- The `hostnossl ... reject` line 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:

```bash
sudo systemctl restart postgresql
sudo ss -ltn | grep 5432
```

You 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](https://rack4.io/help/firewall-with-ufw) on the database server (keep SSH open first, or you'll lock yourself out):

```bash
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 enable
```

The [panel firewall](https://rack4.io/help/server-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`):

```bash
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:

```bash
psql "host=10.240.0.11 dbname=appdb user=app sslmode=disable" -c "select 1;"
```

```text
FATAL:  pg_hba.conf rejects connection for host "10.240.0.10", user "app", database "appdb", no encryption
```

That 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=require`** encrypts 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-full`** also 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:

```text
postgresql://app:<password>@10.240.0.11:5432/appdb?sslmode=require
```

## 6. 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.conf` line**: `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 with `sslmode=disable` the `reject` line 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 was `Connection refused`.)
- **Local administration**: `sudo -u postgres psql` still logs in as `postgres`.
- **After `systemctl reload postgresql` and after a restart**: the app connects over TLS, and `sslmode=disable` is 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](https://rack4.io/help/backups-you-should-run) has `pg_dump` schedules, and [Restore a PostgreSQL database](https://rack4.io/tutorials/restore-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](https://rack4.io/help/private-network)), MariaDB or MySQL (the same layers apply: `bind-address`, user host rules, the firewall), and certificate checking with `verify-full`.
