The day I stopped wrestling with pg_dump is the day I think I got more serious as a systems engineer. pg_dump is an investigation tool — not a production backup tool.

A production backup has to deliver three things:

  1. Point-in-time recovery (PITR). Being able to say “it was deleted at 02:13 last night.”
  2. Integrity verification. Testing that the backup file actually works when you open it.
  3. Retention governed by policy. Hourly/daily/weekly/monthly rotation.

pg_dump covers none of the three.

pgBackRest setup

I prefer — and recommend — taking both a local and an S3 backup. Local backups are fast; S3 backups are fire-resistant.

Off-Site Copy

If the repository lives on a single disk, one VPS fire wipes out everything. pgBackRest can write to an S3 or S3-compatible (R2, MinIO) bucket:

Preference: protect the bucket with a policy that locks versions and forbids delete. Critical in a ransomware scenario.

To keep costs under control over the long term, set the bucket lifecycle:

30 Days → Glacier Instant Retrieval → 120 Days → Delete

/etc/pgbackrest.conf:

[global]

###################################
# Local Repository
###################################
repo1-path=/var/lib/pgbackrest
repo1-retention-full=4
repo1-retention-diff=14
repo1-cipher-pass=...
repo1-cipher-type=aes-256-cbc

###################################
# AWS S3 Repository
###################################
repo2-type=s3
repo2-path=/main
repo2-s3-bucket=...
repo2-s3-endpoint=s3.{region_name}.amazonaws.com
repo2-s3-region={region_name}
repo2-s3-key=...
repo2-s3-key-secret=...
repo2-cipher-type=aes-256-cbc
repo2-cipher-pass=...

###################################
compress-type=zst
compress-level=6
process-max=4
start-fast=y
log-level-console=info
log-level-file=detail
log-path=/var/log/pgbackrest

[main]
pg1-path=/var/lib/postgresql/16/main
pg1-port=5432

Tighten the permissions;

sudo chown postgres:postgres /etc/pgbackrest.conf
sudo chmod 640 /etc/pgbackrest.conf

Create the stanza:

sudo -u postgres pgbackrest --stanza=main stanza-create

On the postgresql.conf side, we route WAL archiving to pgBackRest:

archive_mode = on
archive_command = 'pgbackrest --stanza=main archive-push %p'
archive_timeout = 60
wal_level = replica

archive_timeout = 60 — the last 60 seconds of transactions can be lost; enough for most applications, not for financial transactions.

Backup plan

On the cron side:

0 2 * * 0  pgbackrest --stanza=main --type=full backup
0 2 * * 1-6  pgbackrest --stanza=main --type=diff backup
0 */6 * * *  pgbackrest --stanza=main --type=incr backup

Weekly full, daily diff, 6-hourly incremental. Storage consumption is surprisingly low — pgBackRest detects changed pages with its delta block technique.

Integrity: if there’s a backup, can it be restored?

Taking the backup is not enough. A weekly restore drill:

pgbackrest --stanza=main --delta restore --pg1-path=/tmp/pg_restore_test

Then I start that instance on a temporary port and run a few sanity queries. Skip this step and the backup becomes false confidence.

PITR rehearsal

Rolling back to a specific point in time:

pgbackrest --stanza=main \
  --type=time \
  --target='2026-05-07 02:13:00+03' \
  restore

Running this command for the first time during a real incident is a disaster. Rehearse it beforehand — write the steps into the runbook.

Retention

Cleaning up unused backups in the repository with the expire command:

pgbackrest --stanza=main expire

With repo1-retention-full=4 the last 4 full backups are kept; older ones (and the diff/incr backups depending on them) are cleaned up automatically.

Conclusion

For copying a schema or moving a small table, pg_dump is still the right tool. But on any system that needs PITR, the backup software has to be pgBackRest (or barman, or your database provider’s native tool). The moment a backup is untested, saying “I have a backup” is a sophisticated way of lying.