Overview
Postgres Multi-Location deploys one PostgreSQL 17 cluster, managed by Patroni, whose members span several Control Plane locations: a single primary that takes all writes, asynchronous streaming replicas in the other locations, and automatic promotion of a replica in a surviving location when the primary’s location is lost. An HAProxy tier in every location routes connections to whichever member currently holds the leader lock, so applications connect to a stable name in their own region and never need to know where the primary is. An optional PgBouncer pooler per location and optional backups to object storage (scheduledpg_dumpall, or continuous WAL-G archiving) complete the stack.
Consensus is provided by a bundled etcd Multi-Location cluster with one member per location. That quorum — not PostgreSQL — decides what the cluster survives; see Locations and Quorum.
global.locations must already be one of them. A GVC location you do not list runs nothing — its deployment reads This workload location is deactivated because maxScale is set to 0. For a cluster in a single location use PostgreSQL Highly Available instead; this template cannot run in one location because its consensus store is a stretched etcd cluster with exactly one member per location.What Gets Created
repl uses the same password. Patroni members are named RELEASE_NAME-postgres-LOCATION-INDEX so that patronictl list maps directly onto your location list.
Prerequisites
Two things must exist before you install: a GVC with the right locations, and the credentials secret. Secrets are org-level, so no GVC flag is involved.A GVC with at least two locations, covering every entry in global.locations
spec.staticPlacement.locationLinks is the list. Every name you put in global.locations must appear there — a declared location the GVC lacks makes a fresh member refuse to start (see Troubleshooting). Three locations is the recommended production shape; see Locations and Quorum.Create the database credentials secret
username, password and database. Patroni creates that superuser and that database when the cluster first bootstraps, and the replication role repl is given the same password. Use plain identifiers for username and database; they are used unquoted when the database is created.postgres.credentialsSecretName to this name. Read it back later with cpln secret reveal POSTGRES_CREDENTIALS_SECRET_NAME -o yaml — a bare cpln secret reveal prints only a summary table, not the values.Installation
The defaultglobal.locations list (aws-us-east-1, aws-eu-central-1, aws-us-west-2) is an example, not a recommendation — set it to your GVC’s own locations in a values file, then install, pointing the chart at the secret you created:
--set: a --set global.locations[0].name=... override replaces the whole list with a single entry, which the chart rejects.
To install with another method, follow the instructions for your preferred tool:
UI
CLI
Terraform
Pulumi
Configuration
Each block below is the shipped default for one area ofvalues.yaml. Override only what you need with a values file or --set.
Locations and Quorum
global.locations[].name— A Control Plane location that must already be one of the locations of the GVC you install into. At least two are required, each listed once.global.locations[].replicas— Patroni members in that location (minimum1). etcd always runs exactly one member per location and ignores this field.
global so that Helm passes the same list to the bundled etcd chart; the two can never be edited apart. Removing a location from the list is the supported way to shrink the cluster — there is no per-location suspend, and a replicas of 0 is rejected at render time with a message telling you to remove the location instead.
The consensus store commits a write only when a majority of its members agree, and it runs exactly one member per location. That arithmetic decides what the cluster survives:
N locations you survive floor((N-1)/2) losses, so an even count buys nothing over the odd count below it. Two locations is permitted as a deliberate warm-standby topology, but it survives nothing automatically.
global.locations and the GVC in neither direction, so the template closes both. A GVC location you do not list runs nothing (harmless — it is how you deploy into a subset of a larger GVC). A listed location the GVC does not have is accepted and stored, and the member that should run there never starts; the members catch this themselves by reading their own GVC at boot. On a fresh data directory that is a hard failure with a [patroni] FATAL: line naming the missing location; on an already-initialized member it is a warning only, because a location removed from a GVC looks exactly like a location that is down, and surviving that is what this template is for. See Troubleshooting for the exact messages.global.locations on a running release restarts every Patroni member and every etcd member with a new roster. Plan it as a maintenance window and change the GVC’s locations and this list together.Image and Resources
image— PostgreSQL 17 with Patroni and WAL-G. The same image runs the WAL-G sidecar whenbackup.mode: wal-g.resources.minCpu/resources.minMemory— Reserved CPU and memory per member.resources.maxCpu/resources.maxMemory— Limits per member. On a stateful workload the ratio ofmaxCputominCpumay not exceed 4:1; a larger ratio is rejected when the workload is applied.
max_connections: 100, shared_buffers: 128MB, wal_level: replica, hot_standby: on, max_slot_wal_keep_size: 10GB) and is initialized with UTF8 encoding and data checksums. Client authentication is password-based (md5) from any address; which workloads can reach the cluster at all is decided by Internal Access.
Credentials
postgres.credentialsSecretName names the dictionary secret described in Prerequisites. The Patroni members, the WAL-G sidecar, PgBouncer and the backup job all read the same secret. The three keys are applied only when the cluster first bootstraps; changing the secret afterwards does not change the stored password — see Rotating the Password.
Preferred Primary Location
primaryLocation names the location you want the primary in. Leave it empty for no preference, in which case the primary starts wherever a member gets there first. When set, it must be one of the names in global.locations and does three things:
- On a fresh install it decides where the primary starts. Members outside the preferred location wait up to 90 seconds for that location to initialize the cluster before bootstrapping themselves. If the preferred location is down, slow or misconfigured they bootstrap anyway and log
WARNING: no leader in LOCATION after 90s — bootstrapping here instead; the cluster is then correct but the primary is elsewhere, and you move it withpatronictl switchover(see Managing the Cluster With patronictl). The wait cannot split the cluster: the consensus store is the arbiter, so a member that gives up early finds the cluster already initialized and joins it as a replica. - It biases failover elections toward that location through Patroni’s
failover_prioritytag, a tiebreaker between candidates that have replayed the same amount of WAL. It does not fail back automatically after an outage: a restored preferred location keeps the current leader until the next election. - Changing it on a live cluster moves the leader. The value is baked into the startup script, so editing it restarts every member, and the election that follows picks the preferred location. That is a full restart with a write outage — to move a primary without one, use
patronictl switchoverinstead.
Storage
volumeset.capacity— Initial size in GiB of the volume every member gets forPGDATA(minimum 10). The volume set keeps snapshots for 7 days and takes a final snapshot when it is deleted.volumeset.autoscaling.*— Grow the volume automatically when free space drops belowminFreePercentage, multiplying the capacity byscalingFactorup tomaxCapacity. The growth event itself has not been exercised on this template.
Internal Access
internalAccess applies to the PostgreSQL, HAProxy and PgBouncer tiers alike. There is no public access — every endpoint is internal to Control Plane.
internalAccess can take several minutes to take effect in every location — re-test before concluding it did not apply.
Leader-Routing Proxy
GET /primary on port 8008) and forwards port 5432 to whichever one currently answers as the primary, so applications get a stable endpoint in their own location that follows the leader across a failover.
proxy.minReplicas/proxy.maxReplicas— Autoscaling bounds per location; the default2means two proxy replicas in each configured location.- Port
8404serves/healthz(healthy while a primary backend is reachable) and port8405serves/stats, the live HAProxy status of every member.
logical backups, which must dump from the current primary; the chart refuses to render backup.mode: logical with the proxy disabled. Enabling PgBouncer enables the proxy implicitly, even if proxy.enabled is false.PgBouncer
pgbouncer.minReplicas/pgbouncer.maxReplicas— Autoscaling bounds per location.pgbouncer.maxDbConnections— Enforced per PgBouncer replica, not across the tier. PgBouncer instances do not coordinate, so the real ceiling on server connections ismaxReplicas × maxDbConnections— at the shipped values 4 × 100 = 400 against the cluster’smax_connectionsof 100, three of which are reserved for superusers. Size it somaxReplicas × maxDbConnectionsstays comfortably under 97, and treatdefaultPoolSizethe same way; otherwise clients getremaining connection slots are reservedunder load instead of being queued.
Backups
backup.location—logicalmode only. A cron workload runs in every location of its GVC, so without this selector a three-location cluster would dump the same database three times a night into one bucket. The WAL-G sidecar needs no selector: only the member holding the leader lock pushes, so the archive follows the primary across a failover.backup.resources.memory— Do not lower it below the shipped512Mi. The Google Cloud Storage path fails at128Miwith no error line anywhere — alogicaljob simply reportsfailed, and thewal-gsidecar is killed in a loop while WAL accumulates with no base backup to restore against.backup.providerand theaws/gcp/minioblocks apply to whichever mode is enabled. Each provider’s bucket, cloud account, policy and secret steps are under Backing Up.
backup.mode to or from wal-g restarts PostgreSQL in every location, because it changes archive_mode. Enabling logical backups does not restart anything.etcd Consensus Store
etcd block is passed straight through to the bundled etcd Multi-Location chart, pinned to 2.0.0, which is where its image, resources, Raft timers, storage and access settings are documented in full. It reads its location list from the same global.locations.
etcd.tuning.autoCompactionMode/autoCompactionRetention/quotaBackendBytes— Leave at the defaults. Patroni renews its leader lease every few seconds and every renewal is an etcd revision, so an uncompacted backend grows with time alone until etcd goes read-only and takes the database’s failover with it. Compaction cannot be switched off, only retuned; see Compaction and Backend Growth for the mechanism and the accepted value formats.etcd.tuning.heartbeatIntervalMs/electionTimeoutMs— The Raft timers, tuned for cross-region round trips; raise both in proportion if your locations are more than about 250 ms apart.etcd.recovery.forceNewClusterInLocation— Leave empty in normal operation. It is the manual recovery path for a two-location cluster that has lost a location permanently; see Recovering From a Lost Location.
Connecting
The cluster has no public endpoint: external inbound traffic is closed and no load balancer is created. Clients connect from inside the GVC, subject to Internal Access, or from your machine through a port-forward. Always use the fully qualified hostname; the bare workload name is not reliably resolvable.psql:
pg_is_in_recovery() returns f — the proxy always lands on the primary. To see every member, its location, role and replication lag, run patronictl inside any member (the startup script writes Patroni’s config to /tmp/patroni_config.yml):
Operations
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. Both modes write to the same bucket settings.
logical— On every run ofbackup.logical.schedulethe cron workload connects through the proxy to the current primary, runspg_dumpall, and uploadspostgres-TIMESTAMP.sql.gzunderbackup.PROVIDER.prefix. The dump includes every database and role.wal-g— Every member archives WAL segments withwal-g wal-pushand, on the member holding the leader lock, thewal-g-backupsidecar pushes a base backup everybackup.walg.intervalSeconds(retrying every minute until the first one succeeds, so a newly promoted leader backs up promptly). Base backups and WAL land underbackup.PROVIDER.prefix, compressed withzstd.
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, together withcpln-connector, to the workload identity and nothing else — the bucket in your policy is the only storage the backup can reach.
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 (
roles/storage.admin) role on the project. The template additionally binds the identity toroles/storage.objectAdminon exactly the bucket inbackup.gcp.bucket.
MinIO
No cloud account is needed — the backup authenticates with a second prerequisite secret.-
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— use the fully qualified name, because a bare short name does not resolve for every workload type. -
Create a dictionary secret holding exactly the keys
accessKeyandsecretKey, and setbackup.minio.credentialsSecretNameto its name. For the MinIO template these are its admin username and password:
reveal on this secret as well as the database credentials secret, and on nothing else.
Manual Dump on Demand
For a one-off copy outside the schedule, dump a database from inside a member. Pass the output throughbase64 on both sides — the exec stream is text-only and corrupts raw binary — and run pg_dump against the local member, which works on a replica as well as on the primary:
PG_USERNAME, PG_PASSWORD and PG_DATABASE.
Restoring a Backup
Logical
Each scheduled backup is a gzip-compressedpg_dumpall script, so it is restored by connecting through the proxy — which always writes to the current primary — to the postgres maintenance database as the superuser from your credentials secret; the script recreates the roles and databases it contains. 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-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 role and the database that the cluster created at bootstrap 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. Replicas receive the restored data through streaming replication; nothing has to be done per location.
A manual pg_dump -Fc file from Manual Dump on Demand goes back in with pg_restore, again through the proxy and again base64-encoded, with --stdin so the file actually reaches the container:
pg_dumpall dump of this template was replayed into a fresh cluster with zero errors during the template’s test cycle; the exact commands above follow that procedure but were not themselves run against a 2.0.0 release. Rehearse them on a test release.WAL-G
The template provides no supported way to restore a WAL-G backup into the running cluster. A WAL-G restore needs an empty data directory, the Patroni workload’s volume set is named by the release and rendered into the workload, and nothing invalues.yaml names or substitutes a volume set; re-pointing the workload by hand would be reverted by the next upgrade. What the archive does give you is a complete, restorable copy of the database outside this release.
To see what is in the archive, list the base backups from the sidecar, which already carries the bucket settings and credentials:
WALG_* and cloud settings as the sidecar — wal-g backup-fetch DATA_DIRECTORY BACKUP_NAME, then start PostgreSQL with restore_command = 'wal-g wal-fetch %f %p' and a recovery.signal file so it replays the archived WAL — and load what you need into the cluster with pg_dump / pg_restore through the proxy. Follow the WAL-G documentation for the restore itself.
Rotating the Password
Thepassword key is applied when the cluster first bootstraps and is then used in three places: by the superuser role, by the repl replication role, and by Patroni’s own connections to each member’s PostgreSQL. Changing the secret alone changes none of them — a running replica resolves the secret only when it starts. This rotation has not been exercised on this template; treat it as a short planned outage and rehearse it on a test release first.
Change both roles inside PostgreSQL
Update the secret to match
password key and apply the whole secret manifest:Force every consumer to re-read it
Managing the Cluster With patronictl
patronictl is in the image and reads the config the startup script writes to /tmp/patroni_config.yml. Run it inside any member; --location picks the member, and any name from global.locations will do. Members are named RELEASE_NAME-postgres-LOCATION-INDEX.
ttl: 45, loop_wait: 10, retry_timeout: 15, maximum_lag_on_failover: 33554432 (32 MiB), failsafe_mode: true and use_pg_rewind: true. Change them on a live cluster with edit-config, keeping all three timers together because Patroni requires loop_wait + 2 × retry_timeout <= ttl and silently substitutes its own values when that is violated:
Upgrading From 1.0.0 or 1.0.1
Version 1.0.2 dropped the-ml infix from every PostgreSQL-side resource name: the workloads, secrets, identity, policy and the volume set changed from …-postgres-ml… to …-postgres… (for example the proxy workload from …-postgres-ml-proxy to RELEASE_NAME-postgres-proxy, and the data volume set from …-postgres-ml-vs to RELEASE_NAME-postgres-vs). The etcd resources did not change.
Since every 1.x release also created its own GVC, a cluster on 1.0.0 or 1.0.1 should go straight to 2.0.0 by the procedure below rather than stopping at 1.0.2 — it is the same backup-and-restore either way.
Migrating From Version 1
Every release before 2.0.0 created its own GVC; 2.0.0 deploys into one you already have. Install 2.0.0 as a new release against an existing GVC, move the data across, then remove the old release.Back up the 1.x cluster and verify the dump
backup.mode: logical, or run pg_dumpall through the old release’s proxy endpoint (on 1.0.0 and 1.0.1 that workload carries the -ml infix). Confirm the object is a real dump and not a nearly empty one before going further — it is the only copy that crosses the boundary.Choose the target GVC and rewrite your values
cpln gvc get GVC_NAME -o yaml. Do not use the GVC the 1.x release created — it is still owned by the old Helm release and goes away when you uninstall it. In your values, delete global.gvc.name and rename global.gvc.locations to a top-level global.locations.Install 2.0.0 as a new release
postgres.credentialsSecretName at a secret holding the credentials your applications already use.Restore the dump and move your connection strings
RELEASE_NAME-postgres-proxy.GVC_NAME.cpln.local:5432 (or the PgBouncer address).Remove the old release
Scaling and Availability
What the cluster survives follows from the quorum table in Locations and Quorum; the points below are how each case plays out.- Losing a replica, or a whole non-primary location, is survived automatically. The primary keeps serving writes; a member that comes back with its volume intact rejoins and catches up by streaming. The primary retains up to 10 GB of WAL for an absent member (
max_slot_wal_keep_size) — a durability-for-availability trade, so that a location down for hours cannot fill the primary’s volume — and a member absent longer than that re-clones from the primary automatically. - Losing the primary, or its location, with three or more locations is an automatic failover. Patroni promotes a replica in a surviving location, every location’s proxy re-routes to it, and PgBouncer follows the proxy, so clients need no reconfiguration. The old primary rejoins as a replica when it returns (
use_pg_rewindis on). Only replicas withinmaximum_lag_on_failover(32 MiB) of the primary are eligible — check the lag column ofpatronictl list. - Replication is asynchronous, so a failover can lose the most recent transactions — those that had not yet reached the promoted replica. Check lag on the primary with
pg_stat_replication, usingapplication_name(the member name) to tell members apart. - With two locations there is no automatic failover. The survivor holds current data but stays read-only until you promote it by hand — see Recovering From a Lost Location. This manual path has not been exercised on this template.
- A planned move is nearly seamless.
patronictl switchover(see Managing the Cluster With patronictl) hands the leader lock over with a brief write pause, and a member that is shut down cleanly — a restart, a redeploy — performs the same switchover first through its pre-stop hook. - A
cpln helm upgradecan restart members in every location together, including an upgrade that changes nothing. The data survives, but expect a brief write outage while a leader is re-elected, so plan every upgrade — and every change toprimaryLocation,global.locationsorbackup.mode— as a short planned outage with application retries in place. - A restart of the etcd tier briefly interrupts writes and recovers on its own. Patroni’s failsafe mode lets the primary keep serving while the consensus store is unreachable as long as it can still reach every other member; the cluster re-promotes and resumes without operator action.
- Applications must retry transient errors. During a failover expect connection-level failures (
server closed the connection unexpectedly,terminating connection due to administrator command) and, briefly,ERROR: cannot execute INSERT in a read-only transactionwhile the proxy’s health check catches up with a demoted member. Exponential backoff over a few seconds covers a failover; an upgrade needs a window of a couple of minutes. - To scale, raise
global.locations[].replicasfor more members in a location (new members clone from the primary),proxy.*Replicas/pgbouncer.*Replicasfor more capacity per location, andvolumeset.autoscalingfor storage. Do not run more than one etcd member per location —replicasaffects Patroni only. - Cost scales with write volume multiplied by the members outside the primary’s location. Each of them receives a full copy of the WAL stream, and cross-region traffic is billed. Read-mostly workloads are inexpensive to stretch; write-heavy ones are not.
Recovering From a Lost Location
With three or more locations this section does not apply — losing one location is an automatic failover, and a member that returns rejoins on its own. With two locations, losing one loses consensus quorum permanently: the surviving member holds current data but cannot be granted the leader lock, and consensus writes hang rather than failing fast. Recovery is the bundled etcd chart’s emergency path, driven from this chart’setcd.recovery.forceNewClusterInLocation value. It has not been exercised on this template; follow it on a test release before relying on it.
Confirm the loss is permanent
Force a new single-member consensus store in the surviving location
etcd.recovery.forceNewClusterInLocation to the surviving location’s name and upgrade the release with your existing values:patronictl list.Clear the setting
etcd.recovery.forceNewClusterInLocation back to "" and upgrade again. Leaving it set means the flag fires on every future restart of that member.Reset the lost location's volumes before its members rejoin
global.locations and the GVC, or reset its volumes, before bringing it back.Troubleshooting
When a member will not come up, its own[patroni] log lines name the reason (the etcd members log [etcd] lines in the same way):
Workload never becomes ready and cpln logs returns nothing
Workload never becomes ready and cpln logs returns nothing
RELEASE_NAME-postgres stays not ready in every location indefinitely, and cpln logs '{gvc="GVC_NAME", workload="RELEASE_NAME-postgres"}' --limit 50 --since 10m returns zero lines.Cause: postgres.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 --gvc GVC_NAME.Install or upgrade fails: the global.gvc values key was REMOVED
Install or upgrade fails: the global.gvc values key was REMOVED
global.gvc block. Version 2.0.0 no longer creates a GVC and refuses to render rather than let an in-place upgrade delete the one the old release created. Because the bundled etcd chart renders first, the message may name etcd-multi-location rather than this chart; the remedy is the same.Fix: do not upgrade in place. Follow Migrating From Version 1: install 2.0.0 as a new release with global.locations, restore, then uninstall the old release.Install fails: requires at least 2 locations in global.locations
Install fails: requires at least 2 locations in global.locations
global.locations at render time and rejects a list with fewer than two entries, a duplicated location, or a replicas below 1. A --set global.locations[0].name=... override replaces the whole list with a single entry and triggers this too. The message may name etcd-multi-location, which renders first.Fix: supply the full list in a values file (see Installation). For a single-location cluster use PostgreSQL Highly Available.Install fails: backup.mode 'logical' requires the leader-routing proxy
Install fails: backup.mode 'logical' requires the leader-routing proxy
backup.mode: logical with proxy.enabled: false and pgbouncer.enabled: false. The dump must run against the current primary, and without the proxy there is no stable address for it.Fix: set proxy.enabled: true (enabling PgBouncer also enables it), or use backup.mode: wal-g, which runs on the members themselves.Member exits with [patroni] FATAL: locations declared in global.locations are not in GVC
Member exits with [patroni] FATAL: locations declared in global.locations are not in GVC
1; the log names the missing location(s) and reads That is a bare majority: the cluster would form, report healthy, and have ZERO fault tolerance.Cause: global.locations names a location the GVC does not have. The members in the other locations could still form a cluster, but one with no fault tolerance, so a fresh member refuses to bootstrap it. An already-initialized member logs the same condition as a [patroni] WARNING and keeps serving.Fix: add the missing location to the GVC, or remove it from global.locations, and redeploy. cpln gvc get GVC_NAME -o yaml shows the GVC’s list under spec.staticPlacement.locationLinks.Member exits with [patroni] FATAL: the etcd DCS can never reach quorum
Member exits with [patroni] FATAL: the etcd DCS can never reach quorum
global.locations exist in the GVC, so the consensus store can never elect a leader and Patroni could never promote a primary. A fresh member exits immediately rather than waiting forever; an already-initialized member logs a [patroni] WARNING and keeps running.Fix: add the missing locations to the GVC or remove them from global.locations so that the GVC has every declared location, and redeploy.Member exits with [patroni] FATAL: this member is running in location ..., which is not in the configured location list
Member exits with [patroni] FATAL: this member is running in location ..., which is not in the configured location list
global.locations does not list. Such a member would join the cluster through etcd and could even win the leader election while no proxy routes to it, so it exits unconditionally. Normally an unlisted GVC location runs nothing at all; this line only appears if a replica was placed there anyway.Fix: add that location to global.locations and upgrade, or remove it from the GVC.Log shows [patroni] WARNING: could not read the location list of GVC
Log shows [patroni] WARNING: could not read the location list of GVC
RELEASE_NAME-postgres-gvc-policy policy is missing — so it skipped the GVC location check and started anyway. The warning is informational; the member runs normally.Fix: nothing, if the policy exists and the warning was transient. If it recurs on every start, confirm the policy with cpln policy get RELEASE_NAME-postgres-gvc-policy -o yaml and that it grants view on the GVC the release is installed into.Log shows WARNING: no leader in LOCATION after 90s — bootstrapping here instead
Log shows WARNING: no leader in LOCATION after 90s — bootstrapping here instead
primaryLocation is set, but no member in that location initialized the cluster within the 90-second head start — it was slow, down, or (as the same boot check will have warned) names a location the GVC does not have. The member that logged this bootstrapped the cluster where it is. The cluster is healthy; only the primary’s placement differs from your preference.Fix: once every member is up, move the primary with patronictl switchover --candidate RELEASE_NAME-postgres-LOCATION-0 --force as shown in Managing the Cluster With patronictl. Do not change primaryLocation to force it — that restarts every member.The primary was lost but no replica was promoted
The primary was lost but no replica was promoted
maximum_lag_on_failover (32 MiB) is excluded from the election.Fix: run patronictl list and read the Lag in MB column. With two locations follow Recovering From a Lost Location; with three or more, wait for the lagging replica to catch up, or check pg_stat_replication on the primary if it never does.Members restart-loop with exitCode 0 and reason Completed
Members restart-loop with exitCode 0 and reason Completed
NOSPACE alarm — so Patroni cannot renew its leader lease and exits cleanly. The restarts look healthy because the exit code is zero.Fix: check etcd before touching the database: cpln workload get-deployments RELEASE_NAME-etcd --gvc GVC_NAME -o yaml, then the read-only etcdctl commands in the etcd Multi-Location page’s If the Backend Quota Is Already Full section.The proxy never becomes ready and its log repeats waiting for Patroni endpoints before starting haproxy
The proxy never becomes ready and its log repeats waiting for Patroni endpoints before starting haproxy
8008, naming the member it is waiting for. One that never answers — a member wedged on a missing secret, or a declared location the GVC does not have — blocks every proxy in every location. After the gate’s timeout the proxy container exits and is restarted to check again.Fix: fix the named member first (the accordions above); the proxies start on their own once every member answers.A logical backup object in the bucket is only a few bytes
A logical backup object in the bucket is only a few bytes
pg_dumpall failed — usually because the job could not reach the proxy or authenticate — and the upload pipeline still wrote an empty gzip under a normal-looking name; the job itself reported failed.Fix: read the job’s log with cpln logs '{gvc="GVC_NAME", workload="RELEASE_NAME-postgres-backup"}' --limit 50 --since 24h, fix the cause, and treat the object as not a backup.A logical backup run reports failed with no error in its log, or the wal-g-backup sidecar keeps restarting
A logical backup run reports failed with no error in its log, or the wal-g-backup sidecar keeps restarting
128Mi: a logical job dies without an error line and the wal-g-backup sidecar is killed in a loop, taking its member out of service while WAL keeps archiving with no base backup. cpln workload get-deployments RELEASE_NAME-postgres --gvc GVC_NAME -o yaml shows the restart reason.Fix: keep backup.resources.memory at the shipped 512Mi or higher.After enabling wal-g with a MinIO endpoint, members in some locations never finish starting
After enabling wal-g with a MinIO endpoint, members in some locations never finish starting
restore_command runs on every member, and the MinIO endpoint is reachable only from the location it runs in. Members elsewhere cannot fetch from the archive and are restarted indefinitely while the leader stays healthy and writes keep succeeding.Fix: point backup.minio.endpoint at a MinIO deployment present in every location or at an external S3-compatible service, or use S3 or GCS. Disabling backups heals the members.Clients get remaining connection slots are reserved through PgBouncer
Clients get remaining connection slots are reserved through PgBouncer
pgbouncer.maxDbConnections is enforced per PgBouncer replica, so the tier can open up to maxReplicas × maxDbConnections server connections — 400 at the shipped values — against the cluster’s max_connections of 100.Fix: lower pgbouncer.maxDbConnections (and defaultPoolSize) so that maxReplicas × maxDbConnections stays comfortably under 97.Writes briefly fail with cannot execute INSERT in a read-only transaction
Writes briefly fail with cannot execute INSERT in a read-only transaction
Password changed in the secret but the old one still works
Password changed in the secret but the old one still works
Important Notes
- Do not
cpln helm upgradea 1.x release onto 2.0.0. It deletes the GVC the old release created and every volume set in it. Follow Migrating From Version 1: new release, restore, then uninstall the old one. - Create the credentials secret before installing. A reference to a secret that does not exist wedges the workload in every location with no log output; see Prerequisites for the one command that shows the reason.
global.locationsmust match the GVC you install into. A declared location the GVC lacks makes a fresh member refuse to start with a named[patroni] FATALline; a GVC location you did not list runs nothing. Read the GVC’s list withcpln gvc get GVC_NAME -o yamlbefore installing.- Quorum arithmetic decides your failure tolerance. Two locations survive zero losses and promotion is manual; three give automatic failover.
- Set
primaryLocationbefore the first install if you care where the primary is. Changing it later restarts every member to move the leader; usepatronictl switchoverinstead. - Credentials are read only when the cluster first bootstraps. Rotate with
ALTER ROLEon the superuser andrepl, then update the secret and redeploy — see Rotating the Password. - Replication is asynchronous. A failover can lose the most recent transactions, bounded by the replication lag at the moment of failure.
- Every
cpln helm upgradecan restart members in every location together, including a no-op one. Expect a brief write outage; the data survives. The same applies to changingprimaryLocation,global.locationsorbackup.mode. - With
internalAccess.type: workload-list, list only your clients — this release’s own tiers are added for you. The bundled etcd tier is separate:etcd.internalAccess.type: workload-listrequires adding//gvc/GVC_NAME/workload/RELEASE_NAME-postgresby hand. - Leave etcd compaction at its defaults. An uncompacted consensus store goes read-only and takes the database’s failover with it.
- A WAL-G backup cannot be restored into the running cluster through this template. The archive is restorable outside the release; see Restoring a Backup.
cpln helm uninstalldeletes the volume sets, and the database with them, after a final snapshot kept 7 days. Your credentials secret is yours and is left alone.- Cost scales with write volume multiplied by the members outside the primary’s location. Cross-region replication traffic is billed.