Bahriya Tersane

Documentation / postgresql

PostgreSQL

Creates a PostgreSQL cluster: a single server, or a primary with streaming replicas that fails over automatically.

PostgreSQL is installed in two steps:

  1. Once per cluster, the cloudnative-pg chart, which installs the CloudNativePG operator that runs every PostgreSQL cluster.
  2. Then any number of times, this chart: each release is one PostgreSQL cluster, in any namespace.

The operator keeps each cluster as declared: it creates the instances, keeps every replica streaming from the primary, promotes a replica when the primary fails, switches the primary over to an updated replica before restarting it in a rolling update, and rebuilds an instance whose node or volume is lost.

The chart provides:

  • one instance or several, with failover between them;
  • the image from the Mamluk registry, pinned by digest, chosen by PostgreSQL major version;
  • an application database and its owner, further databases and roles, all declared in values;
  • TLS on every connection, with a certificate authority per cluster or certificates of your own;
  • base backups and continuous WAL archiving to S3-compatible storage, and recovery to any moment within the retention period;
  • a NetworkPolicy, Prometheus metrics, optional access from outside the cluster through Kong, and optional encryption of the volumes;
  • every Pod meeting the restricted Pod Security Standard.

Prerequisites

  • Kubernetes 1.34 or later.
  • The cloudnative-pg chart, installed once in the cluster. Its operator copies its pull secret into this release’s namespace, so no pull secret needs creating here.
  • A StorageClass for the volumes: the cluster’s default, or one named in storage.storageClass.
  • For backups, a bucket in S3-compatible storage and a key that can read and write it.

Installation

A cluster of three instances, the default:

helm install orders tersane/postgresql \
  --namespace orders --create-namespace \
  --version <chart version>

A single server, with no failover:

helm install orders tersane/postgresql --namespace orders \
  --set instances=1

The cluster is named after the release, <release>-postgresql (here orders-postgresql), or the release name alone if it already contains postgresql. Check that it is ready:

kubectl --namespace orders get cluster orders-postgresql

helm install --wait returns before the instances are ready, because the operator creates them after the release is installed. Wait for the cluster’s Ready condition instead:

kubectl --namespace orders wait --for=condition=Ready \
  cluster/orders-postgresql --timeout=10m

Connecting

The operator creates three Services:

Service Reaches
<cluster>-rw The primary, for reads and writes
<cluster>-ro The replicas only, for reads
<cluster>-r Any instance, for reads

Applications connect to <cluster>-rw on port 5432. When the primary changes, the Service follows it; connections open at that moment are closed and must be made again.

The application database’s owner and password are in the Secret <cluster>-app, with the keys username, password, dbname, host, port, uri and jdbc-uri:

env:
  - name: DATABASE_URL
    valueFrom:
      secretKeyRef:
        name: orders-postgresql-app
        key: uri

TLS

Every instance offers TLS. By default the operator creates a certificate authority for the cluster and issues the server certificate from it. Clients that verify the server read the authority’s certificate from the Secret <cluster>-ca, key ca.crt, and connect with sslmode=verify-full, using a host name the certificate names (the three Services, in their short and namespaced forms).

Clients may connect without TLS unless postgresql.pgHba requires it:

postgresql:
  pgHba:
    - hostssl all all 0.0.0.0/0 scram-sha-256
    - host all all 0.0.0.0/0 reject

To use certificates of your own, for example from cert-manager, name their Secrets in tls.serverTLSSecret and tls.serverCASecret (and, for client certificate authentication, tls.clientCASecret and tls.replicationTLSSecret), as CloudNativePG’s certificate documentation describes.

The superuser

The postgres superuser has no password and cannot connect over the network unless superuser.enabled is true, in which case its password is in <cluster>-superuser. Administration does not need it:

kubectl --namespace orders exec -it orders-postgresql-1 -c postgres -- psql

Roles and databases

The application database (bootstrap.database, app by default) and its owner (bootstrap.owner) are created with the cluster. Further roles and databases are declared, and the operator creates them and keeps them as declared:

roles:
  - name: reporting
    login: true
    inRoles:
      - pg_read_all_data
databases:
  - name: reports
    owner: reporting
    extensions:
      - name: vector

A role with login: true is given a password, generated once, in the Secret <cluster>-role-<name> (keys username and password); an upgrade keeps it. To set the password yourself, create a kubernetes.io/basic-auth Secret and name it in the role’s passwordSecret. When the Secret changes, the operator changes the password.

Each role accepts the fields of CloudNativePG’s role configuration, and each database those of its Database resource. A role or database removed from the list is left in place; to drop it, keep it listed with ensure: absent.

High availability

With more than one instance, every replica streams the primary’s WAL. The operator watches every instance; when the primary fails, it promotes the most advanced replica and points <cluster>-rw at it, and the former primary rejoins as a replica when it returns.

  • Placement. The instances are spread across nodes by a preferred anti-affinity on affinity.topologyKey. affinity.podAntiAffinityType: required refuses to place two instances on one node; with fewer nodes than instances, the extra instances then wait unscheduled.
  • Disruption. The operator keeps a PodDisruptionBudget on the cluster, so that draining nodes never stops the primary and every replica together.
  • Synchronous replication. By default replication is asynchronous: a commit returns once the primary has it, and a failover can lose the last transactions not yet replicated. To make each commit wait until a replica has it too, set postgresql.synchronous.method: any and postgresql.synchronous.number: 1. Writes then stop if no replica is available.
  • Rolling updates. A change that restarts the instances restarts the replicas first, one at a time, then switches the primary over to an updated replica (primaryUpdateMethod: switchover), so the primary is not restarted in place.

Backups

Backups are taken by CloudNativePG’s Barman Cloud plugin, which the cloudnative-pg chart installs. A sidecar in each instance archives every WAL segment to object storage as it is completed, and a base backup is taken on backup.schedule, from a replica where there is one. Together they allow recovery to any moment within backup.retentionPolicy.

backup:
  enabled: true
  destinationPath: s3://my-bucket/postgresql
  endpointURL: https://s3.example.com      # empty for Amazon S3
  existingSecret: orders-backup-credentials # keys ACCESS_KEY_ID, ACCESS_SECRET_KEY
  schedule: "0 0 2 * * *"                   # 02:00 every day
  retentionPolicy: 30d

The key may be given in backup.accessKeyId and backup.secretAccessKey instead of a Secret. The schedule has six fields, seconds first. A first base backup is taken as soon as the cluster is ready.

Each cluster writes under <destinationPath>/<serverName>/, where serverName is the cluster’s name unless backup.serverName is set. Check the backups:

kubectl --namespace orders get backups

For storage served under a private certificate authority, put the authority’s certificate in a Secret and name it in backup.endpointCA.secretName.

Recovery

A cluster can be created from the backups of another, or of an earlier cluster of the same name: the latest base backup before the target is restored, and WAL is replayed up to the target. Recovery happens only when a cluster is created, so it is a new release (or a release installed again after its cluster was removed):

recovery:
  enabled: true
  destinationPath: s3://my-bucket/postgresql
  serverName: orders-postgresql             # the folder the backup was written to
  endpointURL: https://s3.example.com
  existingSecret: orders-backup-credentials
  targetTime: "2026-11-03T14:30:00Z"        # empty: to the end of the archive
backup:
  enabled: true
  destinationPath: s3://my-bucket/postgresql
  existingSecret: orders-backup-credentials
  serverName: orders-postgresql-restored    # not the folder recovered from

If recovery names no credentials, those of backup are used. A recovered cluster that also takes backups to the same destinationPath must write under another serverName: the plugin refuses to archive into a folder that holds another cluster’s WAL, and the chart refuses such a release.

The operator sets the application database owner’s password from the recovered cluster’s own <cluster>-app Secret, so applications connect with the new cluster’s credentials, not those of the cluster that was backed up. bootstrap.existingSecret names a Secret of your own instead.

PostgreSQL major versions

postgresql.major selects the major version, and postgresql.images holds the image of each major version the chart offers. Changing postgresql.major on a running cluster upgrades it in place to the new major version:

  1. Take a base backup, and check that it completed.
  2. Read the PostgreSQL release notes of the new major version for changes that affect your applications.
  3. Upgrade the release with the new postgresql.major.

The operator stops every instance, upgrades the primary’s data with pg_upgrade, starts it, and then recreates each replica from the primary. The database is unavailable from the first step until the primary starts. An upgrade cannot be undone; to return to the earlier major version, recover from the backup into a new cluster.

Scaling and storage

  • Instances. Change instances; the operator adds or removes replicas. Going from one instance to more adds failover; going to one removes it.
  • Volume size. Increase storage.size (and walStorage.size); the operator resizes each volume, which needs a StorageClass that allows expansion. A volume cannot be made smaller.
  • Resources. resources applies to each instance; a change restarts the instances in a rolling update. PostgreSQL’s memory settings (shared_buffers and others in postgresql.parameters) should fit within the memory limit.

Network policy

With networkPolicy.enabled (the default), the instances admit:

  • connections to port 5432 from networkPolicy.postgresql.from, every source when it is empty;
  • metrics scrapes on port 9187 from networkPolicy.metrics.from, every source when it is empty;
  • always, the cluster’s own Pods, which replicate and join on 5432 and 8000, and the operator, from networkPolicy.operatorNamespace, on 8000 and 5432;
  • with external access, the ingress controller’s namespace on 5432.

To admit only the applications in the cluster’s namespace:

networkPolicy:
  postgresql:
    from:
      - podSelector: {}

Outgoing traffic is not restricted: the instances reach object storage for backups.

External access

To reach the primary from outside the Kubernetes cluster through a Kong ingress controller, publish it on one of Kong’s TCP ports:

exposure:
  external:
    enabled: true
    port: 15432
    ipAllowlist:
      - 203.0.113.0/24

Kong must already listen on that port as a TCP stream listener. PostgreSQL negotiates TLS inside its own protocol, so Kong forwards the connection unchanged, routed by port and not by host name, and the client and PostgreSQL negotiate TLS end to end. Clients connect to Kong’s address on the port, with sslmode=verify-full only if the server certificate names the host name they use (see TLS). An IP allow-list is required.

Encryption at rest

On Longhorn storage, the chart can create a StorageClass of the release’s own that encrypts each volume with LUKS, under a passphrase kept in a Secret in the release’s namespace:

encryption:
  volume:
    enabled: true
storage:
  storageClass: orders-orders-postgresql-encrypted   # <namespace>-<cluster>-encrypted

The chart refuses the release unless storage.storageClass (and walStorage.storageClass, when enabled) names the class. Decide when the cluster is created: an existing volume is not re-encrypted. The passphrase is generated at install and kept; one given in encryption.volume.passphrase that differs from the one in the namespace is refused, because the existing volumes would no longer unlock. Keep a copy of the passphrase with your backups of Kubernetes Secrets.

Backups in object storage are encrypted by the storage service, not by this setting.

Metrics

Each instance serves Prometheus metrics on port 9187, at /metrics, and carries prometheus.io/scrape and prometheus.io/port annotations for collectors that discover endpoints by annotation (metrics.podAnnotations).

Pod Security

Every Pod of the cluster, the jobs that create and join instances and the helm test Pod meet the Kubernetes restricted Pod Security Standard. Each runs as a user other than root, cannot escalate its privileges, drops all Linux capabilities and runs under the container runtime’s default seccomp profile. Install it in a namespace that enforces restricted.

Testing

helm test orders --namespace orders

The test connects to the primary over TLS as the application database’s owner and runs a query.

Uninstalling

helm uninstall orders --namespace orders

Uninstalling deletes the cluster, its instances and its volumes, with the data. Backups in object storage are kept; recover from them into a new release if needed. The role password Secrets and the volume encryption Secret are deleted with the release.

Parameters

PostgreSQL

Parameter Description Default
postgresql.major The PostgreSQL major version; a key of postgresql.images 18
postgresql.images The image of each major version, by digest the release’s
postgresql.parameters postgresql.conf settings {}
postgresql.sharedPreloadLibraries Libraries loaded at server start []
postgresql.pgHba Lines added to pg_hba.conf []
postgresql.synchronous Synchronous replication (method, number) {} (asynchronous)
instances Instances: 1 is a single server 3
imagePullPolicy Image pull policy IfNotPresent
primaryUpdateMethod switchover or restart switchover

Storage, resources and scheduling

Parameter Description Default
storage.size Data volume size 10Gi
storage.storageClass Data volume StorageClass the default class
walStorage.enabled A separate WAL volume false
walStorage.size WAL volume size 5Gi
walStorage.storageClass WAL volume StorageClass the default class
resources Requests and limits of each instance 250m/1Gi, limits 1000m/1Gi
affinity.topologyKey Topology the instances are spread across kubernetes.io/hostname
affinity.podAntiAffinityType preferred or required preferred
affinity.nodeSelector Node selector of the instances {}
affinity.tolerations Tolerations of the instances []
priorityClassName Priority class of the instances ""

Databases and roles

Parameter Description Default
bootstrap.database The application database app
bootstrap.owner Its owner app
bootstrap.existingSecret A kubernetes.io/basic-auth Secret with the owner’s password generated
bootstrap.postInitApplicationSQL SQL run once in the application database at creation []
superuser.enabled A password for postgres false
roles Roles kept as declared []
databases Databases kept as declared []

TLS

Parameter Description Default
tls.serverTLSSecret Server certificate and key issued by the operator
tls.serverCASecret CA of the server certificate the operator’s
tls.clientCASecret CA of client certificates the operator’s
tls.replicationTLSSecret Client certificate of the replication user the operator’s

Backups

Parameter Description Default
backup.enabled Base backups and WAL archiving false
backup.destinationPath s3://<bucket>/<path> ""
backup.endpointURL S3 endpoint other than Amazon S3 ""
backup.serverName Folder this cluster writes under the cluster’s name
backup.accessKeyId / backup.secretAccessKey The access key ""
backup.existingSecret A Secret with ACCESS_KEY_ID and ACCESS_SECRET_KEY ""
backup.endpointCA.secretName / .key CA certificate of the storage "" / ca.crt
backup.schedule Base backup schedule, six fields 0 0 2 * * *
backup.retentionPolicy Retention of backups and WAL 30d
backup.target prefer-standby or primary prefer-standby
backup.wal.compression / .maxParallel WAL compression and parallel uploads gzip / 2
backup.data.compression / .jobs Base backup compression and parallel uploads gzip / 2
backup.sidecarResources Requests and limits of the backup sidecar 50m/128Mi, limits 500m/512Mi

Recovery

Parameter Description Default
recovery.enabled Create the cluster from a backup false
recovery.destinationPath Where the backup was written ""
recovery.serverName The folder it was written to ""
recovery.endpointURL S3 endpoint ""
recovery.accessKeyId / .secretAccessKey / .existingSecret Credentials; empty uses those of backup ""
recovery.endpointCA.secretName / .key CA certificate of the storage "" / ca.crt
recovery.targetTime Moment to recover to, RFC 3339 "" (end of the archive)

Network, metrics and encryption

Parameter Description Default
networkPolicy.enabled Create the NetworkPolicy true
networkPolicy.operatorNamespace The operator’s namespace cnpg-system
networkPolicy.postgresql.from Sources admitted to 5432 [] (every source)
networkPolicy.metrics.from Sources admitted to 9187 [] (every source)
exposure.external.enabled Publish the primary through Kong false
exposure.external.port Kong’s TCP port 0
exposure.external.ipAllowlist Client addresses admitted, as CIDRs []
exposure.external.ingressClassName Ingress class kong
exposure.external.ingressNamespace The ingress controller’s namespace kong
exposure.external.annotations Extra TCPIngress annotations {}
metrics.podAnnotations Scrape annotations on the instances true
encryption.volume.enabled An encrypted Longhorn StorageClass false
encryption.volume.passphrase LUKS passphrase generated
encryption.volume.existingSecret A Secret with the passphrase ""
encryption.volume.storageClass.* reclaimPolicy, numberOfReplicas, dataLocality Delete, 3, disabled
test.resources Requests and limits of the helm test Pod 10m/32Mi, limits 100m/64Mi