Skip to content
h8lio

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.

apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: postgres
namespace: my-cluster
spec:
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: 1Gi

storageClass values and what they mean are listed in Volumes. Raising storage.size later resizes the volume in place as long as resizeInUseVolumes is true.

For a cluster named postgres, the operator publishes three services:

ServiceUse it for
postgres-rwreads and writes, always routed to the current primary
postgres-roread-only queries, routed to the replicas
postgres-rread-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: uri

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/v1
kind: Cluster
metadata:
name: postgres
spec:
inheritedMetadata:
annotations:
backup.velero.io/backup-volumes: pgdata,pg-wal
# ... the rest of the spec

Which volume names to list:

  • pgdata is the data directory. Always include it.
  • pg-wal exists only if you declared walStorage. Include it whenever it exists, and see the next section for why it is not optional.
  • Do not list scratch-data or shm. 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

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 writes
SELECT pg_is_in_recovery(); -- expected: f
-- 2. the tables that matter carry the volume you expect
SELECT count(*) FROM your_table;
-- 3. the most recent record predates the backup, and by how much
SELECT 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.