VPS Snaps

How to back up PostgreSQL with pgBackRest

pgBackRest is an open-source (MIT-licensed) backup tool for PostgreSQL. It takes physical backups of the whole cluster and archives every WAL segment straight to S3-compatible storage, so you can restore to any moment since the oldest backup you keep. Install the pgbackrest package from the PostgreSQL apt repository, describe the cluster and the bucket in /etc/pgbackrest/pgbackrest.conf, run stanza-create, set archive_command = 'pgbackrest --stanza=app archive-push %p', check it with pgbackrest check, and schedule pgbackrest backup from cron.

10 min readUpdated Checked against official documentation

What pgBackRest adds to plain WAL archiving

Point-in-time recovery needs a base backup plus every WAL segment written since. You can build that by hand with archive_command and pg_basebackup, as in PostgreSQL point-in-time recovery. pgBackRest does the same job with less of your own code:

archive_command + pg_basebackuppgBackRest
Archiving WALYour script must refuse overwrites and flush to diskarchive-push compares checksums: an identical copy is accepted with a warning, a different one is an error
Backup typesFull; incremental only from PostgreSQL 17Full, differential and incremental, plus block-level incremental
Object storageA second tool copies the filesWrites to S3, GCS, Azure or SFTP directly
RetentionYour script, with pg_archivecleanuprepo1-retention-full, applied after every backup
RestoreUnpack, write restore_command and the target, create recovery.signalOne command writes the files and settings; --delta keeps files that already match
Encryption and parallelismSeparate toolsrepo1-cipher-type, process-max

Physical backups restore only to the same PostgreSQL major version and only as a whole cluster. Keep pg_dump dumps too if you need single databases or a path to a newer version.

Install pgBackRest on Ubuntu

Ubuntu 24.04's own archive carries pgBackRest 2.50. The PostgreSQL apt repository carries current releases. Add the repository with its setup script, then install the package:

Terminal
sudo apt install -y postgresql-common
Terminal
sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh
Terminal
sudo apt install pgbackrest
Terminal
pgbackrest version

If you will encrypt the repository, install 2.59.3 or later, released on October 4, 2026. Earlier versions could generate weak encryption subkeys if OpenSSL failed to read random data from the system, and the subkeys stanza-create writes last for the life of the stanza. pgBackRest's news page has a script to check an existing repository.

Make sure the log directory and configuration file exist with the owners the user guide uses. The install commands are harmless if the package already created them, but the last one empties the file:

Terminal
sudo install -d -o postgres -g postgres -m 770 /var/log/pgbackrest
Terminal
sudo install -d -m 755 /etc/pgbackrest
Terminal
sudo install -o postgres -g postgres -m 640 /dev/null /etc/pgbackrest/pgbackrest.conf

pgBackRest reads /etc/pgbackrest/pgbackrest.conf, and falls back to the old location /etc/pgbackrest.conf only when the new file does not exist. Keep one file.

Prepare the bucket and key

Create a private bucket first; pgBackRest will not create it. Unlike a dump script, pgBackRest deletes from the repository itself when backups expire, so its key needs s3:DeleteObject. This policy, adapted from the user guide, keeps it to one path in one bucket:

pgbackrest-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::acme-pg-backups",
      "Condition": {
        "StringEquals": { "s3:prefix": ["", "pgbackrest"], "s3:delimiter": ["/"] }
      }
    },
    {
      "Effect": "Allow",
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::acme-pg-backups",
      "Condition": { "StringLike": { "s3:prefix": ["pgbackrest/*"] } }
    },
    {
      "Effect": "Allow",
      "Action": ["s3:PutObject", "s3:PutObjectTagging", "s3:GetObject", "s3:GetObjectVersion", "s3:DeleteObject"],
      "Resource": "arn:aws:s3:::acme-pg-backups/pgbackrest/*"
    }
  ]
}

A key that can delete can also be used to wipe the repository. Turn on versioning for the bucket: a delete then only hides the current version, and this key cannot remove versions because it lacks s3:DeleteObjectVersion. Add a lifecycle rule with NoncurrentVersionExpiration, as in the S3 bucket guide, so hidden versions are cleared after, say, 14 days. Since version 2.54, --repo-target-time (with --repo=1) reads a versioned repository as it was at a given time, which recovers backups that were expired or deleted by mistake or by malware. Listing old versions needs s3:ListBucketVersions, so run that recovery with an admin key. On EC2, repo1-s3-key-type=auto takes temporary credentials from the instance's role instead of a stored key.

Configure pgbackrest.conf

A stanza describes one PostgreSQL cluster. Name it after what the cluster does (app), not the local cluster name. Generate the encryption passphrase with openssl rand -base64 48.

/etc/pgbackrest/pgbackrest.conf
[global]
repo1-type=s3
repo1-path=/pgbackrest
repo1-s3-bucket=acme-pg-backups
repo1-s3-endpoint=s3.eu-central-1.amazonaws.com
repo1-s3-region=eu-central-1
repo1-s3-key=<access-key-id>
repo1-s3-key-secret=<secret-access-key>
repo1-cipher-type=aes-256-cbc
repo1-cipher-pass=<output of openssl rand -base64 48>
repo1-retention-full=2
repo1-bundle=y
repo1-block=y
compress-type=zst
process-max=4
start-fast=y

[app]
pg1-path=/var/lib/postgresql/16/main
  • repo1-type=s3 and repo1-path=/pgbackrest: the repository lives under pgbackrest/ in the bucket, matching the policy.
  • repo1-s3-endpoint is a host name, not a URL; repo1-s3-region is the bucket's region. For other S3-compatible storage, use the provider's endpoint host and region; some need repo1-s3-uri-style=path.
  • repo1-cipher-type=aes-256-cbc and repo1-cipher-pass encrypt everything on the server before upload. The settings cannot be changed after stanza-create, and without the passphrase nobody can restore, so store it somewhere other than this server.
  • repo1-retention-full=2 keeps two full backups and the WAL needed to restore from them.
  • repo1-bundle=y, repo1-block=y and compress-type=zst are the user guide's recommendations for new repositories: fewer small objects on S3, block-level incrementals, faster compression.
  • process-max=4 compresses and transfers with four processes. The default is 1; keep it low enough not to slow the database.
  • start-fast=y forces a checkpoint so a backup starts at once instead of at the next scheduled checkpoint.
  • pg1-path must be exactly the data_directory PostgreSQL reports.

The file holds the S3 secret and the passphrase, so keep it 640, owned by postgres. The format is INI: no quoting, and comments only on their own lines.

Create the stanza, then turn on archiving

stanza-create writes the repository's info files. It does not need archiving yet, so run it first; the WAL pushes that follow will then find a repository to write to:

Terminal
sudo -u postgres pgbackrest --stanza=app --log-level-console=info stanza-create
/etc/postgresql/16/main/conf.d/pgbackrest.conf
archive_mode = on
archive_command = 'pgbackrest --stanza=app archive-push %p'

%p is the path of the WAL segment PostgreSQL wants archived. wal_level must stay at its default, replica, or higher. archive_mode takes effect only after a restart:

Terminal
sudo systemctl restart postgresql@16-main
Terminal
sudo -u postgres pgbackrest --stanza=app --log-level-console=info check

check creates a restore point, forces a WAL switch and waits for that segment to reach the repository, by default for up to 60 seconds (archive-timeout). It fails loudly if archiving is not working, which is the misconfiguration that otherwise only shows up when a backup cannot be restored.

Take full, differential and incremental backups

Terminal
sudo -u postgres pgbackrest --stanza=app --type=full --log-level-console=info backup
  • --type=full copies every file and depends on nothing else.
  • --type=diff copies files changed since the last full backup. Restoring it needs that full backup too.
  • --type=incr, the default, copies files changed since the last backup of any type. Restoring it needs the whole chain back to the full. With no full backup yet, pgBackRest takes a full one instead and warns.
Terminal
sudo -u postgres pgbackrest --stanza=app info

info lists each backup oldest first with its label (ending in F, D or I), start and stop times, the WAL range it needs, the database size and the compressed size in the repository. --output=json gives the same for scripts. To check the stored files against their checksums, run pgbackrest --stanza=app --output=text verify.

Retention and expire

pgBackRest runs expire after every successful backup. With repo1-retention-full=2, counted in backups, the oldest full backup is removed only after a third one completes, together with the differential and incremental backups and the WAL that depended on it. With a full backup every Sunday, you can therefore always restore to any moment of the last 7 to 14 days.

To keep by age instead, set repo1-retention-full-type=time and repo1-retention-full=14: full backups older than 14 days are removed only while another backup at least 14 days old remains. repo1-retention-diff limits how many differential backups are kept. Run pgbackrest --stanza=app expire yourself after changing retention.

Schedule backups

pgBackRest has no scheduler. The user guide's pattern is a full backup on Sunday and a differential backup Monday to Saturday:

/etc/cron.d/pgbackrest
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

30 1 * * 0   postgres pgbackrest --stanza=app --type=full backup
30 1 * * 1-6 postgres pgbackrest --stanza=app --type=diff backup

The console log level defaults to warn, so a good run prints nothing and anything cron captures is a problem; set MAILTO if the server can send mail. The full log goes to /var/log/pgbackrest. An overlapping run fails on pgBackRest's own lock instead of running twice. To alert on a backup that silently stopped, read the newest backup's end time, in Unix seconds, and compare it with the clock:

Terminal
sudo -u postgres pgbackrest --output=json --stanza=app info | jq '.[0] | .backup[-1] | .timestamp.stop'

Archiving runs all the time, not on the schedule, so also watch pg_stat_archiver as the point-in-time recovery guide shows. On a busy server, archive-async=y with a spool-path pushes WAL in batches.

Restore, to the latest point or a chosen time

Stop PostgreSQL, restore, start it again. Without --delta the data directory must be empty; with it, pgBackRest checksums the files already there, keeps the ones that match and replaces the rest:

Terminal
sudo systemctl stop postgresql@16-main
Terminal
sudo -u postgres pgbackrest --stanza=app --delta restore
Terminal
sudo systemctl start postgresql@16-main

That replays all archived WAL, to the last change that reached the repository. To stop just before a mistake, give a time with its time zone offset:

Terminal
sudo -u postgres pgbackrest --stanza=app --delta --type=time "--target=2026-10-04 09:41:00+00" --target-action=promote restore
  • pgBackRest picks a backup that finished before the target. --set=<label> picks one yourself; xid and name targets need it when they fall before the latest backup.
  • It writes restore_command and recovery_target_time into postgresql.auto.conf, so PostgreSQL can be started at once.
  • --target-action=promote opens the database for writes when the target is reached. The default, pause, leaves it read-only so you can check the data first.

--delta overwrites the data directory in place. If you might need the current state, for instance to try a different target, copy the directory aside first or restore on another server.

Test restores belong on another server with the same PostgreSQL major version and the same pgbackrest.conf. Add --archive-mode=off so the copy does not push its own WAL into your real repository, then compare row counts as in testing a restore.

Common errors

ErrorCause and fix
ERROR: [082]: WAL segment … was not archived before the 60000ms timeout, with HINT: check the archive_command to ensure that all options are correct (especially --stanza).Archiving is not reaching the repository. Check the stanza name in archive_command, that PostgreSQL was restarted, and the PostgreSQL log for archive-push errors. If S3 is just slow, raise archive-timeout.
unable to load info file '…/archive.info' or '…/archive.info.copy', with HINT: has a stanza-create been performed?Run stanza-create. If you did, the bucket, repo1-path or passphrase in this config differs from the one it was created with.
backup and archive info files exist but do not match the database, with HINT: is this the correct stanza?The repository belongs to another cluster or an older major version. After a major upgrade, run pgbackrest --stanza=app stanza-upgrade; for a new cluster, use a new stanza.
archive_mode must be enabledSet archive_mode = on and restart PostgreSQL; a reload is not enough.
unable to restore while PostgreSQL is runningStop PostgreSQL first. The hint names postmaster.pid; delete it only if PostgreSQL really is stopped.
unable to restore to path '/var/lib/postgresql/16/main' because it contains files, with HINT: try using --delta if this is what you intended.Empty the directory or add --delta.
unable to find backup set with stop time less than '…'The target is older than every backup still kept. Pick a later time, or keep more backups.
FATAL: recovery ended before configured recovery target was reached (PostgreSQL log)The chosen --set finished after the target, and replay only goes forward. Leave --set out so pgBackRest picks an earlier backup.
unable to acquire lock on file '…': Resource temporarily unavailable, with HINT: is another pgBackRest process running?The previous backup is still running. Move the schedule or speed backups up with process-max.
ERROR: [039]: HTTP request failed with 403 (Forbidden):The response content shows S3's reason: AccessDenied (policy or path), SignatureDoesNotMatch (wrong secret), RequestTimeTooSkewed (fix the server's clock).
WAL file '…' already exists in the repo1 archive with a different checksumTwo clusters are archiving to one stanza, often a restored copy. Restore test copies with --archive-mode=off.

Frequently asked questions

Is pgBackRest free?
Yes. It is open source under the MIT license, and can be used for personal or commercial purposes.
What is the difference between differential and incremental backups in pgBackRest?
A differential backup copies files changed since the last full backup. An incremental backup copies files changed since the last backup of any type, so it is smaller but needs a longer chain to restore.
Can pgBackRest restore a single database?
It restores the whole cluster. --db-include restores only the named databases; the others come back as zeroed files that cannot be used and should be dropped. For one table, restore elsewhere and copy it out with pg_dump.
Does pgBackRest replace pg_dump?
No. Its backups restore only to the same PostgreSQL major version. Keep logical dumps for upgrades, single databases and moving data between versions.

How this was checked

Commands, limits and prices were checked against these official pages, on October 4, 2026: