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.
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_basebackup | pgBackRest | |
|---|---|---|
| Archiving WAL | Your script must refuse overwrites and flush to disk | archive-push compares checksums: an identical copy is accepted with a warning, a different one is an error |
| Backup types | Full; incremental only from PostgreSQL 17 | Full, differential and incremental, plus block-level incremental |
| Object storage | A second tool copies the files | Writes to S3, GCS, Azure or SFTP directly |
| Retention | Your script, with pg_archivecleanup | repo1-retention-full, applied after every backup |
| Restore | Unpack, write restore_command and the target, create recovery.signal | One command writes the files and settings; --delta keeps files that already match |
| Encryption and parallelism | Separate tools | repo1-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:
sudo apt install -y postgresql-commonsudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.shsudo apt install pgbackrestpgbackrest versionIf 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:
sudo install -d -o postgres -g postgres -m 770 /var/log/pgbackrestsudo install -d -m 755 /etc/pgbackrestsudo install -o postgres -g postgres -m 640 /dev/null /etc/pgbackrest/pgbackrest.confpgBackRest 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:
{
"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.
[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/mainrepo1-type=s3andrepo1-path=/pgbackrest: the repository lives underpgbackrest/in the bucket, matching the policy.repo1-s3-endpointis a host name, not a URL;repo1-s3-regionis the bucket's region. For other S3-compatible storage, use the provider's endpoint host and region; some needrepo1-s3-uri-style=path.repo1-cipher-type=aes-256-cbcandrepo1-cipher-passencrypt everything on the server before upload. The settings cannot be changed afterstanza-create, and without the passphrase nobody can restore, so store it somewhere other than this server.repo1-retention-full=2keeps two full backups and the WAL needed to restore from them.repo1-bundle=y,repo1-block=yandcompress-type=zstare the user guide's recommendations for new repositories: fewer small objects on S3, block-level incrementals, faster compression.process-max=4compresses and transfers with four processes. The default is 1; keep it low enough not to slow the database.start-fast=yforces a checkpoint so a backup starts at once instead of at the next scheduled checkpoint.pg1-pathmust be exactly thedata_directoryPostgreSQL 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:
sudo -u postgres pgbackrest --stanza=app --log-level-console=info stanza-createarchive_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:
sudo systemctl restart postgresql@16-mainsudo -u postgres pgbackrest --stanza=app --log-level-console=info checkcheck 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
sudo -u postgres pgbackrest --stanza=app --type=full --log-level-console=info backup--type=fullcopies every file and depends on nothing else.--type=diffcopies 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.
sudo -u postgres pgbackrest --stanza=app infoinfo 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:
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 backupThe 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:
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:
sudo systemctl stop postgresql@16-mainsudo -u postgres pgbackrest --stanza=app --delta restoresudo systemctl start postgresql@16-mainThat 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:
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;xidandnametargets need it when they fall before the latest backup. - It writes
restore_commandandrecovery_target_timeintopostgresql.auto.conf, so PostgreSQL can be started at once. --target-action=promoteopens 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
| Error | Cause 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 enabled | Set archive_mode = on and restart PostgreSQL; a reload is not enough. |
unable to restore while PostgreSQL is running | Stop 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 checksum | Two 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:
- pgBackRest User Guide (Debian & Ubuntu)
- pgBackRest Configuration Reference
- pgBackRest Command Reference
- pgBackRest release notes
- pgBackRest News: Weak Encryption Subkeys and Salts
- pgBackRest home page (license)
- pgBackRest source: archive timeout error (src/command/archive/find.c)
- pgBackRest source: stanza checks (src/command/check/common.c)
- pgBackRest source: archive info errors (src/info/infoArchive.c)
- pgBackRest source: restore checks (src/command/restore)
- pgBackRest source: lock errors (src/common/lock.c)
- pgBackRest source: HTTP errors (src/common/io/http/request.c)
- pgBackRest source: configuration file loading (src/config/parse.c)
- PostgreSQL: Linux downloads (Ubuntu) and the PostgreSQL Apt Repository
- PostgreSQL documentation: Continuous Archiving and Point-in-Time Recovery
- Amazon S3 API Reference: ListObjectVersions (s3:ListBucketVersions)