Database Backup and Restore
The db-backup Helm chart (manifests/apps/db-backup) backs up the CNPG Postgres cluster (nhn-postgres-cluster) and provides an on-demand restore path.
A CronJob dumps the entire cluster (all databases and roles) into a shared PVC every sixth hour. A SnapshotSchedule then takes a Trident volume snapshot of that PVC, providing a retention window of older dumps.
CNPG cluster (nhn-postgres-cluster)
│ pg_dumpall -c every 6h
│ (CronJob psql-backup)
▼
Backup PVC (pvc1-psql-volumesnap-backup, 4Gi, sc-trident-nfs)
│ Trident volume snapshot, 10 min after each dump
│ (SnapshotSchedule snapschedule-psql-backup)
▼
VolumeSnapshots (trident-csi-snapclass)
│ On-demand restore
▼
Restore PVC → restore pod (psql -f) → CNPG cluster
Components
| Resource | Kind | File | Purpose |
|---|---|---|---|
psql-backup |
CronJob | templates/cronjob.yaml |
Dumps the whole cluster into the backup PVC every 6 hours |
pvc1-psql-volumesnap-backup |
PersistentVolumeClaim | templates/pvc-backup.yaml |
4Gi RWX volume (sc-trident-nfs) holding the dump file |
snapschedule-psql-backup |
SnapshotSchedule (snapscheduler.backube/v1) |
templates/snapshotschedule.yaml |
Takes Trident snapshots of the backup PVC for retention |
trident-csi-snapclass |
VolumeSnapshotClass (cluster-scoped) | restore/volumesnapshotclass.yaml |
Required by the SnapshotSchedule; applied manually once (the cluster had none) |
psql-backup-credentials |
Secret | created by manifests/scripts/bootstrap-db-secret.sh |
username/password of the CNPG superuser, used by the CronJob and the restore pod |
On-demand restore resources (not templated; applied manually when needed):
| Resource | Kind | File | Purpose |
|---|---|---|---|
pvc1-psql-volumesnap-restore |
PersistentVolumeClaim | restore/pvc-restore.yaml |
Restore-time PVC created from a chosen VolumeSnapshot |
psql-restore |
Pod | restore/pod-restore.yaml |
One-shot pod that replays the dump into the CNPG cluster |
Backup
CronJob (psql-backup)
Runs the Bitnami Postgres client image and writes a cluster-wide logical dump:
PGPASSWORD=$POSTGRES_PASSWORD pg_dumpall -c \
-f "/backup/pgbackup-$(date +%Y%m%d%H%M).pgdump" -v \
-h nhn-postgres-cluster-rw.nhn-postgres-cluster.svc.cluster.local \
-U $POSTGRES_USER \
&& ls -1t /backup/pgbackup*.pgdump | tail -n +4 | xargs -r rm -f
Each dump is written to a date-stamped file pgbackup-<YYYYMMDDHHMM>.pgdump (UTC, matching the snapshot naming convention). After a successful dump, the job keeps only the 3 newest files on the PVC and deletes the rest — long-term retention comes from the snapshots, so the PVC only needs to hold 3 dumps.
| Setting | Value | Notes |
|---|---|---|
| Schedule | 0 */6 * * * |
00:00, 06:00, 12:00, 18:00 |
| Image | ncr.sky.nhn.no/dockerhub/bitnami/postgresql:latest |
From image.name/image.tag in values.yaml |
concurrencyPolicy |
Forbid |
A new run is skipped if the previous one is still running |
successfulJobsHistoryLimit / failedJobsHistoryLimit |
3 / 1 |
Job history kept for inspection |
runAsUser |
1001 |
|
| Credentials | Secret psql-backup-credentials |
Keys username, password |
| Output | /backup/pgbackup-<YYYYMMDDHHMM>.pgdump |
PVC pvc1-psql-volumesnap-backup mounted at /backup; one date-stamped file per run, 3 newest kept |
The -c (clean) flag makes the dump include DROP statements before each object, so a restore recreates roles and databases exactly as they existed at backup time.
Client/server version: The Bitnami client major version should match the CNPG server's Postgres version. Check the server version with:
kubectl -n nhn-postgres-cluster get cluster nhn-postgres-cluster \
-o jsonpath='{.spec.postgresql.version}'
Backup PVC
pvc1-psql-volumesnap-backup is a 4Gi ReadWriteMany claim on sc-trident-nfs, labeled postgres: backup — that label is what the SnapshotSchedule selects on. The PVC holds the 3 newest date-stamped dumps, so size it accordingly (increase it in templates/pvc-backup.yaml if 3 dumps approach 4Gi).
Snapshot schedule (snapschedule-psql-backup)
Takes a Trident snapshot of the labeled backup PVC 10 minutes after each dump (10 */6 * * *), using snapshot class trident-csi-snapclass.
| Setting | Value |
|---|---|
claimSelector |
postgres: backup (the backup PVC) |
retention.expires |
168h |
retention.maxCount |
5 |
| Snapshot name pattern | pvc1-psql-volumesnap-backup-snapschedule-psql-backup-<YYYYMMDDHHMM> |
Both retention limits apply; since snapshots are created every 6 hours, maxCount: 5 is the effective limit — the oldest snapshot is at most ~24 hours old. If a longer window is needed, raise maxCount (or lower the dump frequency).
Note that the SnapshotSchedule is independent of the CronJob: if a dump fails, the next snapshot still runs and captures the previous (or partial) dump file. Monitor dump success (see Operations).
Credentials
The psql-backup-credentials secret (keys username, password) holds the CNPG superuser credentials. It is created by manifests/scripts/bootstrap-db-secret.sh, which copies them from the cluster secret nhn-postgres-cluster-superuser in the nhn-postgres-cluster namespace:
# Interactive (prompts for namespace, default db-backup-test)
./manifests/scripts/bootstrap-db-secret.sh
# Or pass the namespace explicitly
./manifests/scripts/bootstrap-db-secret.sh db-backup-test
BACKUP_NAMESPACE=db-backup-test ./manifests/scripts/bootstrap-db-secret.sh
The secret must exist in the namespace of both the CronJob (backup) and the restore pod (restore) — normally the same namespace. Restoring requires superuser privileges because pg_dumpall output contains role definitions and drops/recreates all databases.
Deployment
The chart is deployed by Argo CD through the App of Apps (manifests/appOfApps). Each environment's values.*.yaml lists the enabled applications:
| Environment | App of Apps entry | Application name |
|---|---|---|
| dev | — (not enabled) | — |
| test | db-backup |
db-backup-test |
| prod | — (not enabled) | — |
The environment values files for the chart itself (values.dev.yaml, values.test.yaml, values.prod.yaml) are currently empty, so the chart defaults in values.yaml apply:
| Value | Default |
|---|---|
namespace |
db-backup |
image.name |
ncr.sky.nhn.no/dockerhub/bitnami/postgresql |
image.tag |
latest |
Namespace note: The chart renders its resources into .Values.namespace (default db-backup), while the on-demand restore manifests in restore/ hardcode the namespace db-backup-test. Before a restore, verify where the backup resources actually live in your environment and adjust the restore manifests' namespace fields to match:
kubectl get cronjob psql-backup -A
kubectl get pvc pvc1-psql-volumesnap-backup -A
Restore
Warning: A restore is destructive. The dump was created with
pg_dumpall -c, so replaying it drops and recreates all roles and databases as they existed at backup time. Any data, roles, or schema changes made after the backup point are lost. Restore only into a disposable/test cluster, or after confirming the current state can be discarded.
Prerequisites
- The
trident-csi-snapclassVolumeSnapshotClass exists (cluster-scoped; seerestore/volumesnapshotclass.yaml). - The
psql-backup-credentialssecret exists in the restore namespace. - You know which snapshot to restore from (see step 1).
Step 1 — Choose a snapshot
kubectl -n db-backup-test get volumesnapshots
# NAME READY SOURCEPVC RESTORESIZE AGE
# pvc1-psql-volumesnap-backup-snapschedule-psql-backup-202609181210 true pvc1-psql-volumesnap-backup 1Gi 6h
Pick the snapshot with the YYYYMMDDHHMM timestamp you want to restore.
Step 2 — Create the restore PVC from the snapshot
Edit manifests/apps/db-backup/restore/pvc-restore.yaml and set dataSource.name to the chosen snapshot, then apply:
spec:
dataSource:
name: pvc1-psql-volumesnap-backup-snapschedule-psql-backup-202609181210 # chosen snapshot
kind: VolumeSnapshot
apiGroup: snapshot.storage.k8s.io
# If a restore PVC from a previous restore exists, delete it first
kubectl -n db-backup-test delete pvc pvc1-psql-volumesnap-restore --ignore-not-found
kubectl -n db-backup-test apply -f manifests/apps/db-backup/restore/pvc-restore.yaml
# Wait until the PVC is Bound
kubectl -n db-backup-test get pvc pvc1-psql-volumesnap-restore -w
Step 3 — Run the restore pod
Optionally scale down the API and terminate active connections first, so that DROP DATABASE/CREATE DATABASE statements are not blocked by open sessions.
kubectl -n db-backup-test apply -f manifests/apps/db-backup/restore/pod-restore.yaml
# Follow the replay (psql prints NOTICE/error messages as the script runs)
kubectl -n db-backup-test logs -f psql-restore
# The pod should end in Completed
kubectl -n db-backup-test get pod psql-restore
The pod runs restartPolicy: Never and replays the newest date-stamped dump found on the restore PVC (it falls back to the legacy fixed name pgbackup.pgdump for snapshots taken before date-stamped dumps existed):
f=$(ls -1 /restore/pgbackup-*.pgdump 2>/dev/null | sort | tail -n1);
[ -z "$f" ] && f=/restore/pgbackup.pgdump;
PGPASSWORD=$POSTGRES_PASSWORD psql \
-h nhn-postgres-cluster-rw.nhn-postgres-cluster.svc.cluster.local \
-U $POSTGRES_USER -f "$f"
Step 4 — Verify
# Port-forward to the cluster through the pg-proxy pod (local port 9999)
./manifests/scripts/db-port-forward.sh
# In another terminal:
psql -h localhost -p 9999 -U <superuser> -c "\l" # databases present
psql -h localhost -p 9999 -U <superuser> -c "\du" # roles present
Step 5 — Clean up
kubectl -n db-backup-test delete pod psql-restore --ignore-not-found
kubectl -n db-backup-test delete pvc pvc1-psql-volumesnap-restore
Operations
| Task | Command |
|---|---|
| Check CronJob status and last schedule time | kubectl -n db-backup get cronjob psql-backup |
| List recent dump jobs | kubectl -n db-backup get jobs |
Read a dump job's log (look for pg_dumpall output and errors) |
kubectl -n db-backup logs job/<psql-backup-<timestamp>> |
| List available snapshots | kubectl -n db-backup get volumesnapshots |
| Check SnapshotSchedule status | kubectl -n db-backup get snapshotschedule |
Failure modes:
| Failure | Effect |
|---|---|
| A dump job fails | The PVC keeps the last successful dump; the next snapshot captures that older file. The next scheduled dump (≤ 6h later) recovers |
| 3 dumps exceed 4Gi | New dumps fail on write (PVC full); increase the PVC size in templates/pvc-backup.yaml or lower the kept-file count in the CronJob |
| Client/server version mismatch | pg_dumpall may fail or produce an incompatible dump; align the image tag with the CNPG Postgres version |
| Restore blocked by open connections | DROP DATABASE fails for a database with active sessions; scale down consumers and/or terminate sessions, then re-run the restore pod |