Run Postgres with CloudNativePG
Most real applications need Postgres, and running it well on Kubernetes used to be the argument against running it on Kubernetes at all. CloudNativePG (CNPG) changed that: a CNCF operator that manages PostgreSQL clusters declaratively, with provisioning, replication, failover and backups defined in one short manifest. This guide goes from nothing to a backed-up database your app can use. The steps work on any conformant cluster, a PaaSbox cluster among them.
What you’ll need
Section titled “What you’ll need”- Any Kubernetes cluster and its kubeconfig.
kubectlinstalled locally.- For the backup section: an S3-compatible bucket and its access keys, for example at Hetzner Object Storage, which bills your own Hetzner account.
On a PaaSbox cluster, check one thing first. If the cluster runs the PaaSbox Platform add-on (built in a lab, not published yet) with the profile saas-http01 or saas, CloudNativePG is installed already: skip step 1
and do not install the operator a second time. An app on the platform asks for its database in its
SaaSApplication (data.postgres), with backups to S3; see Learn step 2,
Deploy your first app.
1. Install the operator (once per cluster)
Section titled “1. Install the operator (once per cluster)”kubectl apply --server-side -f \ https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.30/releases/cnpg-1.30.0.yamlThis installs the operator into cnpg-system (release 1.30 as of 2026-07-09; check
CNPG releases for the current one).
From here on, Postgres clusters are Kubernetes resources.
2. A single-instance database
Section titled “2. A single-instance database”Save as pg.yaml. This is the configuration a small app starts with:
apiVersion: postgresql.cnpg.io/v1kind: Clustermetadata: name: pg namespace: myappspec: instances: 1 # A zero-eviction PDB would block node drains and protects nothing with a # single replica. Re-enable (the default) when instances > 1. enablePDB: false bootstrap: initdb: database: myapp owner: myapp storage: size: 2Gi resources: requests: cpu: 100m memory: 256Mi limits: memory: 512Mikubectl create namespace myappkubectl apply -f pg.yamlkubectl -n myapp wait --for=condition=Ready cluster/pg --timeout=300sWhere the 2 Gi volume lives depends on the cluster’s default storage class.
- On a PaaSbox cluster the default class is
local-lvm-thin: the volume is on the server’s own disk, at local disk speed and at no extra cost. It survives a restart, a reboot and an image update in place, not a replaced server. - For a volume that outlives the pods and the server, switch on the Hetzner volumes add-on
(
hcloud-csi, see Choose add-ons) and addstorageClass: hcloud-volumesunderstorage. The volume is then a Hetzner volume in your project, at least 10 GB, on your Hetzner bill. Deleting the PaaSbox cluster deletes it too. On a cluster whose default class is Hetzner’s volume driver, a claim gets such a volume without naming the class.
3. Connect your application
Section titled “3. Connect your application”CNPG generates everything your app needs. For a cluster named pg, you get Services pg-rw
(primary, read-write), pg-ro (replicas, read-only), and a Secret pg-app containing ready-made
credentials, including a uri key of the form
postgresql://myapp:…@pg-rw.myapp:5432/myapp. Most frameworks consume that directly:
env: - name: DATABASE_URL valueFrom: secretKeyRef: name: pg-app key: uriNo password handling, no manual Secret creation. The operator owns and rotates the credentials.
4. High availability: three instances
Section titled “4. High availability: three instances”For production, change one number:
spec: instances: 3CNPG runs one primary and two streaming replicas, spreads them across nodes, and fails over
automatically if the primary’s node dies. The pg-rw Service always points at the current
primary, so your app does not change. Remove the enablePDB: false line (the default
PodDisruptionBudget now protects you during node drains), and make sure the cluster has at least
three nodes that can run them.
A PaaSbox cluster has one node. Three instances there would share one server: failover would cover a crashed Postgres process, not a lost server, and the memory would triple. On a single node, keep one instance and rely on the backups in step 5.
5. Backups to S3-compatible storage
Section titled “5. Backups to S3-compatible storage”Volumes protect against pod restarts, not against DROP TABLE. CNPG does continuous WAL
archiving plus scheduled base backups to object storage. Create a Secret with your bucket
credentials, then add the backup section:
kubectl -n myapp create secret generic pg-backup-creds \ --from-literal=ACCESS_KEY_ID=<your-access-key> \ --from-literal=ACCESS_SECRET_KEY=<your-secret-key>spec: backup: barmanObjectStore: destinationPath: s3://my-backup-bucket/pg endpointURL: https://nbg1.your-objectstorage.com # your S3-compatible endpoint s3Credentials: accessKeyId: name: pg-backup-creds key: ACCESS_KEY_ID secretAccessKey: name: pg-backup-creds key: ACCESS_SECRET_KEY retentionPolicy: 30dThen schedule a nightly base backup:
apiVersion: postgresql.cnpg.io/v1kind: ScheduledBackupmetadata: name: pg-nightly namespace: myappspec: cluster: name: pg schedule: "0 0 3 * * *" # 03:00 daily (six-field cron, seconds first)With WAL archiving running continuously between base backups, you get point-in-time recovery:
a new Cluster manifest with a bootstrap.recovery section restores to any moment inside your
retention window. Rehearse it once before you need it.
Where this leaves you
Section titled “Where this leaves you”A declarative Postgres with automated failover, credentials your app consumes from a Secret, and off-cluster backups with point-in-time recovery: the list that used to justify a managed database bill. What remains yours is the content: schema migrations, data quality, and testing the restore path. (A snapshot of a PaaSbox cluster holds the cluster’s state, its Kubernetes objects, not your database’s content. That is application data, and protecting it is your job. This setup is how.)
To see it inside a full application, the deploy a Django SaaS guide uses this exact CNPG pattern as its database layer.
Clean up
Section titled “Clean up”kubectl delete namespace myapp # removes the cluster and its volumes (backups in the bucket remain)