Databases Azure

pgBackRest on Ubuntu 24.04 on Azure User Guide

| Product: pgBackRest PostgreSQL Backup Host on Ubuntu 24.04 LTS on Azure

Overview

This guide covers the deployment and operation of pgBackRest on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images. 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.

pgBackRest on its own is a command line binary, so this image is not a bare binary drop. It is a complete PostgreSQL backup host: a pinned PostgreSQL 18.4 server, pgBackRest 2.58.0 configured against it, an encrypted repository on a dedicated data volume, WAL archiving already wired through pgbackrest archive-push, and a working schedule of backups and verification. Both packages are installed from the official PostgreSQL Global Development Group (PGDG) apt repository, whose signing key is verified by its full fingerprint at build time.

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, creates the backup stanza for that machine, takes an initial full backup, and verifies it. The machine is protecting data before you log in.

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

Security 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: every build time backup and archived WAL segment is destroyed before capture.

What is included:

  • PostgreSQL 18.4 and pgBackRest 2.58.0, both pinned and installed from the official PGDG repository (signing key verified by full fingerprint)

  • 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'

  • Scheduled full (weekly), differential (daily), incremental (hourly) backups and weekly repository verification, as systemd timers

  • A shipped self test that takes a backup, destroys a table and restores it, so the restore path can be proven rather than trusted

  • Unattended security upgrades left enabled so the OS keeps receiving patches

Prerequisites

  • Active Azure subscription, SSH public key, VNet and subnet in the target region

  • Subscription to the pgBackRest listing on Azure Marketplace

  • A Network Security Group 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 protecting large or busy databases benefit from more CPU for compression and encryption — Standard_D2s_v5 or larger — and from a larger data disk.

Step 1: Deploy from the Azure Portal

Search pgBackRest in Marketplace, select the cloudimg 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 there is nothing listening on that port to reach.

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

Step 2: Deploy from the Azure CLI

RG="database-prod"; LOCATION="eastus"; VM_NAME="pgbackrest1"
GALLERY_IMAGE_ID="/subscriptions/<sub-id>/resourceGroups/azure-cloudimg/providers/Microsoft.Compute/galleries/cloudimgGallery/images/pgbackrest-ubuntu-24-04/versions/<version>"
SSH_KEY="$(cat ~/.ssh/id_rsa.pub)"
az group create --name "$RG" --location "$LOCATION"
az vm create \
  --resource-group "$RG" --name "$VM_NAME" \
  --image "$GALLERY_IMAGE_ID" \
  --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

Step 3: First boot

On first boot the image asserts the repository volume is mounted, generates the per virtual machine PostgreSQL password and repository passphrase, writes the per machine pgbackrest.conf, starts PostgreSQL, creates the main stanza, runs pgbackrest check, takes an initial full backup, verifies the repository, and 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 it your backups cannot be decrypted, and therefore cannot be restored.

Step 4: Confirm the service and the security posture

PostgreSQL is running, pgBackRest and PostgreSQL report the pinned versions, and the listener set confirms the database is bound to loopback while the only externally reachable port is SSH.

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 loopback. Only port 22 is bound to a routable address.

The pgBackRest backup host reporting postgresql at eighteen main service active, pgBackRest version two point five eight point zero, psql version eighteen point four, and a listener set showing PostgreSQL bound to loopback on port five four three two with only SSH port twenty two on a routable address

Step 5: Inspect the repository

pgbackrest info is the single command that tells you 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 the repository is encrypted.

The pgBackRest info command reporting the main stanza with status ok, an aes two five six cbc encrypted repository, the archived write ahead log range, and the initial full backup taken automatically at first boot with its start and stop timestamps and sizes

Step 6: The backup workflow

The pgBackRest workflow is four commands. The image has already run the first two for you; the second 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. You only run this again when you add a new cluster or repository.

check validates the configuration end to end: it confirms 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; --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, so silent corruption is caught before you need the backup rather than during a restore.

sudo -u postgres pgbackrest --stanza=main verify

A pgBackRest check confirming the repository configuration and that a write ahead log segment was successfully archived, followed by an incremental backup completing and a verify command reading the repository back and completing successfully

Step 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 PostgreSQL must be stopped first. --delta makes the restore incremental: pgBackRest compares checksums and copies only files that differ, which is dramatically 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 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 rather than only to a backup boundary. Substitute a timestamp for --type=immediate:

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

Step 8: Prove the restore path with the self test

The image ships /usr/local/sbin/pgbackrest-selftest.sh. It proves the per machine credentials authenticate, that wrong passwords are rejected, that PostgreSQL is loopback bound and the repository encrypted, and 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 the row came back intact.

This is the same check the image passes before it is published. It is destructive and self healing — 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: cloudimg-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.

The pgBackRest self test output showing the per virtual machine credentials authenticating, two wrong passwords being rejected, the loopback only listener and encrypted repository confirmed, then a known row written, backed up, verified, the table dropped, and the row restored from the repository with the restored data matching

Step 9: Scheduled backups

Backups are already scheduled by systemd timers, enabled on first boot. Each runs as the postgres user with a low CPU and idle IO priority so scheduled backups never starve the database, and each carries a randomised delay so a fleet of machines does not stampede the repository.

Timer Schedule Backup type
pgbackrest-full.timer Sunday 01:00 Full
pgbackrest-diff.timer Monday to Saturday 01:00 Differential
pgbackrest-incr.timer Hourly, at 30 minutes past Incremental
pgbackrest-verify.timer Sunday 03:00 Repository verification
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 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.

Step 10: Back up a remote PostgreSQL host

The same appliance can protect a PostgreSQL server running elsewhere — this is a configuration change only, 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, set archive_command there to push to this host, then re-run stanza-create and check. check is the gate: it will not pass until archiving from the remote cluster reaches this repository.

Step 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. Again, configuration only — 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 then check against the new location before relying on it.

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

Step 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, so changing repo1-cipher-pass does not re-encrypt the repository — it makes everything already in it unreadable. If you must rotate, create a new stanza or a new repository with the new passphrase and take a fresh full backup, and 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 make sure it is recoverable independently of the machine, because a repository you cannot decrypt is a repository you do not have.

Step 13: Connecting to PostgreSQL

PostgreSQL is bound to loopback. Administrative work is done 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 with 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 you must make three deliberate changes: set listen_addresses in /etc/postgresql/18/main/conf.d/10-cloudimg-pgbackrest.conf, add a pg_hba.conf rule scoped to your subnet CIDR using scram-sha-256, and 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 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, so read the one for the command that failed — for example sudo tail -n 40 /var/log/pgbackrest/main-backup.log after a failed backup, or 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/cloudimg-firstboot.log

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

findmnt /var/lib/pgbackrest

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

df -h /var/lib/pgbackrest

Support

cloudimg images include 24/7 support. Contact support@cloudimg.co.uk 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. cloudimg is not affiliated with, endorsed by, or sponsored by Crunchy Data or the PostgreSQL Global Development Group.