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
-Tdisables the pseudo-terminal. Without it, Docker can mix terminal control characters into the output and corrupt the binary dump.-Fccreates a compressed custom-format archive. It is smaller than plain SQL and letspg_restorerestore single tables or run in parallel.pg_dumptakes 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 execwithout-Twhen redirecting output. - Backing up the
pgdatavolume withtarwhile Postgres is running. - Forgetting roles, which makes restores fail with "role does not exist".
- Using
postgres:latestand 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.