Running PostgreSQL in production isn't just about having a single server — it's about high availability (HA), automatic failover, and connection pooling. With the right setup, you can ensure your databases survive node failures without downtime.
In this guide, we'll build a 3-node PostgreSQL HA cluster using Patroni, etcd, and PgBouncer, combined with HAProxy + Keepalived for load balancing and a virtual IP.
etcd: Distributed key-value store used by Patroni for consensus and leader election.
Patroni: Orchestrates PostgreSQL, manages replication, failover, and recovery.
PgBouncer: Connection pooler to reduce overhead from many client connections.
HAProxy + Keepalived: Provides a VIP for clients, always pointing to the active PostgreSQL leader.
Step 1. Preparing the Environment
For this setup, I'm running on Ubuntu 22.04.5 LTS (Jammy Jellyfish).
Update /etc/hosts for easier resolution:
Directory Layout and Tablespaces
I created dedicated directories for PostgreSQL data, WAL, logs, and optional tablespaces:
/pg/pgdata→ main PostgreSQL data directory/pg/pgwal→ separate storage for WAL files (recommended for performance and crash recovery)/pg/pglogs→ PostgreSQL logs/tablespaces/…→ optional directories for custom tablespaces Tablespaces let you place different databases or schemas on different storage volumes.
Important: The same directory structure (/pg/pgdata, /pg/pgwal, /pg/pglogs, and any /tablespaces/…) must exist on all Patroni nodes.
(Optional) To create a tablespace in PostgreSQL, you'd run:
Install required packages on all Patroni nodes:
source: https://www.postgresql.org/download/linux/ubuntu/
Install etcd, PgBouncer, and Patroni:
Step 2. Configuring etcd
On each etcd node, configure /etc/default/etcd:
Example for patroni1:
Change etcd default config path:
Repeat with node-specific IPs and names.
Show etcd cluster health:
Step 3. Configuring Patroni
Add Patroni system service:
Each node has a Patroni config (/etc/patroni/patroni.yml). Example (for patroni1)
patroni2 — /etc/patroni/patroni.yml
(same as above but name, connect_address, and IPs updated to 192.168.50.12)
patroni3 — /etc/patroni/patroni.yml
(same as above but name, connect_address, and IPs updated to 192.168.50.13)
Run Patroni as a systemd service:
Step 4. Setting up PgBouncer
Each node runs PgBouncer at port 6432:
/etc/pgbouncer/pgbouncer.ini
/etc/pgbouncer/users.txt
Enable and start:
The /etc/pgbouncer/pgbouncer.ini and /etc/pgbouncer/userlist.txt files must be created and configured on each Patroni node (patroni1, patroni2, patroni3).
Transaction vs Session Pooling
Choosing the right pool mode is critical:
- transaction mode (recommended for microservices):
- Each query/transaction borrows a connection and releases it immediately.
- Great for stateless, API-driven apps.
- GUI tools like DBeaver/pgAdmin may fail because they expect a stable session.
- That's why I added a catch-all
*rule withpool_mode=session, so I can still manage the cluster comfortably from GUI tools while keepingtransactionmode as the default for microservices.
**session**mode (recommended for interactive tools / long-lived sessions):- Client gets the same backend connection for the entire session.
- Required for tools that keep temporary tables, prepared statements, or cursors open.
- Less efficient for microservices.
Authentication Best Practices
Local PgBouncer (same host as Postgres) → auth_type = plain is acceptable since traffic never leaves the machine.
Remote PgBouncer (dedicated pooler nodes) → Use md5 or scram-sha-256 for stronger security.
Step 5. HAProxy + Keepalived
Install HAProxy and KeepAlived:
HAProxy forwards traffic to Patroni leader via PgBouncer:
/etc/haproxy/haproxy.cfg
repeat for haproxy1 and haproxy2.
HAProxy on each LB node is querying Patroni on all DB nodes (via the IPs and port 8008) to monitor their role. You can test this by visiting http://HAPROXY_IP/master in a browser or with curl — the primary will return HTTP 200 and some JSON status, replicas return 503.
Keepalived Configuration
We’ll configure two HAProxy nodes (haproxy1 as MASTER, haproxy2 as BACKUP).
haproxy1 — /etc/keepalived/keepalived.conf
haproxy2 — /etc/keepalived/keepalived.conf
Testing the Cluster
Check Patroni status:
Connect via VIP:
Simulate failover by stopping Patroni on the leader and watching HAProxy switch:
Conclusion
- Patroni + etcd handles PostgreSQL replication and leader election.
- PgBouncer provides lightweight connection pooling.
- HAProxy + Keepalived ensures seamless client access through a single VIP.