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.
- 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/truein 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 switchoverin 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.
sudo apt-get update
sudo apt-get install -y etcdEdit /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.
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.
sudo systemctl restart etcd
sudo systemctl enable etcd
etcdctl cluster-healthStep 2: Install Patroni
Patroni is available in the Ubuntu package repository. The package pulls in the necessary Python dependencies including the etcd client bindings.
sudo apt-get install -y patroniIf 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.
sudo mkdir -p /etc/patroniscope: 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: falseKey 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.
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.
sudo systemctl stop postgresql
sudo systemctl disable postgresql
sudo systemctl enable patroni
sudo systemctl start patroniAfter 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).
sudo journalctl -u patroni -fIf 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.
# List all cluster members and their current state
patronictl -c /etc/patroni/patroni.yml listExpected output with a healthy three-node cluster:
+ 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.
# 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-healthAfter 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
- Test failover in staging first. Run
patronictl switchoverand simulate a primary kill (sudo systemctl stop patronion 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/trueimmediately. The placeholder command discards all WAL archive writes silently. Without a real archive command (e.g.,pgbackrest archive-pushorbarman-wal-archive), you cannot perform point-in-time recovery and cannot usepg_rewindto recover a node that diverged more than one checkpoint ago. - Switch
pg_hbafrommd5toscram-sha-256in production. The example config usesmd5for broad compatibility, butscram-sha-256is required for CIS compliance and is the PostgreSQL 16 default. Update both the Patroni config and existingpg_hba.confbefore exposing the cluster to application traffic. - Monitor
Lag in MBcontinuously. If a replica's replication lag exceedsmaximum_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.
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- 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: truein 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_failoverthreshold 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/truewith 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: