A nightly dump cannot answer the question that actually gets asked after an incident, which is to put the database back the way it was at 10.42, just before the migration ran. Continuous archiving can. This is an operator based setup on Kubernetes with base backups and write ahead log shipped to object storage, plus recovery to a named timestamp. It also flags the configuration change that makes most copied CloudNativePG YAML out of date.
The question that comes up after a bad deploy is never "do we have a backup". It is "can you put it back the way it was at 10.42, just before the migration ran". A nightly dump answers that with a shrug and up to twenty four hours of lost writes. Continuous archiving answers it with a timestamp.
The mechanism is a periodic base backup plus every write ahead log segment shipped to object storage as it is closed. Recovery restores the base backup and replays the log up to the second you name. Your recovery point objective drops from a day to the last archived segment.
Pick the operator, then check which era of its configuration you are in
CloudNativePG is the operator most teams settle on for Postgres on Kubernetes, and there is one thing to know before copying any manifest you find. Native backup and recovery is being progressively phased out of the core operator and moved into official CNPG-I plugins. The in-tree barmanObjectStore method is deprecated starting with v1.26 in favour of the Barman Cloud Plugin, and spec.backup.retentionPolicy on the Cluster is deprecated as well.
Both still work for backward compatibility, which is exactly why so much published YAML is quietly out of date. Write new clusters against the plugin.
Install the plugin
The plugin needs CloudNativePG 1.26 or newer and cert-manager already installed, and it is deployed into the operator's namespace, typically cnpg-system.
kubectl apply -f \
https://github.com/cloudnative-pg/plugin-barman-cloud/releases/download/v0.14.0/manifest.yaml
Check the plugin's releases page for the current version rather than pinning to that one forever.
Describe the bucket once
The plugin introduces an ObjectStore resource, so bucket details and credentials are declared in one place and referenced by any cluster that needs them.
# k8s/pg-objectstore.yaml
apiVersion: barmancloud.cnpg.io/v1
kind: ObjectStore
metadata:
name: pg-backups
spec:
configuration:
destinationPath: s3://backups/
endpointURL: http://minio:9000
s3Credentials:
accessKeyId:
name: minio
key: ACCESS_KEY_ID
secretAccessKey:
name: minio
key: ACCESS_SECRET_KEY
wal:
compression: gzip
The credential blocks point at a Kubernetes Secret, so the Secret name and both keys have to match what you create.
kubectl create secret generic minio \
--from-literal=ACCESS_KEY_ID=REPLACE_ME \
--from-literal=ACCESS_SECRET_KEY=REPLACE_ME
Use a bucket in a different account or project from the one running the cluster, with credentials that cannot delete objects. A single set of leaked keys that can both drop the database and empty the backup bucket leaves you with one copy, not two.
Point the cluster at it
The Cluster references the store through spec.plugins, and isWALArchiver is the flag that turns on continuous archiving rather than only base backups.
# k8s/pg-cluster.yaml
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: pg-main
spec:
instances: 3
storage:
size: 50Gi
plugins:
- name: barman-cloud.cloudnative-pg.io
isWALArchiver: true
parameters:
barmanObjectName: pg-backups
Retention is configured on the ObjectStore rather than on the Cluster now that the in-tree retention field is deprecated. Read the plugin's own API reference for the current field names instead of carrying an old spec.backup.retentionPolicy block across.
Base backups on a schedule
An on-demand backup is a Backup object with the plugin named in pluginConfiguration.
# k8s/pg-backup-now.yaml
apiVersion: postgresql.cnpg.io/v1
kind: Backup
metadata:
name: backup-example
spec:
cluster:
name: pg-main
method: plugin
pluginConfiguration:
name: barman-cloud.cloudnative-pg.io
Or imperatively, if the cnpg kubectl plugin is installed:
kubectl cnpg backup -n default pg-main \
--method=plugin \
--plugin-name=barman-cloud.cloudnative-pg.io
The recurring version is a ScheduledBackup, and its schedule field is the one that trips people up. It takes a six field cron expression that includes seconds as the first entry, which is not the format Kubernetes CronJob uses. The expression below runs at 02:00:00 daily, not at two seconds past midnight.
# k8s/pg-scheduled-backup.yaml
apiVersion: postgresql.cnpg.io/v1
kind: ScheduledBackup
metadata:
name: pg-main-nightly
spec:
schedule: "0 0 2 * * *"
backupOwnerReference: self
cluster:
name: pg-main
method: plugin
pluginConfiguration:
name: barman-cloud.cloudnative-pg.io
By default CloudNativePG takes the backup from the most up to date replica and falls back to the primary if no replica is available, which keeps the base backup off the instance serving your traffic.
Watch the archive, not only the backup job
A base backup that succeeded last night says nothing about whether the write ahead log has been shipping since. When archiving fails, Postgres keeps retrying periodically until it succeeds, and in the meantime the pg_wal directory continues to fill with WAL segment files. If the filesystem containing pg_wal fills up, Postgres does a PANIC shutdown. No committed transactions are lost, but the database stays offline until space is freed, so a broken archive eventually becomes an outage on its own schedule.
Two signals are worth an alert. Free space on the data volume, and the age of the newest object in the backup bucket. If the newest archived segment is older than your archive timeout, your recovery point has quietly stopped moving forward while every pod stays Running and every probe stays green.
Recovering to a timestamp
Recovery in CloudNativePG is not performed in place on an existing cluster. It bootstraps a new cluster from a physical backup, which is a better default than it first looks, because the damaged cluster stays untouched while you work. A valid WAL archive is required for point in time recovery, so this only works if archiving was running before the incident.
# k8s/pg-restore-to-timestamp.yaml
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: pg-main-restored
spec:
instances: 1
storage:
size: 50Gi
bootstrap:
recovery:
source: origin
recoveryTarget:
targetTime: "2026-08-20 10:42:00.00000+00"
externalClusters:
- name: origin
plugin:
name: barman-cloud.cloudnative-pg.io
parameters:
barmanObjectName: pg-backups
serverName: pg-main
serverName is the original cluster's name, which is how the plugin finds the right series of backups in the bucket. Always put an explicit timezone in the timestamp. The documentation warns about ambiguity here for good reason, since an hour either way is usually the whole point of the exercise.
Once the restored cluster is up, connect to it and check the data before you point anything at it. Then decide whether to promote it or to export the rows you actually needed.
Prove it on a Tuesday
Run a restore to an arbitrary timestamp once a quarter and write down how long it took from decision to usable database. That number is your real recovery time objective. Everything else is a claim.
If you want this built, monitored and rehearsed on your own cluster, that is what our Kubernetes management and disaster recovery and backup engagements do.
Or read how we handle it in Kubernetes Management.
Related Articles
How to Run Magento 2 on Kubernetes Without Overpaying
Most Magento clusters cost more than they need to because every tier gets replicated as if every tier were the bottleneck, when only PHP-FPM ever is. This is the shape that keeps the bill honest, with sessions and cache moved to Valkey or Redis, media moved to object storage instead of a shared filesystem, and cron running on exactly one scheduler because Adobe documents that it can only run on one node. It also covers the case for not doing this at all, since a single well-sized server with a warm standby reaches the same uptime for a single steady store.
Server & DevOpsHow to Run Cron in Kubernetes So Jobs Never Overlap or Vanish
Scheduled work in Kubernetes fails in two quiet ways, a slow job that starts a second copy of itself and a schedule that silently stops firing. Both are configuration, not luck. This covers the CronJob fields that control concurrency, missed schedules, history retention and failure handling, with the actual defaults, so the nightly billing run is still there in the morning and there is a log to read when it is not.
CloudHow to Set Up Budgets and Anomaly Detection Before the Bill Surprises You
Finding out about a spend problem from the invoice means finding out weeks late. AWS gives you two different mechanisms for catching it earlier, and they are not interchangeable. A budget fires when a number you chose is crossed, while Cost Anomaly Detection models what your spend normally looks like and tells you when the shape changes. This walks through setting up both from the CLI, the delay each one carries between the spend happening and the alert arriving, who should be on which notification, and the charges neither one will catch for you.