Overview
PostgreSQL Highly Available deploys a multi-member PostgreSQL 17 cluster in one location. Patroni manages the members, elects a leader and fails over automatically when the leader is lost; a dedicated etcd cluster holds the consensus state. An HAProxy workload gives applications one stable address that always routes to the current leader, and an optional PgBouncer in front of it pools connections. Backups are optional, in two modes: scheduledpg_dumpall dumps, or continuous WAL archiving with WAL-G.
Database credentials are not template values. The cluster reads its username, password and database name from a dictionary secret you create before installing, so no password passes through Helm or lands in the release. The template creates no credential secret of its own.
What Gets Created
Prerequisites
One secret must exist before you install. It holds the credentials your applications put in their connection strings. Secrets are org-level, so no GVC flag is involved.Create the database credentials secret
username, password and database. Patroni creates that user as the cluster superuser and that database the first time the cluster is initialized, and uses the same password for its internal replication role:config.credentialsSecretName to this name. Read it back later with cpln secret reveal CREDENTIALS_SECRET_NAME -o yaml — a bare cpln secret reveal prints only a summary table, not the values. Secret names are org-wide, so give each release its own.accessKey and secretKey — see Backing Up. Nothing else is required for a default install.
Installation
Install the chart from the marketplace registry, pointing it at the secret you created:UI
CLI
Terraform
Pulumi
Configuration
Each block below is the shipped default for that part ofvalues.yaml.
Cluster and Resources
replicas— Number of PostgreSQL members. Keep at least 3 for a cluster that survives the loss of one member; one member is the leader and accepts writes, the others stream from it.multiZone— Spread the members across availability zones within the location, when the location supports it.
maxCpu to minCpu may not exceed 4:1. Raising maxCpu without raising minCpu can cross that limit and is rejected when the workload is applied.Credentials
Storage
volumeset.capacity, so a member that stays down does not fill the leader’s disk; a member that exceeds the cap is re-cloned from the leader when it returns.
Internal Access
etcd Cluster
The bundled etcd template provides the consensus store. Its own values are passed through underetcd:
etcd.replicas— Must be an odd number of at least 3 (3, 5, 7); the chart refuses other values at render. Choose it at install time — the etcd template does not support changing the member count on a running release.etcd.internal_access— Must allow the PostgreSQL members to reach etcd on port2379. Withworkload-list, include//gvc/GVC_NAME/workload/RELEASE_NAME-postgres-haand//gvc/GVC_NAME/workload/RELEASE_NAME-etcd.
etcd History Compaction
Patroni renews its leader lease every few seconds and every renewal is a new etcd revision, so the etcd backend grows with time alone unless old revisions are compacted. Uncompacted, an otherwise idle cluster reaches etcd’s 2 GiB backend quota in a matter of months, etcd raises aNOSPACE alarm and goes read-only, and the PostgreSQL members can no longer renew their leases. The etcd.tuning defaults — periodic, 1h and 0 (etcd’s own 2 GiB limit) — keep the backend flat and should be left alone. The etcd template documents the mechanism, the accepted value formats and the read-only inspection commands.
Template versions before 2.4.2 did not compact — see Upgrading From a Version Before 2.4.2.
HAProxy Leader Endpoint
Only the leader accepts writes; the other members are read-only. HAProxy checks every member’s Patroni REST API and sends port5432 traffic only to the one that reports itself primary, so applications keep one address through a failover.
logical backups, which connect through it. It is forced on when pgbouncer.enabled: true. HAProxy reads the member list from replicas when it starts, so after changing replicas force a redeployment of RELEASE_NAME-postgres-ha-proxy.PgBouncer Connection Pooling
PgBouncer multiplexes many application connections into a smaller pool of real database connections. It connects to HAProxy, so leader routing and failover are handled for it, and it reads the same credentials secret — nothing extra to configure. When enabled it becomes the endpoint your applications connect to.minReplicas and maxReplicas.
Backup
Two modes share the same provider settings.logical runs a cron workload that dumps every database and role with pg_dumpall through HAProxy; wal-g adds a sidecar to each member that archives WAL continuously and takes a base backup from the leader every backup.walg.intervalSeconds. Each provider’s bucket, cloud account and policy steps are under Backing Up.
Connecting
This template exposes no public endpoint. Everything below is reachable from inside the GVC, subject tointernal_access.type.
psql:
pg_is_in_recovery() returns f through HAProxy, because it always reaches the leader. The same query on a member directly returns t on a replica:
--replica to pick a member (RELEASE_NAME-postgres-ha-0 through RELEASE_NAME-postgres-ha-N); without it the CLI picks one for you. The superuser credentials are already present in the container’s environment, so psql does not prompt.
Applications should retry on connection errors with backoff. A failover, a rolling restart or a scale-down moves the leader, and connections open at that moment are reset while HAProxy re-routes to the new leader.
Operations
Checking Cluster State
Patroni’s own tool shows every member, its role, its state and its replication lag:/tmp/patroni_config.yml is the configuration the startup script writes in each member; patronictl needs it to find etcd. The same tool shows the cluster-wide settings Patroni keeps in etcd:
http://127.0.0.1:8405/stats in a browser:
Cluster Metrics
From template version 2.7.0, Patroni’s Prometheus metrics are scraped into Control Plane’s built-in metrics stack automatically — there is no exporter to run and nothing to configure. They appear in the built-in Grafana alongside the standard workload metrics, labelled byworkload, replica and container.
patroni_primary summing to anything other than 1 across the cluster, patroni_cluster_unlocked staying 1, or patroni_postgres_streaming dropping to 0 on a follower. These are cluster-state metrics, not database performance — connection counts, cache hit ratio and slow queries would need a postgres_exporter, which this template does not include.
Backing Up
Backups are off by default. Enable them withbackup.enabled: true, pick a backup.mode and a backup.provider, and complete that provider’s setup below first.
-
logical—RELEASE_NAME-postgres-ha-backupruns onbackup.logical.schedule, connects through HAProxy with theusernameandpasswordfrom your credentials secret, and uploadspostgres-TIMESTAMP.sql.gzunderbackup.PROVIDER.prefix. HAProxy must be enabled. -
wal-g— every member gets awal-g-backupsidecar and PostgreSQL archives each WAL segment to the bucket as it completes (archive_timeout60 s). The sidecar on whichever member is the leader takes a base backup everybackup.walg.intervalSeconds, retrying every minute until one succeeds — including right after a failover. WAL archiving on its own cannot restore a cluster; it replays from a base backup. Confirm one exists before relying on this mode:The sidecar does not prune old backups; manage retention in the bucket.
Backup Prerequisites
Complete the steps for your provider before enabling backups.AWS S3
-
Create an S3 bucket. Set
backup.aws.bucketto its name andbackup.aws.regionto its region. -
If you do not have one yet, create a Control Plane cloud account for the AWS account holding the bucket. Set
backup.aws.cloudAccountNameto its name. -
Create an IAM policy scoped to exactly that bucket, replacing
YOUR_BUCKET_NAME:
- Set
backup.aws.policyNameto that policy’s name. The template attaches it to the workload identity together withcpln-connectorand nothing else — the bucket in your policy is the only storage the backup containers can reach. Upgrading from2.5.0or earlier narrows this further; see Upgrading From 2.5.0.
GCS
-
Create a GCS bucket. Set
backup.gcp.bucketto its name. -
Create a Control Plane cloud account for the GCP project holding the bucket, and set
backup.gcp.cloudAccountNameto its name. -
Grant the cloud account’s service account the Storage Admin role. The template additionally binds the identity to
roles/storage.objectAdminon exactly the bucket inbackup.gcp.bucket.
MinIO
No cloud account is needed — the backup containers authenticate with a second prerequisite secret. Both modes work:wal-g uses its native S3-compatible support with path-style addressing.
-
Create the bucket in MinIO. Set
backup.minio.bucketto its name. -
Set
backup.minio.endpointto the S3 API address including the port. For the minio template deployed in the same GVC that ishttp://WORKLOAD_NAME.GVC_NAME.cpln.local:9000. -
Create a dictionary secret holding exactly the keys
accessKeyandsecretKey, and setbackup.minio.credentialsSecretNameto its name. For theminiotemplate these are its admin username and password:
reveal on this secret as well as the database credentials secret. If the secret does not exist at install time the deployment wedges exactly as described in Prerequisites.
Restoring a Backup
logical commands are the standard way to replay a pg_dumpall script and match what the backup job writes; the wal-g outline follows the chart’s configuration and WAL-G’s documentation. Rehearse whichever you depend on against a scratch release before you need it.Logical
Each backup is a gzip-compressedpg_dumpall script, so it is restored by connecting through HAProxy to the postgres maintenance database as the superuser from your credentials secret; the script recreates the roles and databases it contains, and HAProxy guarantees the writes land on the leader. Run the commands from a client that can reach both the bucket and the cluster — a workload inside the GVC, or your own machine with cpln port-forward RELEASE_NAME-postgres-ha-proxy 5432:5432 --gvc GVC_NAME open and --host=127.0.0.1 in place of the internal hostname.
- AWS S3
- GCS
- MinIO
already exists errors for the superuser, the repl replication role and the database that the cluster created at first boot are expected; the rest of the script loads normally. Restore into an empty cluster (a fresh install, or one whose databases you have dropped) to avoid duplicate-object errors on existing tables. The replicas receive the restored data through normal streaming replication — nothing is needed on them.
WAL-G
WAL-G restores into an empty data directory, which this template has no automation for: a running member already has a data directory, and Patroni refuses to adopt a data directory whose system identifier does not match the cluster recorded in etcd. The procedure below is the shape of a manual restore. It is unverified on this template — treat it as an outline to rehearse, not a runbook.-
List the base backups and pick one (
LATESTis also accepted bybackup-fetch): -
Install a new release of this template into the GVC with
backup.enabled: false, and stop it before Patroni initializes — or run a one-off workload of your own using the sameimage, with a fresh volume set mounted at/var/lib/postgresql/dataand the sameWALG_*/AWS_*environment the sidecar has (comparecpln workload get RELEASE_NAME-postgres-ha --gvc GVC_NAME -o yaml). -
In that container, fetch the base backup into the empty
PGDATAand configure recovery. The chart already setsrestore_command: wal-g wal-fetch %f %p; for a point in time addrecovery_target_time: - Start PostgreSQL once on that data directory so it replays WAL to the target and finishes recovery, then point the new release’s members at the recovered volume and let Patroni initialize a new cluster from it.
-
Change
backup.aws.prefix(or the GCS / MinIO prefix) before re-enablingwal-gbackups on the restored cluster — it has a new system identifier and must not archive into the old cluster’s prefix.
logical backups for recovery and treat wal-g as an archive.
Rotating the Password
Patroni writes its configuration from the secret each time a member starts and applies the password to the database only when the cluster is first initialized. After that the stored passwords live in PostgreSQL, so a rotation touches three places, and the order matters. This procedure has not been rehearsed on this template — practise it on a scratch release first.-
Update the
passwordkey in your credentials secret to the new value — for example withcpln apply -fof the full secret manifest — so any member that restarts from now on reads the new one. Nothing running changes yet: workloads resolve a secret only when a replica starts. -
On the leader (through HAProxy), change both roles that share the password. The replication role
replwas created with the same password as the superuser, and every member’s Patroni configuration will expect the new one after its next restart: -
Force a redeployment of the members, then of PgBouncer if it is enabled, so every process holds the new password. Expect a brief write outage while the members restart:
logical backup job reads the secret fresh on its next scheduled run and needs nothing else.
Upgrading From a Version Before 2.4.2
Template versions before2.4.2 did not compact the bundled etcd cluster, so its backend grows with time alone and goes read-only once it reaches the 2 GiB quota — after which the PostgreSQL members restart-loop with exitCode: 0 and reason: Completed, which reads as healthy and is easily mistaken for a database fault. Upgrading turns compaction on and stops further growth, but it cannot shrink a backend that has already grown, and a cluster that has already raised a NOSPACE alarm needs operator recovery rather than an upgrade. Check first, with the read-only commands in If the Backend Quota Is Already Full, run against RELEASE_NAME-etcd.
Upgrading From 2.4.x
Template version2.5.0 removed the postgres block. postgres.username, postgres.password and postgres.database are now the prerequisite dictionary secret named by config.credentialsSecretName, and MinIO backup credentials moved the same way. An upgrade that still carries any of the old keys is refused at render, before anything is applied, so your running cluster is left untouched:
Read the credentials the cluster already uses
Create the prerequisite secret with those same values
username, password and database. Different values here do not change the database — they leave every member unable to authenticate after its next restart.Remove the old keys from your values
postgres: block and set config.credentialsSecretName. If you back up to MinIO, replace backup.minio.accessKey and backup.minio.secretKey with backup.minio.credentialsSecretName.Upgrade, then rotate any default password
2.4.2 and earlier shipped username: username and password: password as defaults, published in the public template repository. If your cluster still runs them, treat them as compromised and follow Rotating the Password after the upgrade.Upgrading From 2.5.0
Template version2.5.1 removes aws::ReadOnlyAccess from the backup identity. That AWS managed policy granted read access to every bucket in your AWS account and contains no write actions, so it never carried the backup itself — but it was silently supplying any read action your own bucket-scoped policy happened to omit. Update your IAM policy to the full action list in AWS S3 before upgrading; if it already matches, no action is needed. The identity now carries cpln-connector and your bucket-scoped policy only.
Upgrading From a Version Before 2.6.0
Two things changed in2.6.0 that an upgrade does not apply to a cluster that already exists:
-
Patroni’s consensus timeouts. The chart now initializes new clusters with
ttl: 45,loop_wait: 10andretry_timeout: 15. Earlier versions asked forttl: 30withretry_timeout: 30, a combination Patroni silently clamps rather than rejecting, so those clusters tolerated a shorter etcd outage than configured before demoting a healthy leader. Patroni reads these values only while the data directory is empty; afterwards they live in etcd, and an existing cluster keeps the ones it was created with. Move it withpatronictl— all three together, because Patroni requiresloop_wait + 2 × retry_timeout ≤ ttland quietly rewrites the others when one is changed alone:Confirm with theshow-configcommand in Checking Cluster State. -
Enabling
wal-gon an existing cluster. Before2.6.0, turning onbackup.mode: wal-gafter the cluster was initialized did not start WAL archiving — only a fresh install archived. From2.6.0the archive settings apply to running members when they restart, so enablingwal-gon an upgraded cluster works; verify withwal-g backup-listas in Backing Up.
failsafe_mode is on for clusters created from 2.6.0: when the leader loses etcd but every member still answers its REST API, it keeps serving instead of demoting. A cluster created earlier gains it only through edit-config -s failsafe_mode=true.
Scaling and Availability
- Leader loss fails over automatically. When the leader stops answering, Patroni promotes the most up-to-date replica and HAProxy’s health checks move port
5432to it. Writes are refused in between and connections open at that moment are reset — build retry with backoff into your applications. - Planned restarts hand leadership off first. Before a leader is stopped, the chart asks Patroni to switch over to a replica, so a rolling restart costs a few seconds of refused writes rather than a full failover. A
cpln helm upgradethat changes the member workload can restart members together; expect a brief write outage and plan upgrades as maintenance. - Losing etcd does not fail the cluster over. With
failsafe_modeon, a leader that cannot reach etcd keeps serving as long as every other member still answers its REST API, and only a member Patroni recorded as eligible can win the next leader race. An etcd outage that also hides a replica still demotes the leader. - A member that stays down is re-cloned, not fatal. WAL retained for an absent member is capped at a quarter of
volumeset.capacity; past that, its slot is dropped and it re-clones from the leader when it returns. - Keep
replicasat 3 or more andetcd.replicasodd. Two members cannot fail over safely, and an even etcd count adds nothing to its fault tolerance. Chooseetcd.replicasat install time. - Changing
replicason a running release adds or removes members; a removed member that held the leader role triggers a failover. Afterwards force a redeployment ofRELEASE_NAME-postgres-ha-proxyso HAProxy picks up the new member list. - The first
cpln helm upgradeafter an install can re-apply resources that did not change and restart them. Plan it as a short outage. multiZone: trueon the members and on etcd spreads each across zones within the location, when the location supports it.
Troubleshooting
Workload never becomes ready and cpln logs returns nothing
Workload never becomes ready and cpln logs returns nothing
RELEASE_NAME-postgres-ha stays not ready indefinitely, and cpln logs '{gvc="GVC_NAME", workload="RELEASE_NAME-postgres-ha"}' --limit 50 --since 10m returns zero lines.Cause: config.credentialsSecretName (or, with MinIO backups, backup.minio.credentialsSecretName) names a secret that does not exist, so the container is never started. The message is only visible in status.versions[].message:cpln workload force-redeployment RELEASE_NAME-postgres-ha --gvc GVC_NAME.Upgrade fails with 'the postgres block was REMOVED in 2.5.0'
Upgrade fails with 'the postgres block was REMOVED in 2.5.0'
cpln helm upgrade stops before touching any resource:backup.minio.accessKey / backup.minio.secretKey produce the same kind of message under their own names.Cause: your values still carry keys that were removed in 2.5.0.Fix: follow Upgrading From 2.4.x. Your running cluster is unaffected until the upgrade succeeds.HAProxy logs 'waiting for patroni endpoints to answer before starting haproxy'
HAProxy logs 'waiting for patroni endpoints to answer before starting haproxy'
RELEASE_NAME-postgres-ha-proxy never becomes ready and cpln logs '{gvc="GVC_NAME", workload="RELEASE_NAME-postgres-ha-proxy"}' --limit 50 --since 10m repeats pg-ha proxy: waiting for patroni endpoints to answer before starting haproxy — pgN (...) not responding yet.Cause: HAProxy will not start until every member’s Patroni REST API answers on port 8008. Either the members are still starting (normal for the first minutes of an install), or internal_access.type: workload-list does not include the proxy workload, so it cannot reach them.Fix: wait for RELEASE_NAME-postgres-ha to become ready. If it is ready and HAProxy still waits, add //gvc/GVC_NAME/workload/RELEASE_NAME-postgres-ha-proxy to internal_access.workloads as described in Internal Access. The container gives up and restarts after ten minutes, so it recovers on its own once the members are reachable.Members restart-loop with exitCode 0 and reason Completed
Members restart-loop with exitCode 0 and reason Completed
cpln workload get-deployments RELEASE_NAME-postgres-ha --gvc GVC_NAME -o yaml, each exit reading exitCode: 0 and reason: Completed, while nothing in the database logs looks wrong.Cause: etcd has gone read-only. A Patroni member that cannot renew its lease exits cleanly, so the failure reads as healthy. The usual reason is an uncompacted etcd backend that has reached its 2 GiB quota and raised a NOSPACE alarm (template versions before 2.4.2).Fix: check etcd first, with two read-only commands:alarm list. NOSPACE confirms it; recovery is the operator procedure in If the Backend Quota Is Already Full, followed by an upgrade to 2.4.2 or later so it does not recur.Clients get 'remaining connection slots are reserved' through PgBouncer
Clients get 'remaining connection slots are reserved' through PgBouncer
RELEASE_NAME-pgbouncer fail with FATAL: remaining connection slots are reserved for roles with the SUPERUSER attribute (or ... for non-replication superuser connections) instead of waiting in the pool.Cause: pgbouncer.maxDbConnections is enforced per PgBouncer replica, so maxReplicas × maxDbConnections server connections can be opened against a cluster max_connections of 100 (97 usable).Fix: lower pgbouncer.maxDbConnections or pgbouncer.maxReplicas so their product stays under 97, and keep pgbouncer.defaultPoolSize within the same budget — see PgBouncer Connection Pooling.wal-g backup-list shows no base backup, or the sidecar restarts with exitCode 137
wal-g backup-list shows no base backup, or the sidecar restarts with exitCode 137
wal-g backup-list prints nothing hours after enabling backup.mode: wal-g, and cpln workload get-deployments RELEASE_NAME-postgres-ha --gvc GVC_NAME -o yaml shows the wal-g-backup container restarting with exitCode: 137.Cause: the sidecar is being OOM-killed mid-backup. Nothing is logged from inside the container when that happens; backup.resources.memory is below what wal-g backup-push needs.Fix: set backup.resources.memory to at least the shipped 512Mi and upgrade. The sidecar retries every minute, so a base backup appears shortly after it has enough memory.Apply rejected with 'The ratio between cpu and minCpu must be less than 4:1'
Apply rejected with 'The ratio between cpu and minCpu must be less than 4:1'
resources.maxCpu is more than four times resources.minCpu, which the platform rejects on a stateful workload.Fix: raise resources.minCpu or lower resources.maxCpu so the ratio is at most 4:1.Important Notes
- Create the credentials secret before installing. A reference to a secret that does not exist wedges the members with no log output; see Prerequisites for the one command that shows the reason.
- Connect through HAProxy (
RELEASE_NAME-postgres-ha-proxy), or PgBouncer when enabled. A member reached directly may be a read-only replica. - Credentials are applied only when the cluster is first initialized, and the
replreplication role shares the superuser password. Rotate in the order given in Rotating the Password. - Keep
replicasat 3 or more andetcd.replicasodd, and chooseetcd.replicasat install time. - Do not change
etcd.tuningto disable compaction — an uncompacted etcd takes the whole cluster read-only months later. - With
workload-listaccess, list the release’s own workloads as well as your applications, or HAProxy cannot reach the members. - Confirm a
wal-gbase backup exists withwal-g backup-listbefore relying on that mode; WAL alone cannot be restored. - Restore procedures are unverified on this template — rehearse the one you depend on against a scratch release.
cpln helm uninstalldeletes both volume sets and the data with them. Your credentials secrets are yours and are left alone.- Upgrading from 2.4.x is a breaking change — the removed keys stop the upgrade rather than resetting a password. See Upgrading From 2.4.x.
External References
Patroni Documentation
patronictl reference