| name | backup-restore |
| type | skill |
| description | PostgreSQL backup and restore with pgBackRest — full/incremental/WAL, PITR, K8s CronJob scheduling, and restore verification. |
| related-rules | ["backup-policy.md"] |
| allowed-tools | Read, Write, Edit, Bash |
Skill: Backup & Restore
Expertise: pgBackRest full/WAL/incremental backup, PITR, S3/MinIO storage, CronJob scheduling, CloudNativePG backup CRDs.
When to load
When configuring backup infrastructure, running a restore, verifying backup integrity, or recovering from data loss.
pgBackRest Configuration
[global]
repo1-type=s3
repo1-path=/postgres-backups
repo1-s3-bucket=mycompany-db-backups
repo1-s3-endpoint=s3.eu-west-1.amazonaws.com
repo1-s3-region=eu-west-1
repo1-s3-key=<AWS_ACCESS_KEY>
repo1-s3-key-secret=<AWS_SECRET_KEY>
repo1-cipher-type=aes-256-cbc
repo1-cipher-pass=<STRONG_PASSPHRASE>
repo1-retention-full=4
repo1-retention-diff=14
[global:archive-push]
compress-level=3
[production-db]
pg1-path=/var/lib/postgresql/data
pg1-host=postgres-primary
pg1-host-user=postgres
PostgreSQL: Enable WAL Archiving
ALTER SYSTEM SET wal_level = replica;
ALTER SYSTEM SET archive_mode = on;
ALTER SYSTEM SET archive_command = 'pgbackrest --stanza=production-db archive-push %p';
ALTER SYSTEM SET archive_timeout = '300';
SELECT pg_reload_conf();
Backup Commands
pgbackrest --stanza=production-db stanza-create
pgbackrest --stanza=production-db --type=full backup
pgbackrest --stanza=production-db --type=diff backup
pgbackrest --stanza=production-db --type=incr backup
pgbackrest --stanza=production-db info
pgbackrest --stanza=production-db --set=<backup-label> check
PITR Restore (Point-in-Time Recovery)
systemctl stop postgresql
pgbackrest --stanza=production-db \
--delta \
--target="2024-11-15 03:40:00+00" \
--target-action=promote \
--target-timeline=current \
restore
systemctl start postgresql
tail -f /var/log/postgresql/postgresql.log
psql -c "SELECT count(*) FROM orders WHERE created_at > '2024-11-15 03:39:00';"
K8s CronJob: Automated Backups
apiVersion: batch/v1
kind: CronJob
metadata:
name: postgres-backup-daily
namespace: database
spec:
schedule: "0 2 * * *"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
jobTemplate:
spec:
template:
spec:
serviceAccountName: postgres-backup
restartPolicy: OnFailure
containers:
- name: pgbackrest
image: pgbackrest/pgbackrest:2.50
command:
- /bin/sh
- -c
- |
pgbackrest --stanza=production-db --type=diff backup
pgbackrest --stanza=production-db check
envFrom:
- secretRef:
name: pgbackrest-s3-credentials
volumeMounts:
- name: pgbackrest-config
mountPath: /etc/pgbackrest
volumes:
- name: pgbackrest-config
configMap:
name: pgbackrest-config
CloudNativePG Backup (K8s Operator)
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: postgres-cluster
spec:
backup:
barmanObjectStore:
destinationPath: s3://mycompany-db-backups/production
s3Credentials:
accessKeyId:
name: s3-creds
key: ACCESS_KEY_ID
secretAccessKey:
name: s3-creds
key: SECRET_ACCESS_KEY
wal:
compression: gzip
data:
compression: gzip
retentionPolicy: "30d"
---
apiVersion: postgresql.cnpg.io/v1
kind: ScheduledBackup
metadata:
name: postgres-daily
spec:
schedule: "0 2 * * *"
backupOwnerReference: self
cluster:
name: postgres-cluster
Restore Verification Script (weekly)
#!/bin/bash
STANZA="production-db"
RESTORE_HOST="postgres-restore-test"
TABLES=("orders" "payments" "users")
echo "=== pgBackRest Backup Verification $(date) ==="
LAST_BACKUP=$(pgbackrest --stanza=$STANZA info --output=json | \
jq -r '.[] | .backup[-1].timestamp.stop')
echo "Last backup: $LAST_BACKUP"
AGE_HOURS=$(( ($(date +%s) - $(date -d "$LAST_BACKUP" +%s)) / 3600 ))
if [ $AGE_HOURS -gt 26 ]; then
echo "ERROR: Last backup is ${AGE_HOURS}h old — exceeds 26h threshold"
exit 1
fi
pgbackrest --stanza=$STANZA --delta --pg1-host=$RESTORE_HOST restore
pg_ctl -D /var/lib/postgresql/restore -l /tmp/restore.log start
for table in "${TABLES[@]}"; do
PROD_COUNT=$(psql -h postgres-primary -c "SELECT count(*) FROM $table;" -t | tr -d ' ')
REST_COUNT=$(psql -h $RESTORE_HOST -c "SELECT count(*) FROM $table;" -t | tr -d ' ')
if [ "$PROD_COUNT" != "$REST_COUNT" ]; then
echo "MISMATCH in $table: prod=$PROD_COUNT restore=$REST_COUNT"
FAILED=1
else
echo "OK: $table = $PROD_COUNT rows"
fi
done
pg_ctl -D /var/lib/postgresql/restore stop
[ -z "$FAILED" ] && echo "BACKUP VERIFICATION PASSED" || exit 1