Deployment guide

pgBackRest

Overview

pgBackRest is a reliable backup and restore solution for PostgreSQL, developed and maintained by Crunchy Data. It takes full, differential and incremental backups, archives write-ahead log (WAL) continuously, compresses and encrypts everything it stores, verifies what it has written, and restores a cluster to a chosen point in time.

On its own, pgBackRest is a command-line binary. This SecureAnyCloud image on Ubuntu 24.04 is a complete PostgreSQL backup host: a pinned PostgreSQL 18.4 server with pgBackRest 2.58.0 already configured against it, an encrypted repository, WAL archiving and a working schedule of backups and verification. This guide covers deployment and operation on Azure.

What you get:

  • PostgreSQL 18.4 and pgBackRest 2.58.0, both pinned and installed from the official PostgreSQL Global Development Group (PGDG) apt repository, whose signing key is verified by its full fingerprint at build time
  • An encrypted (aes-256-cbc) repository on a dedicated 64 GiB data volume mounted at /var/lib/pgbackrest
  • Continuous WAL archiving via archive_command = 'pgbackrest --stanza=main archive-push %p', already wired through pgbackrest archive-push
  • Scheduled full (weekly), differential (daily) and incremental (hourly) backups, plus weekly repository verification, as systemd timers
  • A shipped self test that takes a backup, destroys a table and restores it, so you can prove the restore path rather than trust it
  • Unattended security upgrades left enabled so the OS keeps receiving patches

Ready on first boot. On the first boot of every virtual machine, the image generates a unique PostgreSQL superuser password and a unique repository encryption passphrase. It then creates the backup stanza for that machine, takes an initial full backup and verifies it. The machine is protecting data before you log in.

Secure by design: nothing exposed. PostgreSQL is configured with listen_addresses = 'localhost', so it binds 127.0.0.1 only. This image ships no network-reachable database port, and the only inbound port is SSH 22. pg_hba.conf is restrictive and contains no trust rule anywhere: it uses peer for the local postgres superuser and scram-sha-256 for every other path.

Secure by design: no baked credential. No password and no repository passphrase ship in the image. Both are generated per virtual machine on first boot and written to /root/pgbackrest-credentials.txt, readable only by root. The image also carries no backup data, because every build-time backup and archived WAL segment is destroyed before capture.

Prerequisites

You need:

  • An active Azure subscription, an SSH public key, and a VNet with a subnet in the target region
  • A subscription to the pgBackRest listing on Azure Marketplace
  • A Network Security Group (NSG) rule allowing TCP 22 for administration

Recommended virtual machine size: Standard_B2s (2 vCPU, 4 GB RAM) is sufficient for the backup host itself. Repositories that protect large or busy databases benefit from more CPU for compression and encryption, such as Standard_D2s_v5 or larger, and from a larger data disk.

1

Deploy from the Azure Portal

Search Marketplace for pgBackRest, select the SecureAnyCloud publisher, and click Create.

Configure the Network Security Group to allow TCP 22 for administration only. Do not open TCP 5432. This image deliberately binds PostgreSQL to loopback, so nothing is listening on that port to reach.

The image declares a dedicated data disk for the backup repository. Azure provisions the disk and mounts it at /var/lib/pgbackrest automatically. Size it for your retention policy before you deploy.

2

Deploy from the Azure CLI

RG="database-prod"; LOCATION="eastus"; VM_NAME="pgbackrest1" IMAGE_URN="sac1647283583153:pgbackrest-ubuntu-24-04:default:latest" SSH_KEY="$(cat ~/.ssh/id_rsa.pub)" az group create --name "$RG" --location "$LOCATION"
# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn sac1647283583153:pgbackrest-ubuntu-24-04:default:latest
az vm create \
  --resource-group "$RG" --name "$VM_NAME" \
  --image "$IMAGE_URN" \
  --size Standard_B2s \
  --admin-username azureuser \
  --ssh-key-values "$SSH_KEY" \
  --public-ip-sku Standard
# Open the administration port only. Do NOT open 5432 — PostgreSQL is loopback bound.
az vm open-port --resource-group "$RG" --name "$VM_NAME" --port 22 --priority 1001
3

First boot

On first boot, the image runs through these steps:

  • Asserts that the repository volume is mounted
  • Generates the per-VM PostgreSQL password and repository passphrase
  • Writes the per-machine pgbackrest.conf
  • Starts PostgreSQL and creates the main stanza
  • Runs pgbackrest check
  • Takes an initial full backup and verifies the repository
  • Enables the backup timers

This completes within a minute or two. SSH in as azureuser and read the credentials:

sudo cat /root/pgbackrest-credentials.txt

The file records the PostgreSQL superuser password, the repository encryption passphrase, the stanza name and the repository path. It is 0600 root:root, so only root can read it.

Back up the repository passphrase somewhere safe. The repository is encrypted with aes-256-cbc using a passphrase unique to this machine. Without the passphrase, your backups cannot be decrypted and therefore cannot be restored.

4

Confirm the service and the security posture

Confirm that PostgreSQL is running and that pgBackRest and PostgreSQL report the pinned versions. The listener list shows that the database is bound to loopback and that SSH is the only externally reachable port.

systemctl is-active postgresql@18-main.service pgbackrest version psql --version ss -tlnH | awk '{print $4}' | sort -u

Expected output:

active pgBackRest 2.58.0 psql (PostgreSQL) 18.4 0.0.0.0:22 127.0.0.1:5432 127.0.0.53%lo:53 127.0.0.54:53 [::]:22

127.0.0.1:5432 is PostgreSQL on loopback. 127.0.0.53 and 127.0.0.54 are the systemd resolver stub, also on loopback. Only port 22 is bound to a routable address.

5

Inspect the repository

The pgbackrest info command shows the state of your backup chain: the stanza, the cipher, the WAL archive range, and every backup with its size and timing. The initial full backup taken at first boot is already there.

sudo -u postgres pgbackrest --stanza=main info

Expected output:

stanza: main status: ok cipher: aes-256-cbc db (current) wal archive min/max (18): 000000010000000000000002/000000010000000000000003 full backup: 20260719-022052F timestamp start/stop: 2026-07-19 02:20:52+00 / 2026-07-19 02:20:57+00 wal start/stop: 000000010000000000000002 / 000000010000000000000003 database size: 22.5MB, database backup size: 22.5MB repo1: backup set size: 2.7MB, backup size: 2.7MB

status: ok means the stanza is healthy and WAL archiving is working. cipher: aes-256-cbc confirms that the repository is encrypted.

6

The backup workflow

The pgBackRest workflow is four commands. The image has already run the first two for you. The last two are what you run day to day.

stanza-create defines a stanza, a named backup configuration for one PostgreSQL cluster. First boot created the main stanza. Run this command again only when you add a new cluster or repository.

check validates the configuration end to end. It confirms that pgBackRest can reach the cluster, that the repository is writable, and that WAL archiving actually delivers a segment into the repository. Run it after any configuration change.

sudo -u postgres pgbackrest --stanza=main check

backup takes a backup. --type=full copies everything. --type=diff copies what changed since the last full backup. --type=incr copies what changed since the last backup of any type.

sudo -u postgres pgbackrest --stanza=main --type=incr backup

verify re-reads the repository and checks every file against its stored checksum. It catches silent corruption before you need the backup, rather than during a restore.

sudo -u postgres pgbackrest --stanza=main verify
7

Restore

A backup you have never restored is a hypothesis. This is the restore path, and it is the same one the image proves on every self test.

restore writes the cluster back from the repository, so stop PostgreSQL first. The --delta option makes the restore incremental. pgBackRest compares checksums and copies only the files that differ, which is much faster than a full copy on a large cluster.

sudo systemctl stop postgresql@18-main.service
sudo -u postgres pgbackrest --stanza=main --delta --type=immediate --target-action=promote restore
sudo systemctl start postgresql@18-main.service

--type=immediate recovers to the earliest point at which the cluster is consistent, which is the end of the backup being restored. --target-action=promote brings the cluster out of recovery and makes it writable again once that point is reached.

Confirm that the cluster is back and out of recovery:

sudo -u postgres psql -tAc "SELECT pg_is_in_recovery();"

f means recovery has finished and the cluster is accepting writes.

Point-in-time recovery. Because WAL is archived continuously, you can restore to any moment covered by the archive, not only to a backup boundary. Replace --type=immediate with a timestamp target:

sudo systemctl stop postgresql@18-main.service
sudo -u postgres pgbackrest --stanza=main --delta \
  --type=time --target="2026-07-19 02:21:00+00" --target-action=promote restore
sudo systemctl start postgresql@18-main.service
8

Prove the restore path with the self test

The image ships /usr/local/sbin/pgbackrest-selftest.sh. It proves that the per-machine credentials authenticate, that wrong passwords are rejected, and that PostgreSQL is bound to loopback and the repository is encrypted. It then performs a complete round trip. It writes a known row, backs it up, verifies the repository, drops the table, restores from the repository, and asserts that the row came back intact.

This is the same check the image passes before it is published. It is destructive and self-healing, so run it on a machine whose current state you are willing to roll back to the backup it takes.

sudo /usr/local/sbin/pgbackrest-selftest.sh

Expected output ends with:

OK: RESTORED DATA MATCHES: sac-selftest-20260719022500-4211 SELFTEST_OK: per-VM credentials authenticate, wrong passwords rejected, listener loopback-only, repository encrypted, and a real backup/verify/restore round trip returned the known row intact.
9

Scheduled backups

systemd timers schedule backups for you, and they are enabled on first boot. Each runs as the postgres user with a low CPU and idle I/O priority, so scheduled backups never starve the database. Each also carries a randomized delay, so a fleet of machines does not stampede the repository.

  • pgbackrest-full.timer: Sunday 01:00, full backup
  • pgbackrest-diff.timer: Monday to Saturday 01:00, differential backup
  • pgbackrest-incr.timer: hourly, at 30 minutes past, incremental backup
  • pgbackrest-verify.timer: Sunday 03:00, repository verification

List the timers and their next run times:

systemctl list-timers 'pgbackrest-*' --all --no-pager

To change a schedule, edit the timer with sudo systemctl edit pgbackrest-incr.timer and set a new OnCalendar= value. Then run sudo systemctl daemon-reload.

Retention is set in /etc/pgbackrest/pgbackrest.conf. repo1-retention-full=2 keeps two full backups and everything that depends on them, and repo1-retention-diff=6 keeps six differentials. pgBackRest expires older backups automatically at the end of each backup run.

10

Back up a remote PostgreSQL host

The same appliance can protect a PostgreSQL server running elsewhere. This is a configuration change only, with no reinstall. pgBackRest reaches the remote host over SSH as the postgres user.

On this backup host, generate a key for postgres and install it on the database server:

sudo -u postgres ssh-keygen -t ed25519 -N '' -f /var/lib/postgresql/.ssh/id_ed25519
sudo -u postgres cat /var/lib/postgresql/.ssh/id_ed25519.pub
# Append that public key to /var/lib/postgresql/.ssh/authorized_keys on the database server.

Then point the stanza at the remote cluster in /etc/pgbackrest/pgbackrest.conf:

[main] pg1-host=db1.internal.example.com pg1-host-user=postgres pg1-path=/var/lib/postgresql/18/main pg1-port=5432

Install pgBackRest on the database server too, give it the same repo1-cipher-pass, and set archive_command there to push to this host. Then re-run stanza-create and check. The check command is the gate: it does not pass until archiving from the remote cluster reaches this repository.

11

Use an Azure Blob or S3 repository

Storing the repository in object storage removes the local disk as a single point of failure and gives effectively unlimited retention. This is again a configuration-only change. Replace the repo1-path block in /etc/pgbackrest/pgbackrest.conf.

For Azure Blob Storage:

[global] repo1-type=azure repo1-azure-account=<storage-account-name> repo1-azure-container=<container-name> repo1-azure-key=<storage-account-key> repo1-path=/pgbackrest repo1-cipher-type=aes-256-cbc repo1-cipher-pass=<your-passphrase>

For S3-compatible storage:

[global] repo1-type=s3 repo1-s3-bucket=<bucket-name> repo1-s3-region=<region> repo1-s3-endpoint=s3.<region>.amazonaws.com repo1-s3-key=<access-key-id> repo1-s3-key-secret=<secret-access-key> repo1-path=/pgbackrest repo1-cipher-type=aes-256-cbc repo1-cipher-pass=<your-passphrase>

Keep repo1-cipher-type and repo1-cipher-pass set. With object storage, the repository leaves your machine, so client-side encryption is what keeps the contents private. After changing the repository, run stanza-create and then check against the new location before you rely on it.

pgBackRest also supports multiple repositories at once (repo2-*, repo3-*). You can keep fast local backups on the data volume and push a second copy to object storage for offsite retention.

12

Repository encryption and passphrase rotation

The repository is encrypted with aes-256-cbc using repo1-cipher-pass, generated uniquely for this machine at first boot and recorded in /root/pgbackrest-credentials.txt.

Rotating the passphrase invalidates every existing backup. pgBackRest cannot decrypt objects written under a previous passphrase. Changing repo1-cipher-pass does not re-encrypt the repository. It makes everything already in the repository unreadable.

If you must rotate, create a new stanza or a new repository with the new passphrase and take a fresh full backup. Keep the old passphrase for as long as you need to restore from the old backups.

Treat the passphrase as you would the backups themselves. Store it in your secret manager and keep it recoverable independently of the machine, because a repository you cannot decrypt is a repository you do not have.

13

Connecting to PostgreSQL

PostgreSQL is bound to loopback. Do administrative work on the machine as the postgres system user through peer authentication, which needs no password:

sudo -u postgres psql -tAc "SELECT version();"

To connect over TCP, use the per-machine superuser password from the credentials file:

PGPASSWORD='<the postgres.password value from /root/pgbackrest-credentials.txt>' \
  psql -h 127.0.0.1 -U postgres -c "SELECT current_user;"

To serve clients on your VNet, make three deliberate changes:

  • Set listen_addresses in /etc/postgresql/18/main/conf.d/10-sac-pgbackrest.conf.
  • Add a pg_hba.conf rule scoped to your subnet CIDR using scram-sha-256.
  • Open TCP 5432 in the Network Security Group for that subnet only.

Never open 5432 to the internet.

Troubleshooting

pgbackrest check fails with an archive error. WAL archiving is not reaching the repository. Confirm that archive_mode is on and archive_command is set, then look at what PostgreSQL reported:

sudo -u postgres psql -tAc "SHOW archive_mode; SHOW archive_command;"
sudo ls -la /var/log/pgbackrest/

pgBackRest writes one log per command, named <stanza>-<command>.log. Read the log for the command that failed. For example, run sudo tail -n 40 /var/log/pgbackrest/main-backup.log after a failed backup, or read main-stanza-create.log after a failed stanza creation. A command that has not run yet has no log file.

First boot did not finish. The first-boot unit logs to the journal and to a fixed log file:

systemctl status pgbackrest-firstboot.service --no-pager sudo tail -n 40 /var/log/sac/sac-firstboot.log

The repository volume is missing. The repository lives on a dedicated data disk mounted at /var/lib/pgbackrest. If the disk is not mounted, first boot refuses to touch the repository instead of silently writing backups onto the OS disk:

findmnt /var/lib/pgbackrest

A backup fails with no space. Check the data volume. Then either increase the disk size or lower retention in /etc/pgbackrest/pgbackrest.conf:

df -h /var/lib/pgbackrest

Support

SecureAnyCloud images include 24/7 support. Contact sales@secureanycloud.com with the offer name, the region, and the output of sudo -u postgres pgbackrest --stanza=main info.

pgBackRest is developed and maintained by Crunchy Data. PostgreSQL is a trademark of the PostgreSQL Community Association of Canada. SecureAnyCloud is not affiliated with, endorsed by or sponsored by Crunchy Data or the PostgreSQL Global Development Group.

Deploy on Azure

Launch pgBackRest PostgreSQL Backup Host on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.

View this image

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on Azure