Skip to main content

Overview

PostgreSQL Highly Available deploys a production-ready PostgreSQL cluster using Patroni for automatic leader election and failover, with etcd providing distributed consensus. An optional HAProxy workload routes all write traffic to the current primary replica, providing a stable connection endpoint regardless of which replica holds the leader role.
For production use, maintain at least 3 PostgreSQL replicas and 3 etcd replicas. etcd requires an odd number of replicas (3, 5, 7) for quorum.

What Gets Created

  • Stateful Workload — A Patroni-managed PostgreSQL cluster with configurable replica count and resources. Each replica has its own volume.
  • etcd Workload — A dedicated etcd cluster (via dependency) providing distributed consensus for Patroni leader election.
  • HAProxy Workload (optional, enabled by default) — A leader-routing proxy that always directs write traffic to the current primary replica.
  • Volume Set — Persistent storage for PostgreSQL data, with optional autoscaling.
  • Secrets — A dictionary secret with database credentials; opaque secrets for the Patroni startup script, HAProxy startup script, and WAL-G backup script (created as needed).
  • Identity & Policy — An identity bound to the workload with reveal access to all required secrets, and cloud storage access when backup is enabled.
This template does not create a GVC. You must deploy it into an existing GVC.

Installation

This template has no external prerequisites unless backup is enabled. To install, follow the instructions for your preferred method:

Configuration

The default values.yaml for this template:
replicas: 3

resources:
  minCpu: 500m
  minMemory: 1Gi
  maxCpu: 1
  maxMemory: 2Gi

image: controlplanecorporation/patroni-postgres:0.7

postgres:
  username: username
  password: password
  database: test

multiZone: false

volumeset:
  capacity: 10 # initial capacity in GiB (minimum is 10)
  autoscaling:
    enabled: false
    maxCapacity: 100 # Maximum capacity in GiB
    minFreePercentage: 10 # Trigger scaling when free space drops below this percentage
    scalingFactor: 1.2 # Multiply current capacity by this factor when scaling up

internal_access:
  type: same-gvc # options: same-gvc, same-org, workload-list
  workloads: # Note: can only be used if type is same-gvc or workload-list
    #- //gvc/GVC_NAME/workload/WORKLOAD_NAME

etcd:
  replicas: 3
  resources:
    cpu: 500m
    memory: 512Mi
  multiZone: false
  volumeset:
    capacity: 10 # initial capacity in GiB (minimum is 10)
  internal_access:
    type: same-gvc # options: same-gvc, same-org, workload-list
    workloads:
      #- //gvc/GVC_NAME/workload/WORKLOAD_NAME

proxy: # HAProxy endpoint to write to leader replica
  enabled: true
  image: haproxy:2.9
  resources:
    cpu: 100m
    memory: 128Mi
  minReplicas: 2
  maxReplicas: 2

backup:
  enabled: false
  mode: logical  # logical or wal-g
  resources:
    cpu: 100m
    memory: 128Mi

  logical:
    image: controlplanecorporation/pg-backup:17.1.0
    schedule: "0 2 * * *" # cron schedule, default is daily at 2am UTC

  walg:
    intervalSeconds: 21600 # interval in seconds between backups, default is every 6 hours

  provider: aws # Options: aws or gcp

  aws:
    bucket: pg-ha-backup-bucket
    region: us-east-1
    cloudAccountName: my-s3-cloud-account
    policyName: pg-ha-backup-policy
    prefix: postgres/backups

  gcp:
    bucket: pg-ha-backup-bucket
    cloudAccountName: my-gcs-cloud-account
    prefix: postgres/backups

Credentials

  • postgres.username — PostgreSQL superuser username. Change before deploying to production.
  • postgres.password — PostgreSQL superuser password. Change before deploying to production.
  • postgres.database — Name of the database created on first startup.
These values are only applied on first startup when the data directory is empty. Updating them after the initial deployment will have no effect on the running database. To change credentials or the database name on an existing instance, use PostgreSQL’s native commands (e.g. ALTER USER, ALTER DATABASE).

PostgreSQL Cluster

  • replicas — Number of PostgreSQL replicas. Minimum 3 recommended for production.
  • resources.minCpu / resources.minMemory — Minimum CPU and memory guaranteed per replica.
  • resources.maxCpu / resources.maxMemory — Maximum CPU and memory per replica.
  • multiZone — Spread replicas across availability zones within the location.

Storage

  • volumeset.capacity — Initial volume size in GiB (minimum 10). Each replica gets its own volume.
  • volumeset.autoscaling.enabled — Automatically expand the volume as it fills. When enabled:
    • maxCapacity — Maximum volume size in GiB.
    • minFreePercentage — Trigger a scale-up when free space drops below this percentage.
    • scalingFactor — Multiply the current capacity by this factor when scaling up.

etcd Cluster

  • etcd.replicas — Number of etcd replicas. Must be an odd number (3, 5, 7) for quorum.
  • etcd.resources.cpu / etcd.resources.memory — CPU and memory per etcd replica.
  • etcd.multiZone — Spread etcd replicas across availability zones.
  • etcd.volumeset.capacity — Initial volume size for etcd data in GiB.
  • etcd.internal_access.type — Controls which workloads can reach the etcd cluster.
In a Patroni cluster, only the leader replica accepts writes — other replicas are read-only. HAProxy automatically routes write traffic to the current leader, providing a stable connection endpoint even during failover.
  • proxy.enabled — Deploy the HAProxy leader-routing workload (default: true).
  • proxy.resources.cpu / proxy.resources.memory — CPU and memory per HAProxy replica.
  • proxy.minReplicas / proxy.maxReplicas — Replica count for the proxy workload.
HAProxy must be enabled (proxy.enabled: true) for logical backups to function correctly. WAL-G backups do not require the proxy.

Internal Access

  • internal_access.type — Controls which workloads can connect to PostgreSQL on port 5432:
TypeDescription
same-gvcAllow access from all workloads in the same GVC
same-orgAllow access from all workloads in the same organization
workload-listAllow access only from specific workloads listed in workloads

Connecting to PostgreSQL

Connect to PostgreSQL through the HAProxy workload, which always routes to the current leader:
RELEASE_NAME-postgres-ha-proxy.GVC_NAME.cpln.local:5432

Backup

Two backup modes are available. Set backup.enabled: true, choose a mode, and configure the storage provider.
ModeDescription
logicalScheduled SQL dumps via pg_dump. Portable and suitable for smaller databases. Requires HAProxy to be enabled.
wal-gContinuous WAL archiving with point-in-time recovery. Suitable for larger databases requiring minimal data loss. Runs as a sidecar container alongside PostgreSQL.
  • backup.modelogical or wal-g.
  • backup.provideraws or gcp.
  • backup.resources.cpu / backup.resources.memory — Resources allocated to the backup container.
Logical backup settings:
  • backup.logical.schedule — Cron expression for backup frequency (default: daily at 2am UTC).
WAL-G backup settings:
  • backup.walg.intervalSeconds — Interval between base backups in seconds (default: 21600, every 6 hours).

Backup Prerequisites

AWS S3

Before enabling backup with provider: aws, complete the following in your AWS account:
  1. Create an S3 bucket. Set backup.aws.bucket to the bucket name and backup.aws.region to its region.
  2. If you do not have a Cloud Account set up, refer to the docs to Create a Cloud Account. Set backup.aws.cloudAccountName to its name.
  3. Create an IAM policy with the following JSON, replacing YOUR_BUCKET_NAME:
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:GetObject",
                "s3:PutObject",
                "s3:DeleteObject",
                "s3:ListBucket",
                "s3:GetObjectVersion",
                "s3:DeleteObjectVersion"
            ],
            "Resource": [
                "arn:aws:s3:::YOUR_BUCKET_NAME",
                "arn:aws:s3:::YOUR_BUCKET_NAME/*"
            ]
        }
    ]
}
  1. Set backup.aws.policyName to the name of the policy created in step 3.
  2. Set backup.aws.prefix to the folder path where backups will be stored.

GCS

Before enabling backup with provider: gcp, complete the following in your GCP account:
  1. Create a GCS bucket. Set backup.gcp.bucket to the bucket name.
  2. If you do not have a Cloud Account set up, refer to the docs to Create a Cloud Account. Set backup.gcp.cloudAccountName to its name.
  3. Add the Storage Admin role to the GCP service account associated with the Cloud Account.
  4. Set backup.gcp.prefix to the folder path where backups will be stored.

Restoring a Backup

Logical

Run the following from a client with access to the backup bucket. Connect through the proxy workload so the restore targets the current leader. AWS S3:
export PGPASSWORD="PASSWORD"

aws s3 cp "s3://BUCKET_NAME/PREFIX/BACKUP_FILE.sql.gz" - \
  | gunzip \
  | psql \
      --host=RELEASE_NAME-postgres-ha-proxy.GVC_NAME.cpln.local \
      --port=5432 \
      --username=USERNAME \
      --dbname=postgres

unset PGPASSWORD
GCS:
export PGPASSWORD="PASSWORD"

gsutil cp "gs://BUCKET_NAME/PREFIX/BACKUP_FILE.sql.gz" - \
  | gunzip \
  | psql \
      --host=RELEASE_NAME-postgres-ha-proxy.GVC_NAME.cpln.local \
      --port=5432 \
      --username=USERNAME \
      --dbname=postgres

unset PGPASSWORD

WAL-G

WAL-G point-in-time restore requires an empty data directory. Follow these steps:
  1. Run wal-g backup-list to identify the desired backup.
  2. Stop the PostgreSQL workload.
  3. Create a new Volume Set for the restored data.
  4. Run a one-off restore workload with the new Volume Set mounted at /var/lib/postgresql/data and run:
wal-g backup-fetch /var/lib/postgresql/data/pgdata <backup_name>
  1. Re-point the PostgreSQL workload to the restored Volume Set and restart.
  2. After the restore, change backup.walg.prefix before re-enabling backups to avoid system identifier conflicts.

External References