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.

Postgres WAL-G Point-in-time recovery
Illustrated guide cover for Enable Point-in-Time Recovery for Postgres in Docker Compose with WAL-G
Postgres • WAL-G • Point-in-time recovery
What you learn

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.

Best for

Operators who already run Postgres in Docker Compose and have outgrown dump-only backups for apps that need tighter recovery options.

Risk to watch

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.

Warning: Keep dump backups anyway. WAL-based recovery is stronger for database-state recovery, but logical dumps are still useful for smaller restore jobs, migrations, and sanity checks.

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.

Expected outcome: By the end of this guide, you will have WAL archiving enabled, one base backup stored off-host, and a rehearsed restore flow to a chosen recovery target time.

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.