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.
Frequently asked questions
Can a nightly dump restore Postgres to the moment before a bad migration?
A nightly dump can only put the database back to when the dump ran, which can cost up to twenty four hours of writes. Restoring to a moment such as 10.42, just before a migration, needs continuous archiving, meaning 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 named, so the recovery point objective drops from a day to the last archived segment.
Is barmanObjectStore deprecated in CloudNativePG?
Yes. The in-tree barmanObjectStore method is deprecated starting with CloudNativePG v1.26 in favour of the Barman Cloud Plugin, and spec.backup.retentionPolicy on the Cluster is deprecated too. Both still work for backward compatibility, which is why so much published YAML is quietly out of date. New clusters should be written against the plugin, which needs CloudNativePG 1.26 or newer and cert-manager, and is deployed into the operator's namespace, typically cnpg-system.
How do I turn on continuous WAL archiving in CloudNativePG with the Barman Cloud Plugin?
Declare the bucket and its credentials once in an ObjectStore resource, then reference it from the Cluster under spec.plugins, naming barman-cloud.cloudnative-pg.io with a barmanObjectName parameter and isWALArchiver set to true. That flag is what turns on continuous archiving on top of base backups. Retention now belongs on the ObjectStore as well. Keep the bucket in a different account or project from the cluster, with credentials that cannot delete objects, so one leaked key cannot drop the database and empty the backups.
What cron format does a CloudNativePG ScheduledBackup use?
A CloudNativePG ScheduledBackup takes a six field cron expression with seconds as the first entry, which is not the format a Kubernetes CronJob uses. A schedule that starts 0 0 2 therefore runs at 02:00:00 daily, not at two seconds past midnight. By default the backup is taken from the most up to date replica, falling back to the primary only if no replica is available, which keeps the work off the instance serving traffic.
What happens to Postgres when WAL archiving fails?
Postgres keeps retrying the archive periodically, and in the meantime WAL segment files keep piling up in the pg_wal directory. If the filesystem holding pg_wal fills, Postgres does a PANIC shutdown. No committed transactions are lost. The database does stay offline until space is freed, so a broken archive eventually turns into an outage on its own schedule.
How do I monitor that Postgres point in time recovery is still working on Kubernetes?
Alert on two signals, free space on the data volume and the age of the newest object in the backup bucket. A base backup that succeeded last night says nothing about whether write ahead log has shipped since. If the newest archived segment is older than the archive timeout, the recovery point has stopped moving forward, even while every pod stays Running and every probe stays green.
How do I restore a CloudNativePG cluster to a specific point in time?
CloudNativePG does not recover in place on an existing cluster. Point in time recovery bootstraps a new cluster from a physical backup, with recoveryTarget.targetTime under bootstrap.recovery and an externalClusters entry that uses the Barman Cloud Plugin with the store's barmanObjectName and a serverName equal to the original cluster's name. Always give the timestamp an explicit timezone, as in 2026-08-20 10:42:00.00000+00, since an hour either way is usually the whole point. A valid WAL archive from before the incident is required.
How often should Postgres point in time recovery be tested?
Run a restore to an arbitrary timestamp once a quarter and write down how long it took from the decision to restore to a usable database. That measured time is the real recovery time objective. Check the restored data before pointing anything at it, then decide whether to promote the restored cluster or export only the rows that were needed.
Read how we handle it in Cloud & Server 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.