Patroni is a Python-based high-availability solution for PostgreSQL that uses a distributed configuration store — in this tutorial, etcd — to manage leader election, automatic failover, and cluster membership. On RHEL 8, both Patroni and etcd are available through the EPEL repository, making the installation straightforward. This guide walks through installing PostgreSQL, etcd, and Patroni, writing the Patroni configuration file, bootstrapping the cluster, verifying replication, and fronting the cluster with HAProxy to separate read-write traffic (primary) from read-only replicas. The result is a self-healing PostgreSQL cluster that automatically promotes a replica if the primary fails.

Prerequisites

  • Three RHEL 8 nodes (or VMs) for a proper quorum: pg1 (primary), pg2, pg3 (replicas) — a single-node setup is used for demonstration but is not suitable for production
  • EPEL 8 repository enabled on all nodes
  • PostgreSQL 14 repository configured (dnf install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x86_64/pgdg-redhat-repo-latest.noarch.rpm)
  • All nodes able to reach each other on ports 2379, 2380 (etcd), 5432 (PostgreSQL), 8008 (Patroni REST API), and 5000/5001 (HAProxy)
  • firewalld running and managing the active zone

Step 1 — Install PostgreSQL, etcd, and Patroni

Disable the built-in AppStream PostgreSQL module to prevent conflicts with the upstream PGDG packages, then install all components:

# Enable EPEL
dnf install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm

# Add PostgreSQL 14 upstream repo and disable the AppStream module
dnf install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x86_64/pgdg-redhat-repo-latest.noarch.rpm
dnf -qy module disable postgresql

# Install PostgreSQL server
dnf install -y postgresql14 postgresql14-server postgresql14-contrib

# Install etcd and Patroni with the etcd provider
dnf install -y etcd patroni patroni-etcd

# Verify versions
postgres --version
etcd --version
patroni --version

Step 2 — Configure and Start etcd

Configure etcd on each node. On a single-node demonstration, the cluster has only one member. Adjust ETCD_INITIAL_CLUSTER and ETCD_ADVERTISE_CLIENT_URLS with the actual IP of your node:

cat > /etc/etcd/etcd.conf << 'EOF'
ETCD_NAME="etcd1"
ETCD_DATA_DIR="/var/lib/etcd/default.etcd"
ETCD_LISTEN_PEER_URLS="http://0.0.0.0:2380"
ETCD_LISTEN_CLIENT_URLS="http://0.0.0.0:2379"
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://YOUR_NODE_IP:2380"
ETCD_ADVERTISE_CLIENT_URLS="http://YOUR_NODE_IP:2379"
ETCD_INITIAL_CLUSTER="etcd1=http://YOUR_NODE_IP:2380"
ETCD_INITIAL_CLUSTER_STATE="new"
ETCD_INITIAL_CLUSTER_TOKEN="patroni-etcd-cluster"
EOF

systemctl enable --now etcd
systemctl status etcd

# Verify etcd is healthy
etcdctl endpoint health
etcdctl member list

Step 3 — Write the Patroni Configuration File

Patroni is configured via a single YAML file. Patroni will bootstrap a new PostgreSQL cluster on first start using the settings defined under bootstrap.dcs:

cat > /etc/patroni.yml << 'EOF'
scope:     postgres-ha
namespace: /db/
name:      pg1

etcd3:
  hosts: "YOUR_NODE_IP:2379"

bootstrap:
  dcs:
    ttl: 30
    loop_wait: 10
    retry_timeout: 10
    maximum_lag_on_failover: 1048576
    postgresql:
      use_pg_rewind: true
      parameters:
        wal_level: replica
        hot_standby: "on"
        max_wal_senders: 10
        max_replication_slots: 10
        wal_log_hints: "on"

  initdb:
    - encoding: UTF8
    - data-checksums

  pg_hba:
    - host replication replicator 127.0.0.1/32 md5
    - host replication replicator 0.0.0.0/0    md5
    - host all         all         0.0.0.0/0    md5

  users:
    admin:
      password: "AdminPassword123!"
      options:
        - createrole
        - createdb

postgresql:
  listen:        "0.0.0.0:5432"
  connect_address: "YOUR_NODE_IP:5432"
  data_dir:    /var/lib/pgsql/14/data
  bin_dir:     /usr/pgsql-14/bin
  pgpass:      /tmp/pgpass0
  authentication:
    replication:
      username: replicator
      password: "ReplicaPassword123!"
    superuser:
      username: postgres
      password: "PostgresPassword123!"

restapi:
  listen:          "0.0.0.0:8008"
  connect_address: "YOUR_NODE_IP:8008"

tags:
  nofailover:    false
  noloadbalance: false
  clonefrom:     false
  nosync:        false
EOF

chown postgres:postgres /etc/patroni.yml
chmod 600 /etc/patroni.yml

Step 4 — Open Firewall Ports and Start Patroni

Allow all required ports through firewalld, then start the Patroni service. On first start Patroni bootstraps a fresh PostgreSQL cluster:

# Open ports for etcd, PostgreSQL, and Patroni REST API
firewall-cmd --permanent --add-port=2379/tcp
firewall-cmd --permanent --add-port=2380/tcp
firewall-cmd --permanent --add-port=5432/tcp
firewall-cmd --permanent --add-port=8008/tcp
firewall-cmd --permanent --add-port=5000/tcp
firewall-cmd --permanent --add-port=5001/tcp
firewall-cmd --reload

# Start Patroni (it manages PostgreSQL — do not use postgresql-14.service)
systemctl enable --now patroni
systemctl status patroni

# Watch the bootstrap log
journalctl -u patroni -f --no-pager

Step 5 — Inspect the Cluster with patronictl

patronictl is the management CLI for Patroni clusters. Use it to view the cluster topology, trigger failovers, and manage configuration:

# List all cluster members — shows role (Leader/Replica), state, and lag
patronictl -c /etc/patroni.yml list

# Show current cluster configuration stored in etcd
patronictl -c /etc/patroni.yml show-config

# Query the Patroni REST API directly
curl -s http://YOUR_NODE_IP:8008/patroni | python3 -m json.tool

# Check the primary endpoint
curl -s http://YOUR_NODE_IP:8008/primary

# Check replica endpoint (returns 200 on replicas, 503 on primary)
curl -s http://YOUR_NODE_IP:8008/replica

# Verify PostgreSQL replication status (run as postgres user)
sudo -u postgres psql -c "SELECT * FROM pg_stat_replication;"

Step 6 — Configure HAProxy for Read/Write Splitting

HAProxy uses Patroni’s REST API health-check endpoints to route connections: port 5000 always connects to the current primary, port 5001 distributes read-only queries across all healthy replicas:

# Install HAProxy
dnf install -y haproxy

cat >> /etc/haproxy/haproxy.cfg << 'EOF'

#---------------------------------------------------------------------
# Patroni primary — read/write endpoint on port 5000
#---------------------------------------------------------------------
listen postgres_primary
    bind *:5000
    mode tcp
    option tcp-check
    default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
    # Patroni returns HTTP 200 on /primary for the current leader
    option httpchk GET /primary
    http-check expect status 200
    server pg1 YOUR_NODE_IP:5432 check port 8008
    # Add pg2, pg3 for multi-node clusters:
    # server pg2 PG2_IP:5432 check port 8008
    # server pg3 PG3_IP:5432 check port 8008

#---------------------------------------------------------------------
# Patroni replicas — read-only endpoint on port 5001
#---------------------------------------------------------------------
listen postgres_replicas
    bind *:5001
    mode tcp
    balance roundrobin
    option tcp-check
    default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
    # Patroni returns HTTP 200 on /replica for healthy replicas
    option httpchk GET /replica
    http-check expect status 200
    server pg1 YOUR_NODE_IP:5432 check port 8008
    # server pg2 PG2_IP:5432 check port 8008
    # server pg3 PG3_IP:5432 check port 8008
EOF

# Validate the HAProxy configuration
haproxy -c -f /etc/haproxy/haproxy.cfg

systemctl enable --now haproxy
systemctl status haproxy

# Test connectivity through HAProxy
psql -h 127.0.0.1 -p 5000 -U postgres -c "SELECT pg_is_in_recovery();"
# Should return: f (false) — this is the primary

Conclusion

You have built a Patroni-managed PostgreSQL cluster on RHEL 8 with etcd as the distributed configuration store, automatic failover and leader election, and HAProxy providing separate read-write (port 5000) and read-only (port 5001) endpoints backed by Patroni REST API health checks. Patroni’s pg_rewind support means a demoted primary can quickly resync from the new leader rather than re-cloning from scratch, minimizing failover downtime. For production deployments run three etcd nodes for quorum, configure pg_hba.conf with more restrictive ACLs, and add TLS certificates to both the Patroni REST API and PostgreSQL connections.

Next steps: Securing Patroni REST API with TLS on RHEL 8, Monitoring a Patroni PostgreSQL Cluster with Prometheus and Grafana, and Performing a Manual Switchover with patronictl on RHEL 8.