Running PostgreSQL in Docker Compose is quick to set up, and that is exactly why so many stacks go to production without a working backup. This guide shows how to make a reliable Docker Compose Postgres backup with pg_dump, automate it, keep copies offsite, and avoid the mistakes that produce empty or unusable dump files.

The problem: the data lives in a volume nobody backs up

In a typical docker-compose.yml, PostgreSQL stores its data in a named volume such as pgdata. Removing the stack with docker compose down -v, a failed disk, a host that gets rebuilt or a ransomware attack deletes that volume, and with it every record of the application. Copying the volume directory while Postgres is running is not a safe backup either: the files can be captured mid-write and fail to start later.

The consequence is a database that cannot be recovered, or that is recovered from a weeks-old copy someone made by hand. The fix is a logical backup made by PostgreSQL itself, on a schedule, stored away from the host.

Example stack

services:
  db:
    image: postgres:17
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
      POSTGRES_DB: app
    volumes:
      - pgdata:/var/lib/postgresql/data
    secrets:
      - db_password
volumes:
  pgdata:
secrets:
  db_password:
    file: ./secrets/db_password.txt

Pin the major version (postgres:17, not postgres:latest). A restore must run on the same or a newer major version, and an unplanned upgrade through latest breaks the data directory.

Back up one database with pg_dump

Run pg_dump inside the database container, so the client version matches the server:

mkdir -p backups
docker compose exec -T db pg_dump -U app -d app -Fc > backups/app-$(date +%F_%H%M).dump
  • -T disables the pseudo-terminal. Without it, Docker can mix terminal control characters into the output and corrupt the binary dump.
  • -Fc creates a compressed custom-format archive. It is smaller than plain SQL and lets pg_restore restore single tables or run in parallel.
  • pg_dump takes a consistent snapshot without blocking the application, so it is safe to run while the stack is in use.

If you prefer plain SQL, which you can read and edit, compress it on the way out:

docker compose exec -T db pg_dump -U app -d app | gzip > backups/app-$(date +%F).sql.gz

Back up roles and every database

pg_dump does not include roles, passwords or databases other than the one you name. Save the global objects too:

docker compose exec -T db pg_dumpall -U app --globals-only > backups/globals-$(date +%F).sql

For a server with several databases, either loop over them with pg_dump or use pg_dumpall, which produces a single plain SQL file with everything (simpler, but you lose the custom format features).

Automate the Docker Compose Postgres backup

Save this script as /opt/app/backup-postgres.sh:

#!/usr/bin/env bash
set -euo pipefail

cd /opt/app                      # directory with docker-compose.yml
DEST=/var/backups/postgres
KEEP_DAYS=14
STAMP=$(date +%F_%H%M)
mkdir -p "$DEST"

docker compose exec -T db pg_dump -U app -d app -Fc > "$DEST/app-$STAMP.dump"
docker compose exec -T db pg_dumpall -U app --globals-only > "$DEST/globals-$STAMP.sql"

# Refuse to keep suspiciously small dumps
if [ "$(stat -c %s "$DEST/app-$STAMP.dump")" -lt 1024 ]; then
  echo "Dump too small, check the database" >&2
  exit 1
fi

# Verify the archive can be read
docker compose exec -T db pg_restore --list < "$DEST/app-$STAMP.dump" > /dev/null

find "$DEST" -type f -mtime +"$KEEP_DAYS" -delete

Make it executable and schedule it with cron:

chmod 700 /opt/app/backup-postgres.sh
crontab -e
# every day at 02:30, log output for troubleshooting
30 2 * * * /opt/app/backup-postgres.sh >> /var/log/backup-postgres.log 2>&1

set -o pipefail matters: without it, a failed pg_dump piped into another command can still exit with success and leave an empty file that looks like a backup. Add an alert when the script fails, for example a monitoring ping that only fires on success.

Alternative: a backup container inside the stack

If you prefer to keep everything in Compose, add a service that runs scheduled dumps, such as the community image prodrigestivill/postgres-backup-local, which handles schedule and retention through environment variables. It is convenient, but review the image, pin its version and remember that the dumps still sit on the same host until you copy them elsewhere.

Move the backups off the host

A dump in /var/backups protects against a bad migration, not against losing the server. Copy every dump to another location with an encrypting tool such as restic or rclone with a crypt remote, using S3-compatible storage or another server. Keep at least one copy that the production host cannot delete, for example with object lock. The 3-2-1-1-0 rule is a good target.

Test the restore

A dump you have never restored is a hope, not a backup. The companion guide Docker Compose Postgres: restore a dump walks through restoring into the same stack or into a fresh test container, and fixing the common errors. Do it once a month and note how long it took.

Common mistakes

  • Using docker compose exec without -T when redirecting output.
  • Backing up the pgdata volume with tar while Postgres is running.
  • Forgetting roles, which makes restores fail with "role does not exist".
  • Using postgres:latest and discovering a major version change during a restore.
  • Keeping dumps only on the Docker host.

For broader strategies, including WAL archiving and point-in-time recovery, read how to automate PostgreSQL backups and Docker backup best practices. Backnix is being built to discover databases in Docker hosts and back them up consistently offsite: see the Docker backup page for the current status and the plans.

References

Topics: DockerDocker Composepg_dumpPostgreSQL