Databases
The platform provides the CloudNativePG operator. You declare a PostgreSQL cluster as a Kubernetes resource in your own cluster, and the operator provisions it, keeps it running, handles failover between instances and rolls out version upgrades.
You do not install the operator. It is shared by the platform and watches your cluster, so a
Cluster resource is all you deploy.
Declare a PostgreSQL cluster
Section titled “Declare a PostgreSQL cluster”apiVersion: postgresql.cnpg.io/v1kind: Clustermetadata: name: postgres namespace: my-clusterspec: instances: 1 imageName: ghcr.io/cloudnative-pg/postgresql:17.5
# Data directory. storage: size: 15Gi storageClass: eu-west-fr-gra-block-nvme-ec-xfs resizeInUseVolumes: true
# Write-Ahead Log on its own volume. Optional, and recommended: # it isolates WAL growth from the data directory. walStorage: size: 5Gi storageClass: eu-west-fr-gra-block-nvme-ec-xfs resizeInUseVolumes: true
bootstrap: initdb: database: app owner: app encoding: UTF8
resources: requests: cpu: 200m memory: 256Mi limits: cpu: "1" memory: 1GistorageClass values and what they mean are listed in Volumes. Raising
storage.size later resizes the volume in place as long as resizeInUseVolumes is true.
Connect your applications
Section titled “Connect your applications”For a cluster named postgres, the operator publishes three services:
| Service | Use it for |
|---|---|
postgres-rw | reads and writes, always routed to the current primary |
postgres-ro | read-only queries, routed to the replicas |
postgres-r | read-only queries, routed to any instance |
Point your application at postgres-rw rather than at a pod. The name survives a failover, a pod
name does not.
Credentials are generated by the operator into a secret named after the cluster and the database
owner, postgres-app, which carries username, password, dbname, host, port, and ready
made connection strings under uri and jdbc-uri. Reference the keys from your deployment
instead of copying the values:
env: - name: DATABASE_URL valueFrom: secretKeyRef: name: postgres-app key: uriInclude the database in your cold backups
Section titled “Include the database in your cold backups”This is the part that is easy to get wrong, so read it even if you already know the Backups guide.
Cold backups only capture the volumes you select, through a pod annotation. The recipe on the
Backups page puts that annotation in a Deployment’s pod template, but you do not own the pod
template here: the operator builds the pods. Annotating the Cluster resource itself would not
help either, because the annotation has to end up on the pods.
CloudNativePG has a field for exactly this. Everything under spec.inheritedMetadata is copied
onto the resources the operator creates, pods included:
apiVersion: postgresql.cnpg.io/v1kind: Clustermetadata: name: postgresspec: inheritedMetadata: annotations: backup.velero.io/backup-volumes: pgdata,pg-wal # ... the rest of the specWhich volume names to list:
pgdatais the data directory. Always include it.pg-walexists only if you declaredwalStorage. Include it whenever it exists, and see the next section for why it is not optional.- Do not list
scratch-dataorshm. They are ephemeral, they hold no durable state, and they are recreated on every pod start.
What the cold backup gives you, and what it does not
Section titled “What the cold backup gives you, and what it does not”The cold backup copies the data directory of a running database at the file level. It is a
crash-consistent copy, the same kind of state PostgreSQL would find after a power loss, not a
quiesced dump. PostgreSQL is built for this: on startup it replays the write-ahead log to reach a
consistent state. That replay is the reason pg-wal belongs in the annotation. Without the WAL,
the copy of the data directory may be missing what makes it consistent.
What it covers well:
- losing a cluster, a volume, or a whole namespace, and rebuilding it in another cluster
- an infrastructure incident, since the backups live in a different data center from your volumes
What it does not cover:
- a specific point in time between two backups. You restore to the schedule hour, not to the minute before a bad migration
- retention beyond the backup’s own expiry. Cold backups are pruned when their retention lapses, so they are not an archive
- a logical mistake you notice late, such as a table dropped three weeks ago
Check a restored database
Section titled “Check a restored database”Follow the drill in Backups, then confirm the database
itself rather than the restore status. Connect to the restored cluster through its -rw service
and check three things:
-- 1. the instance replayed its log and is open for writesSELECT pg_is_in_recovery(); -- expected: f
-- 2. the tables that matter carry the volume you expectSELECT count(*) FROM your_table;
-- 3. the most recent record predates the backup, and by how muchSELECT max(created_at) FROM your_table;The third one is the useful one. A file level copy is taken while the database keeps writing, so the last records land somewhere inside the backup window rather than exactly at its start. Seeing where the data stops is what turns your retention policy into a number you can defend.