CloudNativePG Backup and Point-in-Time Recovery on Kubernetes

Quick answer
Cluster-level backup tools like Velero snapshot Kubernetes objects and volumes — they have no concept of a Postgres transaction. CloudNativePG's backup is Postgres-native: continuous WAL archiving means you can restore to an exact transaction, not just the last snapshot. Here's how the Backup, ScheduledBackup, and recovery bootstrap actually work.
7 min read · Kubernetes
CloudNativePG (CNPG) is a Kubernetes operator for PostgreSQL, originally built by EnterpriseDB and accepted into the CNCF Sandbox in January 2025 — it has since applied to progress to Incubating maturity. It runs Postgres as a Cluster custom resource, with the primary and replicas as pods it manages through streaming replication and automated failover. What makes its backup story different from a generic Kubernetes backup tool is that it's Postgres-native: it understands write-ahead logs, not just volume snapshots.
That distinction matters more than it sounds. A snapshot backup captures a point in time and nothing between snapshots. CloudNativePG's continuous WAL archiving captures every committed transaction, which is what makes recovering to an exact transaction — not just "the last time we happened to take a backup" — possible at all.
Backup: On-Demand and Scheduled
A Backup resource triggers an immediate base backup of the cluster, written to an S3-compatible object store via the Barman Cloud integration:
1apiVersion: postgresql.cnpg.io/v1
2kind: Backup
3metadata:
4 name: payments-db-backup-manual
5 namespace: databases
6spec:
7 cluster:
8 name: payments-dbFor routine backups, a ScheduledBackup runs on a cron expression instead of requiring a manual trigger:
1apiVersion: postgresql.cnpg.io/v1
2kind: ScheduledBackup
3metadata:
4 name: payments-db-nightly
5 namespace: databases
6spec:
7 schedule: "0 2 * * *" # 2 AM daily, standard cron syntax
8 backupOwnerReference: self
9 cluster:
10 name: payments-dbThe object store connection is configured once, on the Cluster resource itself, and both Backup and ScheduledBackup reuse it:
1apiVersion: postgresql.cnpg.io/v1
2kind: Cluster
3metadata:
4 name: payments-db
5 namespace: databases
6spec:
7 instances: 3
8 storage:
9 size: 50Gi
10 storageClass: gp3
11
12 backup:
13 barmanObjectStore:
14 destinationPath: "s3://payments-db-backups/"
15 s3Credentials:
16 accessKeyId:
17 name: backup-s3-creds
18 key: ACCESS_KEY_ID
19 secretAccessKey:
20 name: backup-s3-creds
21 key: SECRET_ACCESS_KEY
22 wal:
23 compression: gzip
24 retentionPolicy: "30d"The wal section is what makes this continuous rather than periodic — CNPG ships each completed WAL segment to the object store as soon as Postgres finishes writing it, independent of when the next base Backup runs.
A note on API currency: the native spec.backup.barmanObjectStore configuration shown above still works as of CNPG 1.30, but it's been deprecated since 1.26 in favor of the CNPG-I barman-cloud plugin, with removal now scheduled for 1.31. The mechanics — continuous WAL archiving, Backup/ScheduledBackup CRDs, PITR targets — are unchanged; only where you declare the object store connection moves, to a separate ObjectStore resource referenced via plugin.name: barman-cloud.cloudnative-pg.io. New deployments should start on the plugin path; check the migration guide if you're still on native config.
Point-in-Time Recovery
Recovery is not an in-place operation on the existing Cluster — you bootstrap a new Cluster resource that recovers from the object store, specifying exactly how far to replay:
1apiVersion: postgresql.cnpg.io/v1
2kind: Cluster
3metadata:
4 name: payments-db-restored
5 namespace: databases
6spec:
7 instances: 3
8 storage:
9 size: 50Gi
10 storageClass: gp3
11
12 bootstrap:
13 recovery:
14 source: payments-db-backup-source
15 recoveryTarget:
16 targetTime: "2026-09-20 14:30:00.000000+00" # Restore to this exact timestamp
17
18 externalClusters:
19 - name: payments-db-backup-source
20 barmanObjectStore:
21 destinationPath: "s3://payments-db-backups/"
22 s3Credentials:
23 accessKeyId:
24 name: backup-s3-creds
25 key: ACCESS_KEY_ID
26 secretAccessKey:
27 name: backup-s3-creds
28 key: SECRET_ACCESS_KEYCNPG restores the most recent base backup taken before targetTime, then replays WAL segments forward from that point until it reaches the exact timestamp — the database ends up in the state it was in at that moment, including every committed transaction up to it and none after.
recoveryTarget isn't limited to a timestamp. It also accepts:
targetXID— recover to immediately after a specific transaction commits (the precise option when you know exactly which write caused the problem)targetLSN— recover to a specific Log Sequence NumbertargetName— recover to a named restore point created in advance withSELECT pg_create_restore_point('before-migration')
The named restore point is the practical one to reach for before a risky schema migration: create it, run the migration, and if it goes wrong, recover with targetName pointed at the label you just created rather than guessing at a timestamp after the fact.
Kubernetes Production Readiness Checklist
The pre-launch checks we run before calling a cluster production-ready — probes, resources, RBAC, upgrades, and backups. Plain Markdown you can commit to your repo.
Free. Instant download. You'll also get the occasional deep-dive from the newsletter — unsubscribe anytime.
Why Not Just Use Velero?
This site already covers Velero for Kubernetes backup and disaster recovery — the natural question is why CNPG needs its own backup mechanism when a cluster-wide tool already exists.
Velero operates at the Kubernetes and infrastructure level: it snapshots object manifests and PersistentVolumes (via the underlying cloud provider's snapshot API or restic/Kopia for volume data). That's the right tool for "restore this entire namespace" or "recover from an accidentally deleted cluster." But Velero has no concept of Postgres transactional consistency — a volume snapshot taken mid-write captures whatever bytes happened to be on disk at that instant. Postgres itself has to replay its own WAL on startup to reach a consistent state from a crash-consistent snapshot, and there's no way to tell Velero "restore to 2:30 PM" in transaction terms — only "restore to whichever snapshot was closest to 2:30 PM."
CloudNativePG's backup is Postgres-native from the start: it uses Postgres's own base-backup and WAL-archiving mechanisms, so recovery targets are expressed in Postgres's own terms (transaction IDs, LSNs, timestamps) and are exact, not "closest available snapshot." The trade-off is scope — CNPG only backs up the Postgres cluster it manages, not your other Kubernetes objects, ConfigMaps, or unrelated PVCs. Most production setups run both: Velero for whole-cluster/namespace disaster recovery, CNPG's native backup for the specific Postgres instances where transaction-level recovery actually matters.
Frequently Asked Questions
How current can WAL archiving keep my RPO?
In practice, seconds — CNPG ships each WAL segment as soon as Postgres completes it, not on a schedule. Your actual recovery point objective is bounded by how much uncommitted data could exist in a WAL segment that hasn't finished writing yet at the moment of failure, not by how often you run a base Backup.
Does a large retention policy mean I need a proportionally huge S3 bucket?
Retention keeps every WAL segment and base backup needed to recover to any point within the window, so storage grows with both write volume and retention length, not just database size. A high-write-throughput database with a 30-day retention policy can accumulate significantly more WAL data than its own on-disk size — budget for this before setting retention, and use the wal.compression and data.compression options to reduce it.
Can I test a recovery without touching the production cluster?
Yes, and you should before you actually need it. Bootstrap the recovery Cluster resource under a different name (as shown above) pointing at the same backup source — it creates entirely new pods and PVCs, leaving the original payments-db cluster untouched and serving traffic throughout the test.
What happens if the S3 bucket is temporarily unreachable during a backup?
The Backup resource's status reflects the failure (phase: failed), and CNPG will retry on the next scheduled run for a ScheduledBackup. WAL archiving similarly buffers and retries — Postgres won't recycle a WAL segment until it's confirmed archived, so a transient object-store outage causes a backlog rather than data loss, provided local disk has room to hold the backlog until connectivity recovers.
For the StatefulSet mechanics and general PostgreSQL-on-Kubernetes operational patterns CloudNativePG builds on, see Kubernetes StatefulSets: Running Stateful Workloads in Production. For whole-cluster backup and disaster recovery beyond a single database, see Velero: Kubernetes Backup and Disaster Recovery.
Running Postgres on Kubernetes and need a recovery strategy that survives an audit? Talk to us at Coding Protocols — we help platform teams design backup and PITR setups they've actually tested, not just configured.
Official References
- CloudNativePG documentation: Backup and Recovery — Backup, ScheduledBackup, and recovery bootstrap reference
- CloudNativePG on CNCF — project maturity and governance
Was this article helpful?
Be the first to rate this article
Related Topics
Found this useful? Share it.


