High Availability

From Standalone to High Availability: Convert Your PostgreSQL Database to a Patroni Cluster

Learn how to convert a standalone PostgreSQL instance to a Patroni high-availability cluster with etcd. Step-by-step guide covering installation, configuration, and failover testing.

JusDB Team
March 4, 2026
9 min read
195 views

A standalone PostgreSQL instance is a liability the moment uptime matters — a single process crash, a botched upgrade, or a host-level failure is all it takes to bring your entire database tier down until someone manually intervenes. Patroni turns that single point of failure into a self-healing cluster that elects a new primary in under 30 seconds without a page waking anyone up at 3 AM. It is the most widely deployed PostgreSQL high-availability solution in production, used by teams running anywhere from two-node hobby clusters to 50-node enterprise deployments. This guide walks through every step of converting a running standalone PostgreSQL 16 instance to a Patroni-managed HA cluster, from etcd installation through failover verification.

TL;DR
  • Patroni is an open-source HA framework that takes over PostgreSQL process management from systemd, using a distributed configuration store (etcd) to coordinate leader election and automatic failover.
  • Converting a standalone instance requires: installing etcd, installing Patroni, writing /etc/patroni/patroni.yml, creating replication credentials, then stopping the systemd PostgreSQL unit and handing control to Patroni.
  • A minimum of three nodes (1 primary + 2 replicas) is required for true quorum — a two-node cluster cannot survive a network partition without split-brain risk.
  • The archive_command: /bin/true in the sample config is a placeholder — replace it with a real archiving command before going to production or you will silently lose WAL archive coverage.
  • Always run a manual patronictl switchover in staging before your production cutover to verify failover behaves as expected under your workload.

What is Patroni?

Patroni is an open-source Python framework maintained by Zalando that manages PostgreSQL high availability by acting as a supervisor for the PostgreSQL process on each node. Unlike solutions that bolt HA on top of an existing PostgreSQL install with external scripts, Patroni owns the entire lifecycle of the PostgreSQL daemon — startup, shutdown, configuration reload, and failover — so its behavior is deterministic and auditable.

Before Patroni, teams typically combined repmgr, Pacemaker, or hand-rolled shell scripts to detect primary failures and promote replicas. These approaches required careful coordination and were notoriously difficult to test. Patroni replaces all of that with a single configuration file and a well-understood state machine that every node in the cluster follows independently, consulting a shared distributed configuration store to agree on who is the leader at any given moment.

Patroni integrates natively with several distributed configuration stores (DCS): etcd, Consul, ZooKeeper, and Kubernetes itself. etcd is the most common choice for standalone deployments because it is purpose-built for exactly this use case — small, frequently written keys with strict consistency guarantees.

How Patroni Works

The Distributed Configuration Store (etcd)

etcd is a strongly consistent key-value store that Patroni uses as its source of truth for cluster state. Every Patroni node writes a heartbeat key to etcd at a configurable interval (loop_wait, default 10 seconds). The leader holds a time-to-live (TTL) lock key; if the leader fails to renew its lock within the TTL window (default 30 seconds), the lock expires and the cluster initiates an election. No single Patroni node makes unilateral decisions — every decision is mediated by etcd, which means etcd itself must be highly available. For production, run etcd on an odd number of nodes (3 or 5) to maintain quorum under a single-node failure.

Leader Election

When the primary's etcd lock expires, all replica nodes simultaneously attempt to acquire it. The first node to successfully write the leader lock key in etcd wins the election. Patroni then promotes that node to primary — running pg_promote() internally — and updates the cluster topology in etcd so all other replicas can reconfigure their primary_conninfo and re-attach as standbys to the new leader. This entire process typically completes in 10–30 seconds, depending on ttl and loop_wait settings.

Automatic Failover

Patroni enforces a safety threshold before promoting a replica: if a standby is lagging behind the failed primary by more than maximum_lag_on_failover bytes (default 1 MB), it will not be elected — Patroni will wait for a replica within the threshold instead, or refuse to promote anyone if no qualifying replica exists. This prevents data loss from promoting a replica that missed recent transactions. Once a new primary is elected, Patroni optionally uses pg_rewind to bring the old primary back as a replica without requiring a full base backup, dramatically reducing recovery time on large databases.

Converting Standalone PostgreSQL to a Patroni Cluster

Prerequisites

This guide uses Ubuntu 20.04 and PostgreSQL 16. The primary node IP is 192.168.33.11. For a production cluster you need a minimum of three nodes — one primary and two replicas — so etcd can maintain quorum if one node fails. You will need sudo access, internet connectivity to install packages, and PostgreSQL superuser credentials on the existing instance.

Step 1: Install and Configure etcd

etcd must be installed and running before Patroni starts, because Patroni will attempt to register with the DCS immediately on startup. Install etcd from the Ubuntu package repository and then write its configuration to /etc/default/etcd.

bash
sudo apt-get update
sudo apt-get install -y etcd

Edit /etc/default/etcd with the following configuration, substituting your actual node IP for 192.168.33.11. In a multi-node etcd cluster, ETCD_INITIAL_CLUSTER must list all members.

bash
ETCD_NAME="etcd0"
ETCD_DATA_DIR="/var/lib/etcd/default.etcd"
ETCD_LISTEN_PEER_URLS="http://192.168.33.11:2380"
ETCD_LISTEN_CLIENT_URLS="http://192.168.33.11:2379,http://127.0.0.1:2379"
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://192.168.33.11:2380"
ETCD_INITIAL_CLUSTER="etcd0=http://192.168.33.11:2380"
ETCD_INITIAL_CLUSTER_STATE="new"
ETCD_INITIAL_CLUSTER_TOKEN="etcd-cluster"
ETCD_ADVERTISE_CLIENT_URLS="http://192.168.33.11:2379"

Restart etcd to apply the configuration and verify it is healthy before proceeding.

bash
sudo systemctl restart etcd
sudo systemctl enable etcd
etcdctl cluster-health

Step 2: Install Patroni

Patroni is available in the Ubuntu package repository. The package pulls in the necessary Python dependencies including the etcd client bindings.

bash
sudo apt-get install -y patroni
Tip

If your Ubuntu version ships an older Patroni release, consider installing via pip3 install patroni[etcd] inside a virtualenv, or use the Patroni packages from the PGDG repository to ensure you get a version compatible with PostgreSQL 16.

Step 3: Create the Patroni Configuration File

Create the directory and write the configuration to /etc/patroni/patroni.yml. This file is the complete specification for how Patroni manages your PostgreSQL instance — cluster scope, DCS connection, bootstrap parameters, and runtime PostgreSQL settings all live here.

bash
sudo mkdir -p /etc/patroni
yaml
scope: postgres-cluster
namespace: /db/
name: postgresql0

restapi:
  listen: 192.168.33.11:8008
  connect_address: 192.168.33.11:8008

etcd:
  host: 192.168.33.11:2379

bootstrap:
  dcs:
    ttl: 30
    loop_wait: 10
    retry_timeout: 10
    maximum_lag_on_failover: 1048576
    postgresql:
      use_pg_rewind: true
      use_slots: true
      parameters:
        wal_level: replica
        hot_standby: "on"
        wal_keep_segments: 8
        max_wal_senders: 5
        max_replication_slots: 5
        wal_log_hints: "on"
        archive_mode: "on"
        archive_command: /bin/true

  initdb:
    - encoding: UTF8
    - data-checksums

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

  users:
    admin:
      password: admin
      options:
        - createrole
        - createdb

postgresql:
  listen: 192.168.33.11:5432
  connect_address: 192.168.33.11:5432
  data_dir: /var/lib/postgresql/16/main
  bin_dir: /usr/lib/postgresql/16/bin
  pgpass: /tmp/pgpass0
  authentication:
    replication:
      username: replicator
      password: replicator_password
    superuser:
      username: postgres
      password: postgres_password
  parameters:
    unix_socket_directories: "."

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

Key parameters to understand: scope is the cluster name that all nodes must share. ttl is how long the leader lock persists without a renewal — if the primary cannot reach etcd for this many seconds, it will demote itself. maximum_lag_on_failover is 1 MB by default; any replica lagging more than this will be skipped during leader election. The bootstrap section applies only when Patroni initializes a fresh cluster — it does not re-run on an existing data directory.

Step 4: Create Replication and Superuser Accounts

Connect to the existing PostgreSQL instance and create the replication user that Patroni will use for streaming replication between nodes, and the admin superuser referenced in the bootstrap config.

sql
CREATE USER replicator WITH REPLICATION ENCRYPTED PASSWORD 'replicator_password';
CREATE USER admin WITH SUPERUSER ENCRYPTED PASSWORD 'admin_password';

Verify that the replication user can connect from the replica IPs by testing the connection string before handing control to Patroni. Any pg_hba.conf issues will surface here rather than during a 3 AM failover.

Step 5: Stop PostgreSQL Systemd Unit and Enable Patroni

This is the critical transition step. Patroni cannot coexist with a systemd-managed PostgreSQL service — it must be the sole process manager. Stop and disable the PostgreSQL systemd unit first, then enable and start Patroni. Schedule this step during a maintenance window on production systems.

bash
sudo systemctl stop postgresql
sudo systemctl disable postgresql
sudo systemctl enable patroni
sudo systemctl start patroni

After starting Patroni, watch the service logs to confirm it successfully connects to etcd and either takes leadership (on the primary) or enters streaming replication (on replicas).

bash
sudo journalctl -u patroni -f
Warning

If PostgreSQL is still managed by systemd when Patroni starts, both processes will compete to write to the same data directory and attempt to bind to port 5432 simultaneously. Always confirm systemctl is-active postgresql returns inactive before starting the Patroni service.

Verifying Cluster Health

Once all nodes are running, use patronictl to inspect the cluster topology. A healthy cluster shows one node in Leader state and all replicas in Streaming state with low lag.

bash
# List all cluster members and their current state
patronictl -c /etc/patroni/patroni.yml list

Expected output with a healthy three-node cluster:

text
+ Cluster: postgres-cluster (7234891234567890) +---------+----+-----------+
| Member      | Host              | Role    | State   | TL | Lag in MB |
+-------------+-------------------+---------+---------+----+-----------+
| postgresql0 | 192.168.33.11:5432| Leader  | running |  1 |           |
| postgresql1 | 192.168.33.12:5432| Replica | streaming|  1 |         0 |
| postgresql2 | 192.168.33.13:5432| Replica | streaming|  1 |         0 |
+-------------+-------------------+---------+---------+----+-----------+

To trigger a manual switchover — promoting a replica to primary without any failure — use patronictl switchover. This is the recommended way to perform planned maintenance on the primary node.

bash
# Manual switchover — promotes a replica gracefully
patronictl -c /etc/patroni/patroni.yml switchover postgres-cluster

# Check Patroni service status on any node
sudo systemctl status patroni

# Verify etcd cluster is healthy (all members responding)
etcdctl cluster-health

After the switchover, run patronictl list again to confirm the new leader is correct and all nodes have reconnected as replicas to the promoted primary. The old primary should appear in Replica state within 20–30 seconds as pg_rewind brings it back in sync.

Production Considerations

Warning: Do Not Skip These Before Going Live
  • Test failover in staging first. Run patronictl switchover and simulate a primary kill (sudo systemctl stop patroni on the leader node) in a non-production environment before your first production cutover. Failover behavior under load can differ from idle-cluster tests.
  • etcd requires 3+ nodes for fault tolerance. A two-node etcd cluster cannot achieve quorum after one node fails — the cluster becomes read-only and Patroni cannot elect a new leader. Always deploy etcd on 3 or 5 nodes.
  • Replace archive_command: /bin/true immediately. The placeholder command discards all WAL archive writes silently. Without a real archive command (e.g., pgbackrest archive-push or barman-wal-archive), you cannot perform point-in-time recovery and cannot use pg_rewind to recover a node that diverged more than one checkpoint ago.
  • Switch pg_hba from md5 to scram-sha-256 in production. The example config uses md5 for broad compatibility, but scram-sha-256 is required for CIS compliance and is the PostgreSQL 16 default. Update both the Patroni config and existing pg_hba.conf before exposing the cluster to application traffic.
  • Monitor Lag in MB continuously. If a replica's replication lag exceeds maximum_lag_on_failover (1 MB by default), Patroni will skip it during leader election. A lagging replica that is your only standby means you have no automatic failover target — set up alerting on this metric before it matters.

For connection routing in front of the Patroni cluster, use HAProxy with Patroni's built-in REST API health endpoint. Patroni exposes GET / on port 8008 that returns HTTP 200 for the current leader and HTTP 503 for replicas, making HAProxy backend health checks trivially accurate without any custom scripts.

text
frontend postgresql_write
    bind *:5000
    mode tcp
    default_backend postgresql_primary

backend postgresql_primary
    mode tcp
    option httpchk GET /
    http-check expect status 200
    server pg1 192.168.33.11:5432 check port 8008 inter 2s rise 2 fall 3
    server pg2 192.168.33.12:5432 check port 8008 inter 2s rise 2 fall 3
    server pg3 192.168.33.13:5432 check port 8008 inter 2s rise 2 fall 3
Key Takeaways
  • Patroni replaces systemd as PostgreSQL's process manager and uses etcd as a distributed lock to coordinate leader election — disable the PostgreSQL systemd service before starting Patroni to avoid dual-process conflicts on the data directory.
  • A three-node cluster (1 primary + 2 replicas) is the minimum viable topology for fault tolerance: two etcd nodes cannot maintain quorum, and a two-node Patroni cluster has no second failover candidate when the replica is promoted.
  • Configure use_pg_rewind: true in the DCS bootstrap settings to allow a demoted primary to rejoin as a replica without a full base backup, which is critical for fast recovery on large databases.
  • The maximum_lag_on_failover threshold directly controls your data loss tolerance during unplanned failover — tune it based on your RPO requirements and measure actual replication lag under peak write load before setting it in stone.
  • Replace archive_command: /bin/true with a real WAL archiving command before production deployment; silent WAL discard eliminates PITR capability and can leave you without a recovery path after a multi-node failure.
  • Use Patroni's REST API health endpoint (port 8008) as the HAProxy backend health check — it returns 200 for the leader and 503 for replicas, giving your load balancer accurate write-routing without any external scripts or DNS TTL tricks.

Working with JusDB on Patroni

JusDB designs, deploys, and operates Patroni HA clusters for engineering teams who need production-grade PostgreSQL reliability without building in-house DBA expertise around cluster management. Our DBAs have migrated standalone instances to Patroni clusters across AWS, GCP, and bare-metal environments — handling etcd topology design, pg_hba hardening, HAProxy routing configuration, WAL archiving setup with pgBackRest, and the maintenance-window cutover itself. We also provide 24/7 on-call support so that when Patroni triggers an unexpected failover at 2 AM, someone who has debugged a hundred of them is available to respond.

Explore JusDB Patroni Cluster Management →  |  Talk to a DBA

Related reading:

Share this article

JusDB Team

Official JusDB content team