The basic pattern
docker exec -i my_pg pg_dump -U postgres --format=custom mydb \ > backup_$(date +%F).dump
docker exec uses the pg_dump INSIDE the container — automatically version-matched to the server. Piping stdout means the dump never fills the container's writable layer.
Footguns
Backing up the volume directory with tar while Postgres runs → torn, inconsistent copies (only safe with pg_backup_start/filesystem snapshots). docker exec without -i on some shells → truncated output. Dumping through docker compose exec adds a TTY by default — use -T:
docker compose exec -T db pg_dump -U postgres -Fc mydb > db.dump
Get the dump off the host
A backup on the same disk as the database survives exactly none of the interesting failures. Stream to S3/R2 (encrypted), keep retention, and restore-test into a throwaway container:
docker run -d --name rt -e POSTGRES_PASSWORD=x postgres:16 docker exec -i rt pg_restore -U postgres --no-owner -d postgres < db.dump
Whatever tool made your backup, the only way to know it works is to restore it. Firedrill does that automatically for every backup — or try a one-off free drill on a dump you already have.