Enable Point-in-Time Recovery for Postgres in Docker Compose with WAL-G
Turn Postgres backups into a real recovery system by combining periodic base backups, continuous WAL archiving, and a restore drill that proves you can recover to a chosen moment instead of only to last night's dump.
How base backups and archived WAL files work together, how to wire WAL-G into a Docker Compose Postgres setup, and how to rehearse recovery to a chosen timestamp.
Operators who already run Postgres in Docker Compose and have outgrown dump-only backups for apps that need tighter recovery options.
Untested PITR is theater. If you never rehearse recovery, you do not know whether your backups, WAL archive, or target-time workflow actually work.
Before you begin
- A working Docker Compose Postgres deployment you can stop and start deliberately.
- A storage target for backups and WAL archives. This draft uses an S3-compatible bucket example.
- Comfort creating env files and running a one-off container with access to the Postgres data directory.
- A separate restore location. Do not learn PITR by experimenting only against the live database volume.
Point-in-time recovery depends on two things together: a base backup and the write-ahead log stream after that
backup. A plain pg_dump captures logical data at one moment, which is still useful, but it cannot
move you to a precise timestamp inside the day. PITR can.
Step 1: Understand the moving parts
For a practical Compose setup, think in layers:
- Postgres data directory: the live database state on your volume.
- Base backup: a full snapshot WAL-G can restore as a starting point.
- Archived WAL files: the change stream needed to replay toward the target time.
- Restore rehearsal: proof that the backup chain and recovery target actually work.
If any one of those layers is missing, you do not have reliable PITR. The big mindset shift is that backup success is not enough. Recovery success is the real metric.
Step 2: Prepare WAL-G storage and secrets
Create a dedicated env file for WAL-G:
mkdir -p ~/apps/postgres-pitr
cd ~/apps/postgres-pitr
nano .walg.env
Example contents for an S3-compatible target:
WALG_S3_PREFIX=s3://my-pg-backups/prod-db
AWS_ACCESS_KEY_ID=replace-me
AWS_SECRET_ACCESS_KEY=replace-me
AWS_REGION=us-east-1
WALG_COMPRESSION_METHOD=zstd
PGHOST=/var/run/postgresql
PGPORT=5432
PGUSER=postgres
PGPASSWORD=replace-with-strong-password
Keep the bucket path specific to one cluster. Mixing multiple clusters into one loose backup prefix is an easy way to make recovery confusing later.
Step 3: Wire Postgres and WAL-G together
Put the wal-g binary into the same image that runs Postgres so archive_command and your manual backup commands use the same executable path every time.
mkdir -p bin
# Download the current PostgreSQL-focused Linux release asset from the official WAL-G releases page
# and save the extracted binary as ./bin/wal-g
chmod 0755 ./bin/wal-g
cat > Dockerfile.postgres-walg <<'EOF'
FROM postgres:16
COPY bin/wal-g /usr/local/bin/wal-g
RUN chmod 0755 /usr/local/bin/wal-g
EOF
Then use that image from Compose:
services:
postgres:
image: postgres-walg:16
build:
context: .
dockerfile: Dockerfile.postgres-walg
container_name: app-postgres
restart: unless-stopped
env_file:
- ./.walg.env
environment:
POSTGRES_DB: appdb
POSTGRES_USER: postgres
POSTGRES_PASSWORD: replace-with-strong-password
command:
- postgres
- -c
- wal_level=replica
- -c
- archive_mode=on
- -c
- archive_timeout=60s
- -c
- archive_command=wal-g wal-push "%p"
volumes:
- postgres-data:/var/lib/postgresql/data
volumes:
postgres-data:
This removes the vague "maybe the binary is mounted somewhere" problem. Postgres sees wal-g on its own
PATH, and the same env_file feeds both the archive command and your later restore tests.
docker compose up -d postgres
docker compose logs -f postgres
What you want to see is a healthy server start with no archive-command syntax errors.
Step 4: Take the first base backup
Run the first base backup from the same derived image so the command path matches the archive path you just configured:
docker compose exec -T postgres \
wal-g backup-push /var/lib/postgresql/data
Then verify that the backup exists:
docker compose exec -T postgres wal-g backup-list
Do not stop at "the command exited zero." Confirm the backup appears in the list and that WAL files begin to land in the configured archive path.
Step 5: Rehearse a restore to a chosen time
Create a separate restore location first:
mkdir -p ~/apps/postgres-pitr/restore-lab
Pull the latest base backup into that lab directory:
docker run --rm \
--env-file .walg.env \
-v ~/apps/postgres-pitr/restore-lab:/restore \
postgres-walg:16 \
wal-g backup-fetch /restore LATEST
Add a recovery target in the restored cluster configuration:
cat > ~/apps/postgres-pitr/restore-lab/postgresql.auto.conf <<'EOF'
restore_command = 'wal-g wal-fetch "%f" "%p"'
recovery_target_time = '2026-07-06 15:25:00+00'
recovery_target_action = 'promote'
EOF
touch ~/apps/postgres-pitr/restore-lab/recovery.signal
Launch a temporary restore container against that lab directory, wait for recovery to finish, and then connect to confirm that rows or timestamps match the moment you targeted.
docker run --rm --name postgres-restore-lab \
--env-file .walg.env \
-v ~/apps/postgres-pitr/restore-lab:/var/lib/postgresql/data \
-p 55432:5432 \
postgres-walg:16
Once it is up, connect with psql and check the expected data. That verification matters more than the
mechanics of starting the lab container.
Troubleshooting and safety notes
WAL files are not appearing in storage.
Check that archive_mode is on, the archive command exists in the running container, and the storage credentials are actually visible to the process running Postgres.
The restore lab boots as a normal server without replaying.
Confirm that recovery.signal exists and that the restore command is present in the restored cluster config.
The target time fails.
Use a timestamp that falls after the base backup start and within the retained WAL window. If your retention is too short, the target may no longer be recoverable.
You need to back out of the change.
Disable archiving only after confirming you still have your older dump routine. Removing WAL-G from the stack without another verified backup path is not a real rollback.