
While moving production database workloads towards cloud-native (Kubernetes) environments has become very popular lately, plenty of users still rely on good old Docker containers. Compared to running PostgreSQL on bare metal, on virtual machines, or via a Kubernetes operator, Docker adds a bit of complexity, especially once you want to go beyond simple pg_dump / pg_restore for backups, upgrades, and disaster recovery.
A few years back, it was common to find open-sourced Dockerfiles from trusted companies bundling PostgreSQL with the extensions and tooling you needed. Most of those projects are now deprecated, as the ecosystem’s focus has shifted hard towar…

While moving production database workloads towards cloud-native (Kubernetes) environments has become very popular lately, plenty of users still rely on good old Docker containers. Compared to running PostgreSQL on bare metal, on virtual machines, or via a Kubernetes operator, Docker adds a bit of complexity, especially once you want to go beyond simple pg_dump / pg_restore for backups, upgrades, and disaster recovery.
A few years back, it was common to find open-sourced Dockerfiles from trusted companies bundling PostgreSQL with the extensions and tooling you needed. Most of those projects are now deprecated, as the ecosystem’s focus has shifted hard towards cloud-native patterns.
In many of my conversations with pgBackRest users, one theme comes up regularly: deploying pgBackRest for backups in Docker is straightforward, but restoring from those backups feels much harder. The usual advice was to “use a trusted Docker image and follow their guidelines”, but those images are mostly out-of-date now. These days you typically need to maintain your own image, and more importantly you need a reliable recovery playbook.
So I asked myself: how hard is point-in-time recovery (PITR) with pgBackRest for a PostgreSQL 18 Docker container? Turns out: not that hard, once you’ve seen it end-to-end.
This post is a small lab you can run locally. You’ll:
- build a PostgreSQL 18 + pgBackRest image
- take a full backup
- create restore points
- delete data on purpose
- restore to the moment just before the delete
The tiny lab setup
The lab image is deliberately small. It just layers pgBackRest on top of the official PostgreSQL 18 image, adds a minimal config, and enables WAL archiving on first init.
Dockerfile
FROM postgres:18
# Install PGDG repository
RUN apt-get update && apt-get install -y \
wget \
gnupg \
lsb-release \
&& wget --quiet -O /usr/share/keyrings/postgresql-archive-keyring.asc https://www.postgresql.org/media/keys/ACCC4CF8.asc \
&& echo "deb [signed-by=/usr/share/keyrings/postgresql-archive-keyring.asc] http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list \
&& apt-get update
# Install pgBackRest from PGDG repository
RUN apt-get install -y pgbackrest \
&& rm -rf /var/lib/apt/lists/*
# pgBackRest config
RUN mkdir -p /etc/pgbackrest
COPY pgbackrest.conf /etc/pgbackrest/pgbackrest.conf
# Enable archive_mode + archive_command on first initdb
RUN mkdir -p /docker-entrypoint-initdb.d && \
cat >/docker-entrypoint-initdb.d/pgbackrest-archive.sh <<'EOF'
#!/bin/bash
set -e
echo "archive_mode = on" >> "$PGDATA/postgresql.auto.conf"
echo "archive_command = 'pgbackrest --stanza=demo archive-push %p'" >> "$PGDATA/postgresql.auto.conf"
EOF
RUN chmod +x /docker-entrypoint-initdb.d/pgbackrest-archive.sh
USER postgres
EXPOSE 5432
A couple of points worth calling out:
- We install pgBackRest from the PGDG APT repository, so we are not relying on distro packages that might lag behind.
- The small init script dropped into
/docker-entrypoint-initdb.d/runs only on the firstinitdb. It flipsarchive_modeon and sets anarchive_commandthat pushes WAL to pgBackRest. Without WAL archiving, PITR cannot work. - Everything else is standard Postgres image behaviour, so you keep the normal entrypoint and defaults.
pgbackrest.conf
[global]
repo1-type=posix
repo1-path=/var/lib/pgbackrest/repo1
repo1-retention-full=1
repo1-bundle=y
repo1-block=y
start-fast=y
delta=y
process-max=2
log-level-console=info
log-level-file=detail
compress-type=zst
[demo]
pg1-path=/var/lib/postgresql/18/docker
This config keeps things simple:
- A single POSIX repo at
/var/lib/pgbackrest/repo1. repo1-retention-full=1to avoid filling your disk during the lab.- Zstandard compression (
compress-type=zst) to keep backups small and fast. - A single stanza called
demopointing at the default PGDATA path in the Postgres 18 image.
The image bundles PostgreSQL 18 and pgBackRest. For the sake of a simple local test, we will use a Docker volume as POSIX repository for pgBackRest backups and WAL archives. In a real setup you could switch to any pgBackRest repo type you like (for example S3), but POSIX is the quickest way to demonstrate the end-to-end flow.
Because we want both the database files and the backup repository to persist across container restarts, we will mount two named Docker volumes later on:
- one for PGDATA
- one for the pgBackRest repo
That gives us a repeatable lab where backups survive even if the container does not.
Build and run the container
1) Build the image
docker build -t pgbackrest-postgres-posix . --no-cache
2) Create the volumes
Two named volumes are used:
pg-data: PostgreSQL data directorypgbr-repo: pgBackRest backup repository
docker volume create pgbr-repo
docker volume create pg-data
3) Run PostgreSQL + pgBackRest
docker run -d \
--name pg18-pgbackrest \
-e POSTGRES_PASSWORD=mysecretpassword \
-p 5432:5432 \
-v pg-data:/var/lib/postgresql \
-v pgbr-repo:/var/lib/pgbackrest \
pgbackrest-postgres-posix
4) Initialise pgBackRest and take a backup
A stanza is a pgBackRest concept that groups config + backups for one PostgreSQL cluster.
# Initialise pgBackRest stanza
docker exec pg18-pgbackrest pgbackrest --stanza=demo stanza-create
# Run a full backup
docker exec pg18-pgbackrest pgbackrest --stanza=demo backup --type=full
# List backups
docker exec pg18-pgbackrest pgbackrest --stanza=demo info
At this point you have a working full backup stored in your POSIX repo.
Testing point-in-time recovery
The PITR flow is:
- create restore point RP1
- insert important data
- create restore point RP2
- delete the data
- restore to RP2
1) Create a database and connect
docker exec -it pg18-pgbackrest psql -U postgres -c "CREATE DATABASE testdb;"
docker exec -it pg18-pgbackrest psql -U postgres -d testdb
2) Generate some test activity
Run the following in psql:
SELECT pg_create_restore_point('RP1');
BEGIN;
CREATE TABLE important_table (field text);
INSERT INTO important_table VALUES ('important data');
COMMIT;
SELECT field FROM important_table;
SELECT pg_create_restore_point('RP2');
BEGIN;
DELETE FROM important_table;
COMMIT;
SELECT field FROM important_table;
SELECT pg_switch_wal();
What this does:
- RP1 is created before we touch the table
- we insert a row with “
important data“ - RP2 is created after the insert but before the delete
- we delete the data
pg_switch_wal()forces a WAL segment switch so the restore target is definitely archived
Now the database is in a “bad” state on purpose.
3) Restore to RP2 (before deletion)
Stop PostgreSQL first:
docker stop pg18-pgbackrest
Then run restore using a temporary container that mounts the same volumes:
docker run --rm \
-v pg-data:/var/lib/postgresql \
-v pgbr-repo:/var/lib/pgbackrest \
pgbackrest-postgres-posix \
pgbackrest restore --stanza=demo --delta --log-level-console=info \
--type=name --target=RP2 --target-action=promote
A quick breakdown of the flags:
--delta: restores only what differs, instead of wiping the directory first--type=name --target=RP2: tells PostgreSQL that we want to recover up until the restore point named RP2--target-action=promote: tells PostgreSQL to promote after recovery and start a new timeline
Start PostgreSQL again:
docker start pg18-pgbackrest
Verify the data is back:
docker exec -it pg18-pgbackrest psql -U postgres -d testdb -c "SELECT field FROM important_table;"
That’s PITR done: we recovered to the state just before the delete.
Test restore on a separate container
Even when we do not want to touch or erase our production system, it is important to test restoring backups regularly. That is the only way to validate that backups are usable and your recovery procedure actually works. With Docker, we can do this safely by restoring into a fresh volume and starting a second container that only has read-only access to the backup repository.
1) Create a fresh PGDATA volume
We restore into a brand new volume so we do not overwrite the main database volume.
docker volume create pg-test-restore-data
2) Restore the latest backup into that volume
Mount the backup repository as read-only (:ro) to avoid any accidental writes to your backups. We also disable archiving on this restored instance to prevent it from pushing WAL archives back to the production repo.
docker run --rm \
-v pg-test-restore-data:/var/lib/postgresql \
-v pgbr-repo:/var/lib/pgbackrest:ro \
pgbackrest-postgres-posix \
pgbackrest restore --stanza=demo --no-delta --log-level-console=info \
--archive-mode=off --target-timeline=current
Notes on the flags:
--no-deltarestores into an empty directory.--archive-mode=offstops the restored server from archiving WAL.--target-timeline=currentasks PostgreSQL to recover along the same timeline that was current when the backup was taken.
3) Start a temporary PostgreSQL container from the restored volume
We map container port 5432 to host port 5433 so it does not clash with the main container.
docker run -d \
--name pg18-pgbackrest-restored \
-e POSTGRES_PASSWORD=mysecretpassword \
-p 5433:5432 \
-v pg-test-restore-data:/var/lib/postgresql \
-v pgbr-repo:/var/lib/pgbackrest:ro \
pgbackrest-postgres-posix
4) Verify the restored data
Because we recovered the cluster following the initial backup timeline, PostgreSQL ignores the previous promote and you should see the table empty again.
docker exec -it pg18-pgbackrest-restored psql -U postgres -d testdb -c "SELECT field FROM important_table;"
5) Clean up the restore test
docker stop pg18-pgbackrest-restored
docker rm pg18-pgbackrest-restored
docker volume rm pg-test-restore-data
Final clean up
When you’re finished:
docker stop pg18-pgbackrest
docker rm pg18-pgbackrest
docker volume rm pgbr-repo
docker volume rm pg-data
docker rmi pgbackrest-postgres-posix
Takeaways
If you’re running PostgreSQL in Docker, pgBackRest is still a powerful backup and recovery option. The restore path is the part most people worry about, but the key steps are simple:
- keep your data and your backups on persistent volumes
- make sure WALs are archived (backups alone aren’t enough)
- stop Postgres, restore from a throwaway container, then start again
After you’ve done it once, it’s not scary anymore and you now have a repeatable playbook for real incidents.