Skip to content

Patroni HA

Run PostgreSQL high availability with Patroni as a pgcli HA mode — automatic failover, switchover, and a DCS-backed cluster

Patroni is the de-facto standard for PostgreSQL high availability: it manages each postmaster’s lifecycle, streams replication between members, and performs automatic failover when the leader is lost. pgcli exposes Patroni as a distinct mode — pg ha — rather than folding it into the plain pg instance path.

This is a separate mode, not an addon subcommand. Patroni (not pgcli) owns the PostgreSQL process. pg ha members do not appear in cfg.Instances, share no instance lifecycle code, and are not created by pg create. The one thing it borrows from the addon system is the etcd DCS.

Linux only, like the etcd addon: Patroni members rely on podman host networking. Both root and rootless podman are supported. On macOS the commands fail fast with a clear message.

Ownership boundary

The single most important thing to internalize is who owns what:

Responsibility Owner
Patroni container (run/start/stop/rm), image, patroni.yml, port allocation, passwords, DCS wiring pgcli
postmaster lifecycle, initdb, PostgreSQL config rendering, replication slots, failover Patroni
switchover / failover / pause / edit-config pgcli wrapping patronictl in short-lived containers

pgcli never edits a running PostgreSQL’s config directly, and never runs initdb — Patroni does both. This is also why the plain PG image’s docker-entrypoint-initdb.d convention (the admin role / default database) does not apply here: Patroni bootstraps its own cluster, so the role system is postgres (superuser), replicator, and rewind_user instead.

Container lifecycle ≠ safe PG restart

Because Patroni is PID 1 inside its container:

  • pg ha create is re-install semantics (stop + recreate the container). Recreating the leader takes that node fully offline and triggers a failover. Changing a member’s config by re-running create is fine; for dynamic settings prefer pg ha edit-config, which never touches the container.
  • pg ha start / pg ha stop are raw container start/stop. Stopping the leader’s container is exactly as disruptive as the node crashing — Patroni will fail over to a replica. For planned maintenance, pg ha pause first (it disables auto-failover), then stop the container.
  • A paused cluster has no automatic failover until resumed.

How It Works

pg ha runs one Patroni container per member. All members of a scope point at the same DCS (a etcd cluster) which holds the cluster’s dynamic configuration and leader lock:

  1. The first pg ha create for a scope bootstraps: Patroni runs initdb, wins the leader race, and becomes the leader.
  2. Every later member automatically pg_basebackups from the current leader and begins streaming — no flag distinguishes “add” from “join”.
  3. pg ha control commands (switchover, pause, …) run patronictl in an ephemeral container against the DCS, so they work even when no member container is running locally.

The DCS is the source of truth: Patroni re-renders each member’s postgresql.conf/pg_hba.conf from it every loop, so hand-edits on disk are lost — use pg ha edit-config instead.

Scope and the DCS layout

The <scope> in pg ha create <scope> is the Patroni cluster name — the identity of one HA cluster. Every command that takes a scope (status, switchover, failover, pause, ctl, …) names the same cluster; the first create for a scope bootstraps it, later creates with the same scope add members. (--member is the per-host node name inside the cluster — one member per machine.)

In pg.yaml the cluster is keyed by that scope under addons.patroni.<scope>.

What actually lands in etcd

Patroni stores everything under a fixed etcd namespace plus the scope:

  • namespace: /service/ — Patroni’s top-level key, not changed by pgcli.
  • scope: pgcli writes PatroniScope(scope) into patroni.yml, which is your scope plus the pgcli namespace suffix. Patroni has no namespace concept of its own (its etcd prefix is the raw scope), so pgcli bakes the suffix into the scope to keep two pgcli namespaces sharing one etcd from cross-talking.

So the full etcd prefix for a cluster is:

/service/<scope>[-<namespace>]/          # e.g. /service/app/  (no namespace)
                                         #      /service/app-prod/  (namespace: prod)
├── initialize       # bootstrap marker (written once)
├── leader           # current leader; value = member name
├── members/<member> # per-member registration (conn_url, api_url, state)
├── status           # cluster LSN / state
├── config           # dynamic config (pause lives here too)
├── history          # config revision history
├── failover         # manual failover request
└── sync             # synchronous-replication state

The keys below the prefix are Patroni’s own DCS layout. To inspect them, point pg etcdctl at a running etcd member of the same host/config:

ETCDCTL_ENDPOINTS=http://127.0.0.1:2379 pg etcdctl get /service/ -- --prefix --keys-only

Note the scope shown there carries the namespace suffix, even if you configured the cluster with the bare name.

Install

Prerequisite: a DCS. Either reuse local etcd addon members or point at an external etcd.

# 1. a DCS — here, the etcd addon (see the etcd page for multi-node setups)
pg addon install etcd --name m1

# 2. the first member bootstraps the cluster (becomes leader)
pg ha create app --member node1 --etcd m1

# 3. further members join automatically as replicas
pg ha create app --member node2 --etcd m1
pg ha create app --member node3 --etcd m1

# 4. watch it
pg ha status app

pg ha status app renders patronictl list — exactly one Leader row and the rest Replica … streaming:

+ Cluster: app (7683433951661608987) +-----------+----+-------------+-----+------------+-----+
| Member | Host            | Role    | State     | TL | Receive LSN | Lag | Replay LSN | Lag |
+--------+-----------------+---------+-----------+----+-------------+-----+------------+-----+
| node1  | 127.0.0.1:5432  | Leader  | running   |  1 |             |     |            |     |
| node2  | 127.0.0.1:5433  | Replica | streaming |  1 |   0/3000060 |   0 |  0/3000060 |   0 |
| node3  | 127.0.0.1:5434  | Replica | streaming |  1 |   0/3000060 |   0 |  0/3000060 |   0 |
+--------+-----------------+---------+-----------+----+-------------+-----+------------+-----+

With no argument, pg ha status rolls up every cluster: container state, member count, and each member’s pg=/rest= ports.

Choosing a DCS

Patroni only needs a reachable etcd cluster, so there are two layouts:

  • Co-located — run the etcd members as addons on the same hosts as the Patroni members and pass --etcd m1,m2,m3. Simplest: one machine per role, no extra hosts. Good for a starter 3-node HA cluster.

  • Dedicated DCS hosts — run the etcd cluster on its own (virtual) machines and point every Patroni member at it with --etcd-endpoints. Each line below runs on a different machine (pgcli is per-host):

    # host E1 (10.0.0.20) — bootstrap the etcd cluster
    pg addon install etcd --name e1 --cluster prod \
        --advertise-host 10.0.0.20 --client-port 2379 --peer-port 2380
    
    # host E2 (10.0.0.21) — join
    pg addon install etcd --name e2 --cluster prod \
        --advertise-host 10.0.0.21 --client-port 2379 --peer-port 2380 \
        --join http://10.0.0.20:2379
    
    # host E3 (10.0.0.22) — join
    pg addon install etcd --name e3 --cluster prod \
        --advertise-host 10.0.0.22 --client-port 2379 --peer-port 2380 \
        --join http://10.0.0.20:2379
    
    # hosts A / B / C — one Patroni member each; same endpoint list everywhere
    pg ha create app --member node1 --advertise-host 10.0.0.11 \
        --etcd-endpoints 10.0.0.20:2379,10.0.0.21:2379,10.0.0.22:2379

    This is the more available topology: etcd’s quorum survives losing a PG host, and re-imaging a database machine never takes the DCS down with it. The PG hosts only ever talk to the DCS; --etcd-endpoints lists all member client URLs, so Patroni falls through to the next endpoint if one etcd host is down (writes still need the etcd quorum itself — 2 of 3). Keep the endpoint list identical on every PG host.

    A single endpoint (--etcd-endpoints 10.0.0.20:2379) does work — that one etcd member serves the whole cluster, and the other two are hidden behind it. But it re-introduces a single point of failure: if E1 goes down, Patroni cannot reach the DCS even though the etcd quorum is healthy, and a leader that fails to renew its lock demotes itself. List every endpoint you actually have.

A dedicated odd-sized etcd cluster (3 or 5) is the production recommendation; co-locating is fine for dev and small footprints.

Commands

Command What it does
pg ha create <scope> --member <m> … Register + (re)install one member — recreate = node offline
pg ha list Compact table of all HA clusters (scope, members, DCS, status)
pg ha status [scope] All clusters, or one cluster’s patronictl list
pg ha switchover <scope> Planned leader change (patronictl confirms; --yes to script)
pg ha failover <scope> Promote a replica now
pg ha pause / resume <scope> Disable / re-enable automatic failover
pg ha edit-config <scope> -- … View or patch the dynamic config in the DCS (never recreates a container)
pg ha start / stop <scope> --member m | --all Raw container start/stop (see lifecycle caveat)
pg ha remove <scope> --member m | --scope-all [--clean-data] [--force] Remove member(s); --scope-all also clears the DCS
pg ha passwords <scope> [--file F] Export the stored password set (the --passwords-file format, for other hosts)
pg ha remote <scope> [--member m] [--ssh-port P] Register a member living on another host (typically the cross-host leader) for backup SSH
pg ha remote remove <scope> --member m Undo such a registration (the remote container is untouched)
pg ha extension install/remove/list/apply Install, remove, or list PostgreSQL extensions (see Extensions in HA)
pg ha exec <scope> "<sql>" [--member m] [--database db] Run one-shot SQL against the leader (or a named member) — no dsn, no container exec
pg ha psql <scope> [--member m] [--database db] [-- <psql-args>…] Interactive psql against the cluster, same resolved target
pg ha ctl <scope> -- <patronictl args…> Passthrough to any patronictl command

Flags after -- reach patronictl verbatim (cobra strips the --), so pg ha ctl app -- show-config, pg ha ctl app -- topology, and pg ha edit-config app -- -s synchronous_mode=true --force all work. edit-config --show is a convenience alias for ctl … -- show-config.

Cross-host members

Each host runs its own pgcli managing only that host’s members; the cluster reassembles through the shared DCS, so two hosts’ pg.yaml files each hold a partial view of one scope.

# host A (10.0.0.11) — bootstrap (its own etcd, or an external DCS)
pg ha create app --member node1 --advertise-host 10.0.0.11 \
    --etcd-endpoints 10.0.0.9:2379,10.0.0.10:2379

# host A — export the generated password set for other hosts
pg ha passwords app --file app-passwd.yml

# host B (10.0.0.12) — join, sharing the SAME DCS and password set
pg ha create app --member node2 --advertise-host 10.0.0.12 \
    --etcd-endpoints 10.0.0.9:2379,10.0.0.10:2379 \
    --passwords-file app-passwd.yml

Cross-host checklist (all three must line up on every host):

  1. Ports — auto-assignment is per-host, but connect_address is stored in the DCS cluster-wide. Pass --host-port / --restapi-port with the same value on every host, or replicas can’t reach each other.
  2. Passwords — Patroni’s replication / rewind / REST-API auth is cluster-wide. Export the first host’s generated set with pg ha passwords app --file app-passwd.yml and pass --passwords-file app-passwd.yml to every other pg ha create.
  3. --advertise-host — required for cross-host members; it flips the listen address to 0.0.0.0 and puts a reachable IP in connect_address. Left empty, a member is loopback-only. This applies to the first (bootstrap) member too: it becomes the leader, and its loopback connect_address is what every later host would try to pg_basebackup from — cross-host joins fail forever once the cluster was bootstrapped with the default. If you may ever add a member on another host, pass your LAN IPv4 from the very first create.
  4. Firewall — allow the two ports (PG + REST API) pairwise between members.

pgcli does not validate the peers’ config; pg ha status shows each member’s connect_address so you can self-check reachability.

Backing up a cross-host leader

Each host’s pg.yaml records only its own members, so this host cannot see a leader running on another host — yet pgBackRest’s stanza-create and full backups must connect to the leader.

This now happens automatically: after pg ha create installs a member, it publishes that member’s pgcli-private ports (SSH / REST API) into an etcd registry at /pgcli/ha/<scope>/<member> (the topology itself — member name + host:pgport — Patroni already stores in the DCS). The backup config generators then run patronictl list for the cluster-wide topology and look up each remote member’s SSH port in the registry, so pgbackrest.conf / ssh_config include members on every host, with pg1-host and the SSH HostName pointing at the remote IP (local members still go via 127.0.0.1). pg ha start/stop/remove, autostart, and pg ha extension skip members that aren’t local — they belong to their own host’s pgcli.

Just refresh the backup config:

pg backup setup

Cross-host backup trust is now automatic. A cluster-wide stanza lists every member as a pg*-host, so a full backup / check from any host SSH-probes the members on all hosts to find the primary — each of those member sshd processes must therefore accept the initiating host’s backup key, and its --advertise-host already flipped the listener to 0.0.0.0 (see above) so cross-host SSH reaches it. pg backup setup handles this: each host’s create publishes its backup public key into the same /pgcli/ha/<scope>/<member> registry, setup merges every member’s key into a per-cluster authorized_keys file, and each member container bind-mounts that file as an extra AuthorizedKeysFile. Because sshd re-reads it on every login and the merge rewrites the file in place, a host that joins later is trusted by the running members without a restart (the file only needs the one-time recreate to add the mount, which setup does inside the pause window alongside the archive-config recreate). A member removed with pg ha remove drops its registry key, so the next merge stops trusting it.

The S3 repository CA is distributed the same way: the host that configured ca_file publishes the certificate to /pgcli/ha/<scope>/.repo/ca, and a joiner that leaves ca_file empty pulls it and points its own ca_file at the pulled copy. When the store itself lives on yet another machine, the first host gets the CA without any file copy too — pg backup fetch-ca <store>:<port> pulls it out of the endpoint’s TLS chain (see the MinIO doc). Only public material — SSH public keys and the self-signed CA — ever enters the registry; private keys, passwords, and the S3 secret_key never do (the DCS link is unauthenticated).

Fallback: a remote member created on its host before that host upgraded pgcli has no registry entry, so the generators fall back to this scope’s base SSH port for it (every host starts at patroni_ssh_start_port, so the first member is usually right). When it isn’t, register just that member manually to override (creates no container):

pg ha remote app --member node3 --ssh-port 42301   # writes members.<m>.remote_host
pg ha remote remove app --member node3             # undo the registration

S3 backups and WAL archiving

Patroni clusters ship with archiving off — full backups alone can never reach a point-in-time. Enabling the S3 repository (backup docs, English; 中文) flips it on. Each cluster is one stanza — pgcli_<scope>, matching the namespaced DCS scope, so two pgcli namespaces sharing one S3 bucket never collide — whose backup-side config lists every member as a pg*-host, so pgBackRest finds the primary itself and keeps working across failovers. pg backup setup renders the identical archive_command into each member’s local patroni.yml (pgcli never writes archive GUCs into DCS — that is edit-config’s territory, and Patroni applies a local value whenever DCS does not manage the key), recreates stale member containers replicas-first inside a pause window (the recreate is also the postmaster restart archive_mode needs), and runs stanza-create + check for the cluster. From then on WAL streams to S3 continuously, from whichever member holds the leader lock.

Expect a short per-member offline window and a planned leader demotion at the end — the same recreate semantics as pg ha create.

Passwords

The first pg ha create for a scope generates four credentials — superuser, replication, rewind, and the restapi basic-auth pair (restapi_user / restapi_password) — and stores them in pg.yaml under the cluster. You never pass passwords on the command line: the first member generates them, and pg ha passwords exports the stored set:

pg ha passwords app                          # print the stored set (YAML) to stdout
pg ha passwords app --file app-passwd.yml    # write it to a file (mode 0600) instead

(Without --file the YAML goes to stdout — prefer --file so the secrets stay out of shell history and scrollback.)

The four roles and their default usernames (fixed by pgcli — you only ever set the passwords, which default to a random 16-char string per role). Override the length with --password-length on pg ha create / pg create (8–64; it only applies to the generate path, not to a --passwords-file or an already-stored set):

pg.yaml key Role Default username Default password
superuser PostgreSQL superuser postgres random 16-char, generated
replication replication / streaming replicator random 16-char, generated
rewind pg_rewind role rewind_user random 16-char, generated
restapi_user / restapi_password Patroni REST API basic-auth postgres random 16-char, generated

The postgres / replicator / rewind_user usernames are baked into the rendered patroni.yml (postgresql.authentication); only the passwords are the generated / --passwords-file part. restapi_user is the one username you can override, via the passwords file.

For full control (or when you’d rather author the file yourself) use the same format with --passwords-file:

# app-passwd.yml
superuser: <postgres superuser password>
replication: <replicator password>
rewind: <rewind_user password>
restapi_user: postgres          # optional, defaults to postgres
restapi_password: <REST API basic-auth password>
pg ha create app --member node1 --etcd m1 --passwords-file app-passwd.yml

pg ha create prints where each password came from (generated-and-stored vs. a file path). The rendered patroni.yml is written mode 0600 because it embeds all of them.

Auto-start on Boot

Like the other infra addons, a Patroni member’s container can be brought up after a host reboot — but it only starts the existing container, reading the patroni.yml already on disk; it never re-renders config or re-creates data.

pg autostart enable --ha --scope app --name node1

Members are toggled one at a time (--ha --scope <scope> --name <member>). Start order relative to the DCS doesn’t matter: Patroni retries until etcd answers, then re-elects normally. See the autostart page for the boot-service mechanics.

Logs

pg logs addon patroni --scope app --name node1          # last 50 lines
pg logs addon patroni --scope app --name node1 -f       # follow

Container name is pgcli-patroni-<scope>-<member> (namespace-prefixed when a namespace is set).

Connecting

pg ha exec and pg ha psql are the zero-config path: pgcli resolves the leader from the DCS itself, authenticates with the stored superuser password, and runs psql in a throwaway container — no dsn to assemble, no member container to enter, and remote members are reachable (plain TCP + scram, so a replica on another host works too; --member aims at a specific node).

pg ha exec app "SELECT version()"
pg ha exec app --member node2 "SELECT pg_is_in_recovery()"   # read-only, on a replica
pg ha psql app                                               # interactive

For clients outside pgcli, connect to the leader’s PostgreSQL port. Find the leader with pg ha status app (the Leader row’s Host is its connect_address), then point pg psql at it. On a single host with default loopback-only members, that is 127.0.0.1:<host_port>.

pg psql --dsn postgres://postgres@127.0.0.1:<leader_port>/postgres

After a failover the leader changes, so a fixed connection string should be avoided unless fronted by a pooler or the Patroni REST API’s leader redirect.

For a stable endpoint that survives failover — and optional read/write separation — put HAProxy in front of the cluster.

Planned leader changes

Patroni owns failover; pg ha wraps the patronictl verbs that move the leader on purpose. All three take just the scope:

pg ha switchover app                     # patronictl prompts for the candidate
pg ha switchover app --candidate node2   # name it up front
pg ha switchover app --candidate node2 --yes   # skip prompts (scripted)

pg ha failover   app --candidate node2 --yes   # promote now, no handoff
pg ha pause      app                          # stop auto-failover
pg ha resume     app                          # re-enable it
  • switchover is the planned, graceful one: the current leader is demoted first, so nothing is in flight when the candidate is promoted. Both members must be healthy and caught up. The old leader automatically rejoins as a streaming replica a few seconds later (you may briefly see it as stopped while Patroni restarts its postmaster). A new timeline is opened — this is normal, not a split-brain.
  • failover force-promotes a replica immediately without a clean handoff from the leader. Use it only when the leader is already gone or you are deliberately discarding it; on a healthy cluster it just causes an unnecessary blip. Reach for switchover for anything planned.
  • pause turns off automatic failover cluster-wide. Do this before any planned container surgery — pg ha stop --all, a pg ha create recreate, or pg ha extension — so Patroni doesn’t promote a replica out from under you. pg ha resume puts auto-failover back. (A paused cluster has no automatic failover until resumed.)

These are pure DCS operations — they run patronictl in a throwaway container, touch no member’s container or data, and work even when some members live on other hosts. Like every pg ha control command, the scope is resolved to the namespaced DCS key (app → app-default), so a single-host or cross-host leader is switched the same way.

Because clients dial the leader’s port, the connection target moves after a switchover — see Connecting and put HAProxy in front if you need one stable endpoint.

pg ha vs. pg replica — which to pick

pgcli has two ways to get a standby:

pg replica + pg failover pg ha (Patroni)
Model Manual: pg replica builds a standby, pg failover promotes it on demand Automatic: Patroni keeps N members in sync and self-heals
Failover A human runs pg failover; primary stays down until promoted Patroni detects the loss and promotes a replica within ~30s
Ownership pgcli drives postmaster (as with any instance) Patroni drives postmaster; pgcli owns containers only
Best for Simple read-scaling, single planned promotion, staying on pg-instance tooling Zero-RTO availability requirements, unattended failover

If you need a standby you control by hand, use pg replica. If you need the cluster to survive a node crash without a human, use pg ha.

Configuration

The rendered patroni.yml per member is derived entirely from pg.yaml under addons.patroni.<scope>. A typical entry:

addons:
  patroni:
    app:
      name: app
      etcd_members: [m1]                # or etcd_endpoints for an external DCS
      passwords:
        superuser: <superuser-password>
        replication: <replication-password>
        rewind: <rewind-password>
        restapi_user: postgres
        restapi_password: <restapi-password>
      members:
        node1:
          container_name: pgcli-patroni-app-node1
          image_tag: ghcr.io/mars-base/pgcli/pgcli-patroni:18-4.1.5
          host_port: 35590
          restapi_port: 39090
          data_dir: /home/you/.pgcli/addon/patroni/app/node1
          autostart: false

Ports come from two independent pools — patroni_start_port (default 35532) for PostgreSQL and patroni_restapi_start_port (default 39060) for the REST API — so Patroni members never collide with plain-instance or addon ports.

The DCS-scope key is the scope plus the namespace suffix (Patroni has no namespace concept of its own, so the prefix is baked into the scope to keep two pgcli namespaces sharing one etcd from cross-talking).

Default patroni.yml

The file below is what pgcli renders and mounts read-only into each member’s container. Passwords are auto-generated at bootstrap time; --passwords-file lets you supply your own set.

scope: app-default
namespace: /service/
name: node1

etcd3:
    hosts: 10.0.0.11:2379       # from --etcd-endpoints or local etcd member
    protocol: http

restapi:
    listen: 0.0.0.0:8009
    connect_address: 10.0.0.11:8009
    authentication:
        username: postgres
        password: <auto-generated>

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"
    initdb:
        - encoding: UTF8
        - data-checksums
    pg_hba:
        - local all all trust
        - local replication all trust
        - host all all all scram-sha-256
        - host replication all all scram-sha-256

postgresql:
    listen: 0.0.0.0:35532
    connect_address: 10.0.0.11:35532
    data_dir: /var/lib/postgresql/data
    bin_dir: /usr/lib/postgresql/18/bin
    pgpass: /patroni/.pgpass
    use_unix_socket: true
    use_unix_socket_repl: true
    authentication:
        superuser:
            username: postgres
            password: <auto-generated>
        replication:
            username: replicator
            password: <auto-generated>
        rewind:
            username: rewind_user
            password: <auto-generated>
    parameters:
        unix_socket_directories: /var/lib/postgresql

Key points:

  • scope = Patroni cluster name. Combined with namespace, it forms the etcd key prefix.
  • etcd3.hosts = host:port only (no http:// scheme). Multiple endpoints are comma-separated.
  • bootstrap.dcs values are defaults only — they take effect on first bootstrap; afterwards use pg ha edit-config.
  • postgresql.listen: 0.0.0.0 is set when --advertise-host is used (cross-host). Without it, listen is 127.0.0.1.
  • Passwords are generated once and stored in pg.yaml; exported via pg ha passwords.

Dynamic Configuration

Patroni’s dynamic configuration lives in the DCS (etcd) under /service/<scope>/config. Every member reads it on each loop (every loop_wait seconds) and applies changes live — so pg ha edit-config is the right way to tune runtime parameters without restarting containers.

bootstrap.dcs is one-time. The bootstrap.dcs block in patroni.yml only takes effect when the first member of a scope runs pg ha create (i.e. the cluster bootstrap). Once Patroni writes the config into the DCS, subsequent changes to bootstrap.dcs in the YAML file are completely ignored — even on re-install (pg ha create). To change dynamic configuration after bootstrap, use pg ha edit-config.

The common pitfall: re-running pg ha create does not re-read bootstrap.dcs from the YAML — Patroni sees the existing config key in the DCS and uses it directly.

Method Description
pg ha edit-config app -- -s key=value Recommended — pgcli’s standard way
pg ha ctl app -- edit-config Passthrough to patronictl, same effect
Patroni REST API (PATCH /config) Requires access to a member’s REST API port

Persistence: changes are written directly to etcd, not to container files. Container restarts, pg ha start/stop, or even pg ha create (re-install) do not lose these settings — new members automatically pick up the latest config from the DCS.

Viewing the current config

pg ha edit-config app --show

This is a convenience alias for pg ha ctl app -- show-config. The output is the full JSON blob stored in /service/<scope>/config.

Modifying parameters

# Set a single parameter
pg ha edit-config app -- -s loop_wait=5

# Set multiple parameters
pg ha edit-config app -- -s loop_wait=5 -s retry_timeout=3

# Apply without confirmation (useful in scripts)
pg ha edit-config app -- -s loop_wait=5 --force

# Interactive edit (opens $EDITOR with the current config)
pg ha edit-config app

After a change, all members apply it on their next loop (within loop_wait seconds). No restart needed.

Common tunable parameters

Parameter Default Description
loop_wait 10 Seconds between leader-loop iterations (lock renewal, DCS updates)
ttl 30 Leader lock TTL. If the leader fails to renew within this window, replicas trigger failover
retry_timeout 10 Timeout for DCS/PostgreSQL operations. Must be < ttl - loop_wait to give the leader at least one retry chance
maximum_lag_on_failover 1048576 Maximum replication lag (bytes) for a replica to be eligible for promotion. Default 1 MB
synchronous_mode false Enable synchronous replication (zero data loss, higher latency)
synchronous_node_count 1 How many synchronous standby nodes (when synchronous_mode=true)
use_pg_rewind true Use pg_rewind to rejoin a failed leader (faster than full pg_basebackup)
use_slots true Use replication slots (prevent WAL loss when a replica disconnects)
failover_timeout 0 How long to wait before failover (0 = immediate when leader is lost)

PostgreSQL runtime parameters can also be set under postgresql.parameters:

pg ha edit-config app -- -s 'postgresql.parameters.max_connections=200'
pg ha edit-config app -- -s 'postgresql.parameters.work_mem=64MB'

These trigger a PostgreSQL reload (or restart, depending on the parameter’s context). Check pg_hba.conf and postgresql.conf parameter documentation for which settings require a restart.

What not to edit

  • Do not edit patroni.yml on disk — it is regenerated from pg.yaml on every pg ha create, and Patroni reads dynamic config from the DCS anyway.
  • Do not edit postgresql.conf directly — Patroni overwrites it each loop from the DCS config.
  • Do not change scope or namespace — these are baked into the DCS key at bootstrap time and cannot be changed without recreating the cluster.

Full parameter reference: Patroni Dynamic Configuration covers every DCS-tunable parameter with defaults, constraints, and examples.

Extensions

pg ha extension install app pg_stat_statements,pg_cron   # install
pg ha extension list app                                  # list
pg ha extension remove app pg_cron                        # remove

Full reference: Extensions in HA Clusters covers the orchestration order, cross-host workflow, shared_preload_libraries ordering, and builtin-only fast path.

Notes

  • Linux only (root or rootless). Rootless members run as the host user via --userns=keep-id; root members chown config and data dirs to postgres (uid 999) so the container can read 0600 config and write its data dir.
  • pg_hba.conf is permissive by design (host all all all scram-sha-256
    • a replication line, plus local trust lines). Rootless podman’s pasta rewrites loopback sources, and Patroni’s own replace_pg_hba step only ever grants its resolved loopback TCP address during custom bootstrap — so postgresql.use_unix_socket/use_unix_socket_repl are set to make Patroni connect to its own instance over the unix socket instead, which is unaffected by the rewrite and keeps bootstrap from deadlocking on its own pg_hba. Tightening the allow-list to a fixed set is a planned future refinement — do not expose these ports to untrusted networks yet.
  • The DCS (etcd) has its own security caveats — see the etcd page: pgcli-managed etcd runs without TLS or auth.
  • No init.sh / docker-entrypoint-initdb.d. Patroni bootstraps the cluster itself, so the admin/default-db convention of plain instances does not exist here; use postgres (superuser) to connect and create roles.
  • use_slots / use_pg_rewind are enabled: Patroni owns replication slots and can rejoin a crashed leader via pg_rewind instead of a full rebase.