How to Back Up a PostgreSQL Database

How to Back Up a PostgreSQL Database

I’ve never regretted taking a backup. I have, on more than one occasion, deeply regretted not taking one — or taking one incorrectly and only discovering that when I actually needed to restore it. Backups are one of those things that feel like a chore right up until the moment they save your entire project, and PostgreSQL gives you several solid, well-tested tools for doing it right. Let me walk through the main approaches, when to use each, and the details that actually matter.

Overview: Your Backup Options

PostgreSQL supports two fundamentally different backup strategies:

For most projects, pg_dump is where you start. For large, mission-critical databases needing point-in-time recovery, physical backups with WAL archiving are the way to go.

Backing Up with pg_dump

pg_dump backs up a single database. The basic form:

pg_dump -U postgres -d mydb -f mydb_backup.sql

This creates a plain-text SQL file. It’s human-readable, which is nice for small databases, but it doesn’t support parallel restore or selective object restoration the way the other formats do.

Custom Format (Recommended for Most Cases)

pg_dump -U postgres -d mydb -F c -f mydb_backup.dump

-F c produces a compressed, PostgreSQL-specific binary format. This is generally my default choice — it’s smaller than plain SQL, supports parallel restore with pg_restore -j, and lets you restore individual tables or objects rather than the whole thing.

Directory Format (Best for Very Large Databases)

pg_dump -U postgres -d mydb -F d -f mydb_backup_dir -j 4

Directory format splits the backup into multiple files and, unlike custom format, supports parallel dumping with -j, not just parallel restoring. For large databases, this can cut backup time significantly.

Tar Format

pg_dump -U postgres -d mydb -F t -f mydb_backup.tar

Less commonly used today — directory format generally offers the same benefits with more flexibility.

Backing Up the Entire Cluster with pg_dumpall

pg_dump only backs up a single database’s contents. If you need roles, tablespaces, and all databases in the cluster, use pg_dumpall:

pg_dumpall -U postgres -f full_cluster_backup.sql

A common pattern is to combine both: use pg_dumpall --roles-only to capture roles and permissions, and pg_dump in custom format for each individual database’s data:

pg_dumpall -U postgres --roles-only -f roles_backup.sql
pg_dump -U postgres -d mydb -F c -f mydb_backup.dump

This gives you the flexibility of pg_dump‘s restore options while still preserving the role/permission structure pg_dumpall captures.

Backing Up Specific Tables or Schemas

pg_dump -U postgres -d mydb -t employees -f employees_backup.sql

Multiple tables:

pg_dump -U postgres -d mydb -t employees -t departments -f partial_backup.sql

Excluding specific tables (useful for skipping huge log tables you don’t need in the backup):

pg_dump -U postgres -d mydb -T audit_log -f mydb_backup_no_logs.sql

Schema-only or data-only backups:

pg_dump -U postgres -d mydb --schema-only -f schema_only.sql
pg_dump -U postgres -d mydb --data-only -f data_only.sql

Physical Backups with pg_basebackup

For a full, file-system-level copy of your running database cluster:

pg_basebackup -h localhost -U replicator -D /backups/base_backup -Fp -Xs -P

This produces a complete, ready-to-use copy of your data directory. It’s version- and platform-specific — you can’t restore a base backup taken on PostgreSQL 16 into a PostgreSQL 15 instance, unlike logical dumps which are more portable.

Continuous Archiving and Point-in-Time Recovery (PITR)

For the strongest recovery guarantees, combine periodic base backups with continuous WAL archiving. In postgresql.conf:

archive_mode = on
archive_command = 'cp %p /var/lib/postgresql/wal_archive/%f'

This tells PostgreSQL to copy every completed WAL segment to your archive location as it’s generated. Combined with a periodic pg_basebackup, this lets you restore to any point in time between backups, not just the exact moment the last backup was taken — genuinely valuable if you need to recover to “just before” a specific incident (like an accidental mass delete) rather than a fixed backup timestamp.

In production, you’d typically replace the simple cp command with something more robust — shipping to S3, a dedicated backup server, or using a tool like pgBackRest or WAL-G, both of which handle compression, retention policies, and parallel operations far better than a bare cp.

Automating Backups with Cron

A simple daily backup script:

#!/bin/bash
BACKUP_DIR="/backups"
DATE=$(date +%Y%m%d_%H%M%S)
pg_dump -U postgres -d mydb -F c -f "$BACKUP_DIR/mydb_$DATE.dump"

# Remove backups older than 14 days
find "$BACKUP_DIR" -name "mydb_*.dump" -mtime +14 -delete

Add it to crontab -e:

0 2 * * * /home/user/scripts/backup_postgres.sh

This runs the backup daily at 2 AM and cleans up backups older than two weeks. For production systems, I’d lean toward a dedicated tool like pgBackRest rather than a hand-rolled script — it handles retention policies, incremental backups, and verification far more robustly.

Encrypting Backups

Since a database dump often contains sensitive data, consider encrypting backups at rest, especially if they’re being stored off-site:

pg_dump -U postgres -d mydb -F c | gpg --symmetric --cipher-algo AES256 -o mydb_backup.dump.gpg

Verifying Backups

A backup you haven’t verified is a hope, not a plan. Periodically test that your backups actually restore cleanly:

createdb -U postgres verify_restore_test
pg_restore -U postgres -d verify_restore_test mydb_backup.dump
psql -U postgres -d verify_restore_test -c "SELECT COUNT(*) FROM employees;"
dropdb -U postgres verify_restore_test

I’d recommend automating this as a scheduled job, not just something you remember to do occasionally — backup verification is exactly the kind of task that’s easy to let slip until it’s too late.

Common Use Cases

Troubleshooting Common Issues

Backup file is unexpectedly small — check for errors in the pg_dump output; a failed connection partway through can leave a truncated, seemingly “complete” file that’s actually missing data.

“out of memory” during pg_dump — this can happen with --inserts on very large tables (each row becomes an individual INSERT statement); stick with the default COPY-based format, which is far more memory-efficient.

Backup takes too long / puts too much load on production — consider running pg_dump against a hot standby replica instead of the primary, or schedule backups during low-traffic windows. For very large databases, directory format with -j for parallel dumping helps significantly.

Archive command failing silently — always monitor archive_command for failures; a failed archive command means WAL segments pile up on the primary and, eventually, fill your disk. Set up alerting on this rather than discovering it during an actual restore attempt.

Best Practices

Wrapping Up

Backing up PostgreSQL isn’t complicated in principle — pg_dump for most use cases, pg_basebackup and WAL archiving when you need point-in-time recovery — but the details around format choice, automation, and verification are where real-world backup strategies succeed or quietly fail. Set it up once, automate it, and then actually test the restore. That last step is the one people skip, and it’s the one that matters most.

Exit mobile version