# CNPG restore Recovery is always into a **new** Cluster that bootstraps from the object store — CNPG never restores in place. The source backups live in `s3://cnpg-` under `serverName: ` (see [cnpg-backups.md](cnpg-backups.md)). Do all of this in the source cluster's namespace so the `cnpg--backup-s3` Secret and `vault-ca-cert` are present. ## (a) Full restore into a new cluster Recover the latest available state into a fresh cluster named `postgres-restore`. The `externalClusters` entry points at the **existing** backup path; `serverName` under `barmanObjectStore` (the new cluster's own archive target) MUST differ from the source, or the restored cluster will overwrite the archive it just recovered from. ```yaml apiVersion: postgresql.cnpg.io/v1 kind: Cluster metadata: name: postgres-restore namespace: spec: instances: 3 imageName: ghcr.io/cloudnative-pg/postgresql:18.1-system-trixie # match source storage: size: 20Gi storageClass: cephrbd-fast-delete bootstrap: recovery: source: source-archive # references externalClusters below # New archive target — DIFFERENT serverName from the source (avoids collision). backup: retentionPolicy: 30d barmanObjectStore: destinationPath: s3://cnpg- endpointURL: https://s3.ceph.unkin.net endpointCA: {name: vault-ca-cert, key: ca.crt} s3Credentials: accessKeyId: {name: cnpg--backup-s3, key: AWS_ACCESS_KEY_ID} secretAccessKey: {name: cnpg--backup-s3, key: AWS_SECRET_ACCESS_KEY} serverName: -restored # NOT "" wal: {compression: zstd} data: {compression: bzip2} externalClusters: - name: source-archive barmanObjectStore: destinationPath: s3://cnpg- endpointURL: https://s3.ceph.unkin.net endpointCA: {name: vault-ca-cert, key: ca.crt} s3Credentials: accessKeyId: {name: cnpg--backup-s3, key: AWS_ACCESS_KEY_ID} secretAccessKey: {name: cnpg--backup-s3, key: AWS_SECRET_ACCESS_KEY} serverName: # the SOURCE archive to read from ``` ```bash kubectl apply -f postgres-restore.yaml kubectl -n get cluster postgres-restore -w # wait for Cluster in healthy state ``` ## (b) Point-in-time recovery (PITR) Same as above, but add `recoveryTarget` to stop replay at a timestamp. WAL is replayed from the most recent base backup up to `targetTime`. ```yaml bootstrap: recovery: source: source-archive recoveryTarget: # RFC3339 with timezone. Also valid: targetLSN, targetXID, targetName. targetTime: "2026-07-26 14:30:00.000000+00" ``` ```bash # List backups to pick a base that precedes your target time kubectl -n get backups.postgresql.cnpg.io \ -o custom-columns=NAME:.metadata.name,START:.status.startedAt,STOP:.status.stoppedAt ``` To recover from one **specific** base backup instead of the newest, point the source at a `Backup` object: ```yaml externalClusters: - name: source-archive # ...barmanObjectStore as above... bootstrap: recovery: backup: name: recoveryTarget: targetTime: "2026-07-26 14:30:00+00" ``` ## (c) Verify, then cut over ```bash # 1. Sanity-check the recovered data before touching production. kubectl cnpg psql postgres-restore -n -- -c '\l' kubectl cnpg psql postgres-restore -n -d -- \ -c 'select max(id), count(*) from ;' # 2. Confirm the restored cluster is archiving to its NEW serverName. kubectl cnpg status postgres-restore -n ``` Cutover = repoint the app at the new cluster. CNPG service names track the Cluster name (`-rw` / `-ro` / `-r`), so update whatever the app connects through — the CNPG `Pooler` (`cnpg_pooler.yaml`) `cluster.name`, or the app's DB host env — to `postgres-restore`, then retire the old cluster. There is no in-place rename; the new name is the cluster's identity. If you truly need the old name back, restore again with `metadata.name` set to the original (after deleting the old one). ## (d) Gotchas - **serverName collision.** The new cluster's `spec.backup...serverName` must differ from the source's, or it re-uses the same path and corrupts/overwrites the source archive on its first WAL push. Use `-restored` (or similar) as above. - **Secrets must exist in the target namespace.** `cnpg--backup-s3` and `vault-ca-cert` are referenced by both `externalClusters` and `backup`. Restoring into a *different* namespace means recreating (or reflecting) those first — the `ObjectStoreUser`/`Bucket` CRs are namespace-scoped. - **Match the image major.** Bootstrap-recovery replays WAL; use the same `imageName` Postgres major as the source (mismatched majors will refuse to start). - **PITR base must precede the target.** `targetTime` has to fall after a completed base backup's start; otherwise there's nothing to replay onto. - **`recoveryTarget` is one-shot.** It only applies during bootstrap. Once promoted, the cluster is a normal primary — you can't "re-PITR" it; start a new restore.