This guide helps you install and configure PostgreSQL on an Ubuntu/Debian VPS securely, suitable for production environments.
- 1. Install PostgreSQL
- 2. Create Database and User
- 3. Configure Security for Remote Access
- 4. Verify Connection
- Appendix: Transaction Isolation Explained
sudo apt update
sudo apt install postgresql postgresql-contribsudo systemctl start postgresql.serviceBy default, PostgreSQL does not start automatically when VPS reboots. Run this command to enable it:
sudo systemctl enable postgresqlsudo service postgresql statussudo -u postgres psqlCREATE USER <username> WITH ENCRYPTED PASSWORD '<password>';-- Set UTF-8 encoding for client
ALTER ROLE <username> SET client_encoding TO 'utf8';
-- Set default isolation level (see explanation below)
ALTER ROLE <username> SET default_transaction_isolation TO 'read committed';
-- Set timezone
ALTER ROLE <username> SET timezone TO 'UTC';CREATE DATABASE <database_name>
WITH OWNER = <username>
ENCODING = 'UTF8'
TEMPLATE = template0;\q💡 Replace
<username>,<password>,<database_name>with your actual values.
If you need to connect to PostgreSQL remotely (e.g., from your local machine or another server), follow these steps.
On the machine that needs to connect (your local machine), get the public IP:
curl ifconfig.meNote this IP, for example: 172.456.68.78
Open the config file:
sudo nano /etc/postgresql/16/main/postgresql.conf📝 Replace
16with your PostgreSQL version (check withls /etc/postgresql/)
Find and modify these lines:
# Only listen on localhost and server's public IP
listen_addresses = 'localhost,<SERVER_PUBLIC_IP>'
port = 5432
# REQUIRED: Enable SSL to encrypt connections
ssl = on
⚠️ NEVER uselisten_addresses = '*'in production!
This file determines who is allowed to connect to PostgreSQL.
sudo nano /etc/postgresql/16/main/pg_hba.conf# host all all 0.0.0.0/0 md5 <-- DANGEROUS! Allows any IP
# Remote access - ONLY specific IP + require SSL
hostssl all all 172.456.68.78/32 scram-sha-256
Explanation:
| Component | Meaning |
|---|---|
hostssl |
Require SSL connection |
all (first) |
Applies to all databases |
all (second) |
Applies to all users |
172.456.68.78/32 |
Only this exact IP (/32 = single IP) |
scram-sha-256 |
Strongest authentication method |
💡 Security tip: Replace
allwith specific database and user names to limit access.
# Allow specific IP to access port 5432
sudo ufw allow from 172.456.68.78 to any port 5432
# Block all other IPs
sudo ufw deny 5432Check firewall status:
sudo ufw status🛡️ Two layers of protection: Firewall (UFW) + pg_hba.conf = Double security
sudo systemctl restart postgresqlsudo ss -lntp | grep 5432Expected output:
LISTEN 0 244 <SERVER_PUBLIC_IP>:5432 *:*
LISTEN 0 244 127.0.0.1:5432 *:*
psql -h <SERVER_PUBLIC_IP> -U <username> -d <database_name>When multiple requests/users read and write to the database simultaneously, these issues can occur:
| Issue | Description |
|---|---|
| Dirty read | Reading uncommitted data |
| Non-repeatable read | Reading the same record but getting different results |
| Phantom read | Subsequent query returns additional "ghost" records |
| Lost update | Two requests overwrite each other |
Isolation level determines whether to allow or prevent these phenomena.
| Level | Dirty Read | Non-repeatable | Phantom | Performance |
|---|---|---|---|---|
READ UNCOMMITTED |
❌ (PostgreSQL maps to READ COMMITTED) | 🚀 High | ||
READ COMMITTED |
❌ No | 🚀 High | ||
REPEATABLE READ |
❌ No | ❌ No | ⚖️ Medium | |
SERIALIZABLE |
❌ No | ❌ No | ❌ No | 🐢 Slow |
📌 PostgreSQL defaults to
READ COMMITTED- suitable for 90% of applications.
Rules:
- Each
SELECTonly sees committed data - Cannot see data being updated by another transaction that hasn't committed yet
Example:
Table accounts: id=1, balance=1000
┌─────────────────────────────────┬─────────────────────────────────┐
│ Transaction A │ Transaction B │
├─────────────────────────────────┼─────────────────────────────────┤
│ BEGIN; │ │
│ UPDATE accounts │ │
│ SET balance = 500 │ │
│ WHERE id = 1; │ │
│ -- not yet COMMIT │ │
│ │ SELECT balance FROM accounts │
│ │ WHERE id = 1; │
│ │ --> Result: 1000 (cannot see │
│ │ uncommitted data) │
│ COMMIT; │ │
│ │ SELECT balance FROM accounts │
│ │ WHERE id = 1; │
│ │ --> Result: 500 │
└─────────────────────────────────┴─────────────────────────────────┘
| Use Case | Recommended |
|---|---|
| Standard CRUD APIs | READ COMMITTED |
| Reports, statistics | READ COMMITTED |
| Orders, booking | REPEATABLE READ or SELECT ... FOR UPDATE |
| Finance, accounting | SERIALIZABLE |
ALTER ROLE app_user SET default_transaction_isolation TO 'read committed';Benefits:
- PostgreSQL still uses
READ COMMITTEDby default - However:
- Apps can override it arbitrarily
- Migrations / tools / scripts might set different isolation levels
- ✅ Ensures all connections from this user have consistent behavior
- ✅ Prevents developers/tools from accidentally setting different isolation levels
- ✅ Easier to debug and predict behavior