This episode builds an automated backup system for the expense-management app: pg_dump every night, media synced to S3 or MinIO, and regular restore drills — so when something breaks, you recover in minutes instead of losing data. Following episode 14 on secure file uploads, this one protects the very files and data those uploads created — because data without a backup is just data waiting to be lost.
1. Automating PostgreSQL Backups with pg_dump
pg_dump produces a single SQL file containing the database’s entire schema and data. Run it with the –no-owner and –no-acl flags so the file restores cleanly into a different database without getting stuck on ownership. Run the backup at 2 a.m., compress it with gzip, name the file by date, keep the 7 most recent copies, and delete older ones with find -mtime +7. Never back up onto the same disk as the database: copy the file to S3, MinIO, or another machine right after the dump finishes. Pass the password through the PGPASSWORD environment variable — never write it into the script.
#!/bin/bashset -euo pipefailDATE=$(date +%Y%m%d)pg_dump --no-owner --no-acl -h "$DB_HOST" -U "$DB_USER" "$DB_NAME" \ | gzip > "/backup/db-$DATE.sql.gz"find /backup -name "db-*.sql.gz" -mtime +7 -deleteaws s3 cp "/backup/db-$DATE.sql.gz" "s3://my-backups/db/"
0 2 * * * /opt/app/backup.sh >> /var/log/backup.log 2>&1
2. Backing Up Media and Uploaded Files
Media — attached images, user-uploaded files — is not inside pg_dump, so it needs a separate backup. Use rclone sync or aws s3 sync from the media directory to a dedicated bucket, and enable versioning so you can recover a file that was overwritten or deleted by mistake. If you self-host MinIO, snapshot the volume holding its data as well. Check the bucket size every week, because media grows faster than you think.
3. Practicing Restores Regularly
A backup that has never been restored is not a backup yet. Once a month, create an empty database, restore the newest backup file into it, run migrate –check, then open a few key pages and count the important records. Measure the restore time and write it down: if a restore takes two hours, a 30-minute recovery target is just wishful thinking. Keep the restore procedure as a runbook in the repo so anyone on call can follow it at 3 a.m.
gunzip -c /backup/db-20261110.sql.gz | psql -h localhost -U app restore_testpython manage.py migrate --check --database restore_test
Apply It to Your Project
Start tonight: write a backup script for the expense-management database, schedule it with cron for 2 a.m., sync media to S3, and set a reminder to practice a restore on the first day of next month. Once the system has run a week without errors, try deleting a test transaction and recovering it from a backup, so you can be confident everything really works.
Related Posts
- Django in Production #14: Secure File and Media Uploads — Validate File Types, Limit Size, Signed URLs
- Django in Production #4: PostgreSQL in Production — Connections, CONN_MAX_AGE, Pooling with PgBouncer, Basic Backups
- Django in Production #13: Rate Limiting and Abuse Protection for DRF
Frequently Asked Questions
How often should I back up the database?
For an expense-management app, a full backup every night is enough. If data changes constantly during the day, add WAL archiving to recover to any point in time.
Does pg_dump lock the database?
No. pg_dump runs inside a consistent read transaction, so the app keeps writing normally — it only costs extra CPU and I/O while the dump runs.
Do media backups need encryption?
Yes, if the media contains sensitive data. Turn on the bucket’s server-side encryption (SSE-S3 or SSE-KMS) and give the backup account write-only permission, never delete permission.
What should I watch for when restoring to another machine?
Check that the target PostgreSQL version is equal to or newer than the source, create the right user and extensions (for example pg_trgm), and only then run the SQL file.
References
- PostgreSQL Documentation — pg_dump: https://www.postgresql.org/docs/current/app-pgdump.html
- PostgreSQL Documentation — Backup and Restore: https://www.postgresql.org/docs/current/backup.html
- rclone Documentation — rclone sync: https://rclone.org/commands/rclone_sync/
