Deploying a High-Availability PostgreSQL 18 Cluster on RedHat 10 with Patroni

Introduction

PostgreSQL is one of the most powerful open-source relational databases, and PostgreSQL 18 brings even more performance, security, and scalability improvements. But how do you ensure high availability (HA) and automatic failover for your critical applications?

In this guide, we will walk you through setting up a PostgreSQL 18 cluster on three Red Hat Enterprise Linux 10 VMs using Patroni — a popular open-source tool for managing PostgreSQL HA clusters. All data on the leader node will be preserved and replicated to the other 2 nodes. By the end, you will have a fully redundant, self-healing database cluster ready for production workloads.

Why Patroni?

  • Automatic failover: No manual intervention needed if the primary node fails.
  • Synchronous replication: Data consistency across all nodes.
  • REST API: Easy monitoring and management.
  • Integration with etcd/consul: Distributed configuration and leader election.

Who is this for?

  • DevOps engineers
  • Database administrators
  • Sysadmins managing critical PostgreSQL workloads

Architecture

Production / Large-Scale Environment Design
For large-scale production environments managing multiple Patroni PostgreSQL clusters (e.g., up to 10 clusters with 2–4 replicas each), the established best practice is to deploy a dedicated, external etcd cluster. We implement this using a 3‑node etcd ensemble (running exclusively etcd) to serve as the centralized Distributed Configuration Store (DCS) for all database clusters. This decoupled architecture ensures optimal resource isolation, prevents I/O and CPU contention with database workloads, simplifies operational maintenance (e.g., backups, monitoring, and upgrades), and guarantees a robust Raft consensus quorum for stable leader elections across the entire PostgreSQL fleet.

Test / Development Environment
In contrast, our test and development systems deliberately consolidate the stack to reduce infrastructure footprint and simplify bootstrapping. In this environment, we run PostgreSQL, Patroni, and etcd collocated on the same 3 nodes. While this all‑in‑one setup is perfectly sufficient for validation, functional testing, and rapid prototyping, we strictly avoid this configuration in production due to the increased risk of resource starvation and cascading failures when a single node becomes overloaded.


Prerequisites

Before we begin, ensure you have the following:

Hardware/Software Requirements

RequirementDetails
Operating SystemRed Hat Enterprise Linux 10 (RHEL 10) on all 3 nodes (Minimum 4GB RAM, 2 vCPUs, 50GB storage per VM).
NetworkStatic IPs for all nodes, firewall rules allowing traffic on ports 2379 (etcd), 5432 (PostgreSQL), 8008 (Patroni REST API)
Root/Sudo AccessFull administrative privileges on both VMs

Assumptions

  • All three nodes are fresh RHEL 10 installations as described in this post.
  • You are comfortable with Linux command line and basic networking.
  • You have sudo/root access on both VMs.

1. Install PostgreSQL 18

First we need to install PostgreSQL 18 on all 3 nodes as described in this post: 1. Installing PostgreSQL 18 on RHEL 10.


2. Prepare the OS

2.1 Install the required OS packages

The following packages need to be installed on all nodes:

# run as root on all nodes
dnf -y update
dnf install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-10.noarch.rpm
dnf config-manager --set-enabled codeready-builder-for-rhel-10-x86_64-rpms
dnf config-manager --enable pgdg-rhel10-extras
dnf -y install patroni patroni-etcd etcd

Note: The patroni-etcd package provides the necessary dependencies for Patroni to communicate with etcd.

2.2 (Optional) Configure firewalld

If you have a active firewall (firewalld) on the nodes you need to open specific ports to allow communication:

# run as root on all nodes
firewall-cmd --permanent --add-port={2379,2380}/tcp   # etcd
firewall-cmd --permanent --add-port={5432,5433}/tcp   # PostgreSQL
firewall-cmd --permanent --add-port=8008/tcp          # Patroni API
firewall-cmd --reload

3. Setup and Start etcd

In a Patroni cluster, etcd serves as the distributed consensus store that acts as the cluster’s “control plane.” Its primary job is to manage the leader election lock, ensuring that only one PostgreSQL node holds the Primary role at any given time. It also stores the definitive cluster state—including the current leader, replica positions (LSNs), and configuration—which all Patroni nodes continuously watch to stay synchronized. By providing strong consistency and fault-tolerance, etcd guarantees that Patroni can execute safe, automatic failovers without the risk of “split-brain” scenarios. The following script can be run on any of the three nodes as root. It will ask for the member nodes of the cluster (the first one entered will be the leader node where data will be preserved) and will create command snippets that need to be run on each node to configure and start etcd (The script by itself doesn’t change anything on the system):

#!/bin/bash
# etcd-config-generator

echo "=== etcd cluster configuration generator ==="
read -p "Enter comma separated node names (e.g., lin1,lin2,lin3): " NODES
[ -z "$NODES" ] && echo "No nodes given." && exit 1

read -p "Cluster token [etcd-cluster-01]: " TOKEN
TOKEN="${TOKEN:-etcd-cluster-01}"
read -p "Cluster state [new]: " STATE
STATE="${STATE:-new}"

IFS=',' read -ra NODE_ARRAY <<< "$NODES"

# Reset arrays to avoid duplicate accumulation when script is re-sourced
NAMES=()
IPS=()

for node in "${NODE_ARRAY[@]}"; do
    node="$(echo "$node" | xargs)"          # trim whitespace
    [ -z "$node" ] && continue
    NAMES+=("$node")
    # Resolve IP
    ip="$(getent ahosts "$node" 2>/dev/null | head -n1 | awk '{print $1}')"
    if [ -z "$ip" ]; then
        if [[ "$node" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
            ip="$node"
        else
            echo "ERROR: Cannot resolve '$node' and it's not an IP." >&2
            exit 1
        fi
    fi
    IPS+=("$ip")
done

# Build initial cluster string
INITIAL_CLUSTER=""
for i in "${!NAMES[@]}"; do
    [ $i -gt 0 ] && INITIAL_CLUSTER+=","
    INITIAL_CLUSTER+="${NAMES[$i]}=http://${IPS[$i]}:2380"
done

# Generate snippets – each includes the systemctl start command
for i in "${!NAMES[@]}"; do
    name="${NAMES[$i]}"
    ip="${IPS[$i]}"
    echo "------------------------------------------------------------"
    echo "# On node $name (IP $ip), run the following block:"
    cat <<EOF
mkdir -p /etc/etcd
cat > /etc/etcd/etcd.conf <<'ETCDEOF'
# [Member]
ETCD_NAME="${name}"
ETCD_DATA_DIR="/var/lib/etcd/default.etcd"
ETCD_LISTEN_PEER_URLS="http://${ip}:2380"
ETCD_LISTEN_CLIENT_URLS="http://${ip}:2379,http://127.0.0.1:2379"

# [Clustering]
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://${ip}:2380"
ETCD_ADVERTISE_CLIENT_URLS="http://${ip}:2379"
ETCD_INITIAL_CLUSTER="${INITIAL_CLUSTER}"
ETCD_INITIAL_CLUSTER_TOKEN="${TOKEN}"
ETCD_INITIAL_CLUSTER_STATE="${STATE}"
ETCDEOF
systemctl enable etcd --now
EOF
    echo
done
Sample Script Output (click to expand):
=== etcd cluster configuration generator ===
Enter comma separated node names (e.g., lin1,lin2,lin3): lin6,lin7,lin8
Cluster token [etcd-cluster-01]:
Cluster state [new]:
------------------------------------------------------------
# On node lin6 (IP 11.1.1.186), run the following block:
mkdir -p /etc/etcd
cat > /etc/etcd/etcd.conf <<'ETCDEOF'
# [Member]
ETCD_NAME="lin6"
ETCD_DATA_DIR="/var/lib/etcd/default.etcd"
ETCD_LISTEN_PEER_URLS="http://11.1.1.186:2380"
ETCD_LISTEN_CLIENT_URLS="http://11.1.1.186:2379,http://127.0.0.1:2379"

# [Clustering]
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://11.1.1.186:2380"
ETCD_ADVERTISE_CLIENT_URLS="http://11.1.1.186:2379"
ETCD_INITIAL_CLUSTER="lin6=http://11.1.1.186:2380,lin7=http://11.1.1.195:2380,lin8=http://11.1.1.196:2380"
ETCD_INITIAL_CLUSTER_TOKEN="etcd-cluster-01"
ETCD_INITIAL_CLUSTER_STATE="new"
ETCDEOF
systemctl enable etcd --now

------------------------------------------------------------
# On node lin7 (IP 11.1.1.195), run the following block:
mkdir -p /etc/etcd
cat > /etc/etcd/etcd.conf <<'ETCDEOF'
# [Member]
ETCD_NAME="lin7"
ETCD_DATA_DIR="/var/lib/etcd/default.etcd"
ETCD_LISTEN_PEER_URLS="http://11.1.1.195:2380"
ETCD_LISTEN_CLIENT_URLS="http://11.1.1.195:2379,http://127.0.0.1:2379"

# [Clustering]
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://11.1.1.195:2380"
ETCD_ADVERTISE_CLIENT_URLS="http://11.1.1.195:2379"
ETCD_INITIAL_CLUSTER="lin6=http://11.1.1.186:2380,lin7=http://11.1.1.195:2380,lin8=http://11.1.1.196:2380"
ETCD_INITIAL_CLUSTER_TOKEN="etcd-cluster-01"
ETCD_INITIAL_CLUSTER_STATE="new"
ETCDEOF
systemctl enable etcd --now

------------------------------------------------------------
# On node lin8 (IP 11.1.1.196), run the following block:
mkdir -p /etc/etcd
cat > /etc/etcd/etcd.conf <<'ETCDEOF'
# [Member]
ETCD_NAME="lin8"
ETCD_DATA_DIR="/var/lib/etcd/default.etcd"
ETCD_LISTEN_PEER_URLS="http://11.1.1.196:2380"
ETCD_LISTEN_CLIENT_URLS="http://11.1.1.196:2379,http://127.0.0.1:2379"

# [Clustering]
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://11.1.1.196:2380"
ETCD_ADVERTISE_CLIENT_URLS="http://11.1.1.196:2379"
ETCD_INITIAL_CLUSTER="lin6=http://11.1.1.186:2380,lin7=http://11.1.1.195:2380,lin8=http://11.1.1.196:2380"
ETCD_INITIAL_CLUSTER_TOKEN="etcd-cluster-01"
ETCD_INITIAL_CLUSTER_STATE="new"
ETCDEOF
systemctl enable etcd --now

[root@lin6 ~]#

After the generated commands are run on all 3 nodes etcd should be active. You can verify the state of etcd with:

# run as root
etcdctl member list
# and with
etcdctl endpoint health --cluster

Expected Output:

[root@lin6 ~]# etcdctl member list
bbe914cd14e288f, started, lin7, http://11.1.1.195:2380, http://11.1.1.195:2379, false
2c828c931533228f, started, lin8, http://11.1.1.196:2380, http://11.1.1.196:2379, false
60432468aecdf222, started, lin6, http://11.1.1.186:2380, http://11.1.1.186:2379, false
[root@lin6 ~]# etcdctl endpoint health --cluster
http://11.1.1.186:2379 is healthy: successfully committed proposal: took = 3.529991ms
http://11.1.1.195:2379 is healthy: successfully committed proposal: took = 4.110628ms
http://11.1.1.196:2379 is healthy: successfully committed proposal: took = 12.164748ms
[root@lin6 ~]#

4. Setup and Start Patroni

Patroni is an open‑source “maestro” for PostgreSQL that automates high‑availability and replication management. Its primary job is to monitor the health of all PostgreSQL instances and orchestrate automatic failover—promoting a replica to primary if the current leader fails—without manual intervention. To coordinate this safely, Patroni relies on a distributed consensus store (etcd in our case) that holds the cluster’s state and manages leader election locks, ensuring every node agrees on who the primary is at all times. By combining robust health checks with a consistent control plane, Patroni gives you resilient, self‑healing PostgreSQL clusters that minimise downtime and data loss. The following steps will preserve the data on the leader node (the one entered first in the scripts node list), configure and start Patroni. Two replicas of the leader node will be created.

4.1 Create the PostgreSQL replication user

For the replication to work we need a PostgreSQL database user (rep_user) on the leader node:

# run as root on the leader node
su - postgres -c "psql -c \"create user rep_user with replication encrypted password 'changeme';\""

4.2 Configure and Start Patroni

First we need to stop and disable PostgreSQL 18 on all nodes. Also we disable automatic startup by systemd:

# run as root on all nodes
systemctl stop postgresql-18
systemctl disable postgresql-18

Now as we did for the etcd config we will run a script on one of the nodes that will create commands to run on each node. After that Patroni will be enabled and started:

#!/bin/bash
# patroni-config-generator – no bootstrap (manual user creation)

unset ORDERED_NAMES ORDERED_IPS UNIQUE_IPS

echo "=== Patroni cluster configuration generator ==="
echo "Note: The FIRST node you enter will be the initial leader."
read -p "Enter comma separated node names (e.g., lin1,lin2,lin3): " NODES
[ -z "$NODES" ] && echo "No nodes given." && exit 1

read -p "Cluster scope [pgcluster]: " SCOPE
SCOPE="${SCOPE:-pgcluster}"

read -p "Superuser password [changeme]: " SUPERPW
SUPERPW="${SUPERPW:-changeme}"

read -p "rep_user password [changeme]: " REPPW
REPPW="${REPPW:-changeme}"

read -p "Shared buffers [1GB]: " SHARED_BUFFERS
SHARED_BUFFERS="${SHARED_BUFFERS:-1GB}"

read -p "Max connections [200]: " MAX_CONNS
MAX_CONNS="${MAX_CONNS:-200}"

declare -A UNIQUE_IPS
ORDERED_NAMES=()
ORDERED_IPS=()

IFS=',' read -ra NODE_ARRAY <<< "$NODES"
for node in "${NODE_ARRAY[@]}"; do
    node="$(echo "$node" | xargs)"
    [ -z "$node" ] && continue
    ip="$(getent ahosts "$node" 2>/dev/null | head -n1 | awk '{print $1}')"
    if [ -z "$ip" ]; then
        if [[ "$node" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
            ip="$node"
        else
            echo "ERROR: Cannot resolve '$node' and it's not an IP." >&2
            exit 1
        fi
    fi
    if [ -z "${UNIQUE_IPS[$ip]}" ]; then
        UNIQUE_IPS["$ip"]="$node"
        ORDERED_NAMES+=("$node")
        ORDERED_IPS+=("$ip")
    fi
done

LEADER="${ORDERED_NAMES[0]}"

# Build etcd3 hosts list
ETCD3_HOSTS=""
for ip in "${!UNIQUE_IPS[@]}"; do
    ETCD3_HOSTS+="    - ${ip}:2379\n"
done

# pg_hba: local + replication + general
PG_HBA_LINES="  - local all postgres peer\n"
for ip in "${!UNIQUE_IPS[@]}"; do
    PG_HBA_LINES+="  - host replication rep_user ${ip}/32 md5\n"
done
PG_HBA_LINES+="  - host all all 0.0.0.0/0 md5"

for i in "${!ORDERED_NAMES[@]}"; do
    name="${ORDERED_NAMES[$i]}"
    ip="${ORDERED_IPS[$i]}"
    echo "------------------------------------------------------------"
    echo "# On node $name (IP $ip), run the following block:"

    if [ "$name" == "$LEADER" ]; then
        CLEANUP_CMD=""
    else
        CLEANUP_CMD="rm -rf /var/lib/pgsql/18/data"
    fi

    cat <<EOF
mkdir -p /etc/patroni
cat > /etc/patroni/patroni.yml <<'PATRONIEOF'
scope: ${SCOPE}
name: ${name}

etcd3:
  hosts:
$(echo -e "$ETCD3_HOSTS")

postgresql:
  listen: ${ip}:5432
  connect_address: ${ip}:5432
  data_dir: /var/lib/pgsql/18/data
  bin_dir: /usr/pgsql-18/bin

  replication:
    listen: ${ip}:5433
    connect_address: ${ip}:5433
    username: rep_user
    password: ${REPPW}

  superuser:
    username: postgres
    password: ${SUPERPW}

  parameters:
    shared_buffers: ${SHARED_BUFFERS}
    max_connections: ${MAX_CONNS}

  pg_hba:
$(echo -e "$PG_HBA_LINES")

restapi:
  listen: ${ip}:8008
  connect_address: ${ip}:8008
PATRONIEOF

mkdir -p /etc/systemd/system/patroni.service.d
cat > /etc/systemd/system/patroni.service.d/override.conf <<'OVERRIDEEOF'
[Unit]
After=etcd.service
Wants=etcd.service
OVERRIDEEOF

systemctl daemon-reload
$( [ -n "$CLEANUP_CMD" ] && echo "$CLEANUP_CMD" )
systemctl enable patroni --now
EOF
    echo
done
Sample Output (click to expand)
=== Patroni cluster configuration generator ===
Note: The FIRST node you enter will be the initial leader.
Enter comma separated node names (e.g., lin1,lin2,lin3): lin6,lin7,lin8
Cluster scope [pgcluster]:
Superuser password [changeme]:
rep_user password [changeme]:
Shared buffers [1GB]:
Max connections [200]:
------------------------------------------------------------
# On node lin6 (IP 11.1.1.186), run the following block:
mkdir -p /etc/patroni
cat > /etc/patroni/patroni.yml <<'PATRONIEOF'
scope: pgcluster
name: lin6

etcd3:
  hosts:
    - 11.1.1.186:2379
    - 11.1.1.196:2379
    - 11.1.1.195:2379

postgresql:
  listen: 11.1.1.186:5432
  connect_address: 11.1.1.186:5432
  data_dir: /var/lib/pgsql/18/data
  bin_dir: /usr/pgsql-18/bin

  replication:
    listen: 11.1.1.186:5433
    connect_address: 11.1.1.186:5433
    username: rep_user
    password: changeme

  superuser:
    username: postgres
    password: changeme

  parameters:
    shared_buffers: 1GB
    max_connections: 200

  pg_hba:
  - local all postgres peer
  - host replication rep_user 11.1.1.186/32 md5
  - host replication rep_user 11.1.1.196/32 md5
  - host replication rep_user 11.1.1.195/32 md5
  - host all all 0.0.0.0/0 md5

restapi:
  listen: 11.1.1.186:8008
  connect_address: 11.1.1.186:8008
PATRONIEOF

mkdir -p /etc/systemd/system/patroni.service.d
cat > /etc/systemd/system/patroni.service.d/override.conf <<'OVERRIDEEOF'
[Unit]
After=etcd.service
Wants=etcd.service
OVERRIDEEOF

systemctl daemon-reload

systemctl enable patroni --now

------------------------------------------------------------
# On node lin7 (IP 11.1.1.195), run the following block:
mkdir -p /etc/patroni
cat > /etc/patroni/patroni.yml <<'PATRONIEOF'
scope: pgcluster
name: lin7

etcd3:
  hosts:
    - 11.1.1.186:2379
    - 11.1.1.196:2379
    - 11.1.1.195:2379

postgresql:
  listen: 11.1.1.195:5432
  connect_address: 11.1.1.195:5432
  data_dir: /var/lib/pgsql/18/data
  bin_dir: /usr/pgsql-18/bin

  replication:
    listen: 11.1.1.195:5433
    connect_address: 11.1.1.195:5433
    username: rep_user
    password: changeme

  superuser:
    username: postgres
    password: changeme

  parameters:
    shared_buffers: 1GB
    max_connections: 200

  pg_hba:
  - local all postgres peer
  - host replication rep_user 11.1.1.186/32 md5
  - host replication rep_user 11.1.1.196/32 md5
  - host replication rep_user 11.1.1.195/32 md5
  - host all all 0.0.0.0/0 md5

restapi:
  listen: 11.1.1.195:8008
  connect_address: 11.1.1.195:8008
PATRONIEOF

mkdir -p /etc/systemd/system/patroni.service.d
cat > /etc/systemd/system/patroni.service.d/override.conf <<'OVERRIDEEOF'
[Unit]
After=etcd.service
Wants=etcd.service
OVERRIDEEOF

systemctl daemon-reload
rm -rf /var/lib/pgsql/18/data
systemctl enable patroni --now

------------------------------------------------------------
# On node lin8 (IP 11.1.1.196), run the following block:
mkdir -p /etc/patroni
cat > /etc/patroni/patroni.yml <<'PATRONIEOF'
scope: pgcluster
name: lin8

etcd3:
  hosts:
    - 11.1.1.186:2379
    - 11.1.1.196:2379
    - 11.1.1.195:2379

postgresql:
  listen: 11.1.1.196:5432
  connect_address: 11.1.1.196:5432
  data_dir: /var/lib/pgsql/18/data
  bin_dir: /usr/pgsql-18/bin

  replication:
    listen: 11.1.1.196:5433
    connect_address: 11.1.1.196:5433
    username: rep_user
    password: changeme

  superuser:
    username: postgres
    password: changeme

  parameters:
    shared_buffers: 1GB
    max_connections: 200

  pg_hba:
  - local all postgres peer
  - host replication rep_user 11.1.1.186/32 md5
  - host replication rep_user 11.1.1.196/32 md5
  - host replication rep_user 11.1.1.195/32 md5
  - host all all 0.0.0.0/0 md5

restapi:
  listen: 11.1.1.196:8008
  connect_address: 11.1.1.196:8008
PATRONIEOF

mkdir -p /etc/systemd/system/patroni.service.d
cat > /etc/systemd/system/patroni.service.d/override.conf <<'OVERRIDEEOF'
[Unit]
After=etcd.service
Wants=etcd.service
OVERRIDEEOF

systemctl daemon-reload
rm -rf /var/lib/pgsql/18/data
systemctl enable patroni --now

[root@lin6 ~]#

The status of the Patroni cluster can be verified with the following command:

# run as root
patronictl -c /etc/patroni/patroni.yml list

After the replicas have been build the status should look similar to:

[root@lin6 ~]# patronictl -c /etc/patroni/patroni.yml list
+ Cluster: pgcluster (7672094740335959559) -+----+-------------+-----+------------+-----+
| Member | Host       | Role    | State     | TL | Receive LSN | Lag | Replay LSN | Lag |
+--------+------------+---------+-----------+----+-------------+-----+------------+-----+
| lin6   | 11.1.1.186 | Leader  | running   |  3 |             |     |            |     |
| lin7   | 11.1.1.195 | Replica | streaming |  3 |   0/5000218 |   0 |  0/5000218 |   0 |
| lin8   | 11.1.1.196 | Replica | streaming |  3 |   0/5000218 |   0 |  0/5000218 |   0 |
+--------+------------+---------+-----------+----+-------------+-----+------------+-----+
[root@lin6 ~]#

Congrats! You have a running 3 node Patroni Cluster.

Performing a switchover

The following command can be used on any running cluster node to switchover the leader to another node and setup the current leader as a replica from the new leader node:

# run as root
# show the current status
patronictl -c /etc/patroni/patroni.yml list
# switchover
patronictl -c /etc/patroni/patroni.yml switchover
# show the new status
patronictl -c /etc/patroni/patroni.yml list
Sample Output (click to expand):
[root@lin7 ~]# patronictl -c /etc/patroni/patroni.yml list
+ Cluster: pgcluster (7672094740335959559) -+----+-------------+-----+------------+-----+
| Member | Host       | Role    | State     | TL | Receive LSN | Lag | Replay LSN | Lag |
+--------+------------+---------+-----------+----+-------------+-----+------------+-----+
| lin6   | 11.1.1.186 | Leader  | running   |  3 |             |     |            |     |
| lin7   | 11.1.1.195 | Replica | streaming |  3 |   0/5000218 |   0 |  0/5000218 |   0 |
| lin8   | 11.1.1.196 | Replica | streaming |  3 |   0/5000218 |   0 |  0/5000218 |   0 |
+--------+------------+---------+-----------+----+-------------+-----+------------+-----+
[root@lin7 ~]# patronictl -c /etc/patroni/patroni.yml switchover
Current cluster topology
+ Cluster: pgcluster (7672094740335959559) -+----+-------------+-----+------------+-----+
| Member | Host       | Role    | State     | TL | Receive LSN | Lag | Replay LSN | Lag |
+--------+------------+---------+-----------+----+-------------+-----+------------+-----+
| lin6   | 11.1.1.186 | Leader  | running   |  3 |             |     |            |     |
| lin7   | 11.1.1.195 | Replica | streaming |  3 |   0/5000218 |   0 |  0/5000218 |   0 |
| lin8   | 11.1.1.196 | Replica | streaming |  3 |   0/5000218 |   0 |  0/5000218 |   0 |
+--------+------------+---------+-----------+----+-------------+-----+------------+-----+
Primary [lin6]:
Candidate ['lin7', 'lin8'] []: lin8
When should the switchover take place (e.g. 2026-08-10T11:25 )  [now]:
Are you sure you want to switchover cluster pgcluster, demoting current leader lin6? [y/N]: y
2026-08-10 10:26:03.98239 Successfully switched over to "lin8"
+ Cluster: pgcluster (7672094740335959559) ----+-------------+-----+------------+-----+
| Member | Host       | Role    | State   | TL | Receive LSN | Lag | Replay LSN | Lag |
+--------+------------+---------+---------+----+-------------+-----+------------+-----+
| lin6   | 11.1.1.186 | Replica | stopped |    |     unknown |     |    unknown |     |
| lin7   | 11.1.1.195 | Replica | running |  3 |   0/5000360 |   0 |  0/5000360 |   0 |
| lin8   | 11.1.1.196 | Leader  | running |  3 |             |     |            |     |
+--------+------------+---------+---------+----+-------------+-----+------------+-----+
[root@lin7 ~]# patronictl -c /etc/patroni/patroni.yml list
+ Cluster: pgcluster (7672094740335959559) -+----+-------------+-----+------------+-----+
| Member | Host       | Role    | State     | TL | Receive LSN | Lag | Replay LSN | Lag |
+--------+------------+---------+-----------+----+-------------+-----+------------+-----+
| lin6   | 11.1.1.186 | Replica | streaming |  4 |   0/50004A0 |   0 |  0/50004A0 |   0 |
| lin7   | 11.1.1.195 | Replica | streaming |  4 |   0/50004A0 |   0 |  0/50004A0 |   0 |
| lin8   | 11.1.1.196 | Leader  | running   |  4 |             |     |            |     |
+--------+------------+---------+-----------+----+-------------+-----+------------+-----+
[root@lin7 ~]#

Troubleshooting Common Issues

IssueSolution
Patroni fails to startCheck logs: journalctl -u patroni -f. Ensure etcd is running and reachable.
etcd cluster unhealthyRestart etcd on both nodes: sudo systemctl restart etcd. Verify with etcdctl endpoint health --cluster.
PostgreSQL not startingCheck PostgreSQL logs.
Leader election stuckRestart Patroni on both nodes. Check etcd for conflicts.

Performance Tuning Tips

1. Adjust PostgreSQL Parameters (in patroni.yml):

The file is located here: /etc/patroni/patroni.yml

parameters:
  shared_buffers: 4GB
  effective_cache_size: 12GB
  work_mem: 64MB
  maintenance_work_mem: 2GB
  random_page_cost: 1.1

Ressources

The following links have further information about a Patroni PostgreSQL Cluster:

Conclusion

You have now successfully set up a highly available PostgreSQL 18 cluster on Red Hat 10 using Patroni and etcd. Here is a quick recap:

  • Three-node PostgreSQL 18 cluster with automatic failover.
  • Patroni manages leader election and replication.
  • etcd ensures cluster state consistency.

Next Steps:

  • Deploy a monitoring solution (Prometheus + Grafana).
  • Set up regular backups (WAL archiving, pg_dump).
  • Test failover scenarios in a staging environment.
  • Scale the cluster by adding more nodes.

Final Thoughts:

This setup is production-ready but can be further customized based on your workload. Patronis REST API and etcd integration make it a robust choice for managing PostgreSQL HA clusters.

Happy clustering!

1