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:
- Once per cluster, the
cloudnative-pgchart, which installs the CloudNativePG operator that runs every PostgreSQL cluster. - 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
restrictedPod Security Standard.
Prerequisites
- Kubernetes 1.34 or later.
- The
cloudnative-pgchart, 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: requiredrefuses 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: anyandpostgresql.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:
- Take a base backup, and check that it completed.
- Read the PostgreSQL release notes of the new major version for changes that affect your applications.
- 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(andwalStorage.size); the operator resizes each volume, which needs a StorageClass that allows expansion. A volume cannot be made smaller. - Resources.
resourcesapplies to each instance; a change restarts the instances in a rolling update. PostgreSQL’s memory settings (shared_buffersand others inpostgresql.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 |