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 — provisioning, replication, failover, backups — from one short manifest. This guide goes from nothing to a backed-up database your app can use. The steps work on any conformant cluster; we use a paasbox cluster as the example.
What you’ll need
Section titled “What you’ll need”- A cluster and its kubeconfig — the quickstart gets you one in ten minutes.
kubectlinstalled locally.- For the backup section: an S3-compatible bucket and its access keys — Hetzner Object Storage works well here (it lives in the same locations as your cluster and lands on your own Hetzner bill), as does any other S3-compatible store.
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 current). From
here on, Postgres clusters are just Kubernetes resources.
2. A single-instance database (dev, staging)
Section titled “2. A single-instance database (dev, staging)”Save as pg.yaml — this is the exact shape 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=300sThe 2 Gi volume is provisioned as a Hetzner volume in your project — it appears on your Hetzner
bill, resizes online when you edit storage.size, and persists through
hibernation, so your data survives a sleeping cluster.
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 rotates and owns 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 doesn’t change. Remove the enablePDB: false line (the default PodDisruptionBudget
now protects you during node drains), and make sure the cluster has at least three schedulable
workers — see scale & autoscale workloads.
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 — worth rehearsing once before you need it for real.
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 checklist that used to justify a managed database bill. What remains yours is the semantics: schema migrations, data quality, and testing the restore path. (The platform backs up your cluster’s control plane; your database content is exactly the kind of application data that’s yours to protect — 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)