DevOps Advanced

Backups and Disaster Recovery for Django: Dumps, Restic, Retention, and Restore Drills

Everything a Django deployment needs to survive losing a server: consistent database dumps, media and secrets, encrypted offsite copies with restic, retention that survives ransomware, point-in-time recovery, and the restore drill nobody runs.

DjangoZen Team Aug 07, 2026 15 min read 3 views

Every team has backups. Far fewer have restores. The distinction only becomes visible on the day a server disappears, and by then it is not a technical question but a commercial one: how much of the business is still there?

This guide covers backup and recovery for a Django application end to end — what to capture, how to store it so it survives the failure you are protecting against, how to automate it without creating a silent single point of failure, and how to prove it works before you need it.

Decide what you must be able to restore

A Django deployment is four separable things, and a backup strategy that covers three of them will still leave you stranded:

  • The database — orders, users, everything transactional.
  • User-uploaded media — outside the repository and outside the database.
  • Configuration and secrets — environment files, certificates, keys.
  • The ability to rebuild the server — packages, services, web configuration.

The fourth is the one most often missing. Teams hold a perfect database dump and then discover that reconstructing the machine around it takes two days of archaeology. Write down how the server is built, or build it from a script, so the recovery path is short and repeatable.

Define your two numbers before choosing tools

Two questions determine everything else. How much data can you afford to lose, measured in time? That is your recovery point objective. And how long can you afford to be down? That is your recovery time objective.

An internal tool might tolerate losing a day and being down for eight hours. A shop taking orders cannot. The honest answer to those two questions tells you whether nightly dumps are sufficient or whether you need continuous archiving, and whether a manual rebuild is acceptable or whether you need a warm standby. Choosing tools before answering them is how teams end up with an expensive setup that still loses the last six hours of orders.

The rule that still holds

Three copies of the data, on two different kinds of storage, with one of them somewhere else. The reasoning is about correlated failure. A second copy on the same server dies with the server. A copy in the same provider account disappears if the account is suspended, closed or compromised. The offsite copy exists precisely for the failure that takes out everything else.

The uncomfortable corollary: a backup stored with the same provider as the server is not really an offsite copy. It protects against disk failure and mistakes, not against losing the account.

Database dumps that you can actually work with

For PostgreSQL, the custom format is the right default. It compresses, and it allows selective restores — a single table, a single schema — which turns out to matter far more often than a full disaster restore.

pg_dump -U appuser -Fc -f /var/backups/appdb-$(date +%F).dump appdb

# Verify the file is what you think it is
head -c 5 /var/backups/appdb-$(date +%F).dump   # PGDMP
pg_restore --list /var/backups/appdb-$(date +%F).dump | head

The listing step is worth building into your routine. It parses the archive and prints its table of contents, which proves the file is structurally sound rather than a truncated write from a disk that filled up overnight.

Consistency, locks, and dumping under load

A dump runs inside a single transaction and gives you a consistent snapshot — but it holds a lock that blocks schema changes for its duration, and on a large database it can run for a long time while generating substantial I/O.

On a modest database, dumping at a quiet hour is entirely adequate. As data grows, move dumps to a read replica so production is untouched, and consider whether a file-level base backup plus continuous archiving fits your recovery objectives better than a nightly logical dump.

Media files are not in the database

Everything users upload lives on a filesystem or in object storage. It is not in git and not in the dump, and a database restored without it produces an application full of broken images and missing documents.

rsync -az --delete /srv/django-app/media/ /var/backups/media/
find /srv/django-app/media -type f | wc -l

If your media lives in object storage, remember it is a separate product with a separate lifecycle. It is not covered by a server snapshot, and closing the account that owns the bucket removes it. Back up the bucket to a different provider, and treat its contents with the same seriousness as the database.

Secrets need backing up too — and separately

Environment files, TLS private keys and API credentials belong in your recovery plan; restoring a database into an application that cannot authenticate to anything is only half a recovery. But they should not sit in the same archive as the data, unencrypted, where a single leaked backup compromises everything at once.

Keep secrets in a password manager or a dedicated secret store, back that up on its own schedule, and document which secrets exist and where they come from. Some — a certificate, an API key — can be reissued; knowing which ones is part of the plan.

Encrypt before it leaves the building

A backup is a complete copy of your production data with none of the access control that protects the live system. Encrypt it before it goes anywhere, so that storage is a place where bytes live rather than a place where your customers' data is readable.

Client-side encryption — where the data is encrypted before upload and the storage provider never holds the key — is the standard to aim for. It also means an accidentally public bucket is an embarrassment rather than a breach.

Restic: deduplicated, encrypted, verifiable

Restic covers encryption, deduplication, retention and integrity checking in one tool, and speaks to local disks, SFTP and S3-compatible object storage alike.

export RESTIC_REPOSITORY="s3:https://storage.example.com/backups"
export RESTIC_PASSWORD_FILE=/root/.restic-pass

restic init
restic backup /var/backups /srv/django-app/media
restic snapshots

Deduplication means a daily backup of a mostly-unchanged dataset costs a fraction of a full copy, which in turn means you can afford to keep more history. Losing the repository password means losing the backups, so store it somewhere that is not the server being backed up.

Retention is a security control

Keeping only the most recent backup protects against hardware failure and nothing else. Data corruption and malicious deletion are usually discovered days later, by which time a single-copy strategy has faithfully overwritten the last good version.

restic forget --prune \
  --keep-daily 7 --keep-weekly 4 --keep-monthly 12

Seven days, four weeks, twelve months is a reasonable default: enough granularity to recover from last night, enough history to recover from a problem introduced last quarter. If your storage supports it, enable immutability or object-lock so that a compromised server cannot delete its own backup history — which is precisely what ransomware attempts first.

Automate with something that reports failure

A backup job that runs is not the same as a backup job that succeeded. Automate with cron or a systemd timer, capture both output streams, and make failure visible.

15 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
#!/usr/bin/env bash
set -euo pipefail
pg_dump -U appuser -Fc -f /var/backups/appdb.dump appdb
restic backup /var/backups /srv/django-app/media
restic forget --prune --keep-daily 7 --keep-weekly 4 --keep-monthly 12
restic check --read-data-subset=5%

The set -euo pipefail line matters more than it looks. Without it, a failing dump is followed cheerfully by a backup of yesterday's file, and the log reports success.

Silent failure is the normal failure

Backups do not usually fail loudly. They fail because a disk filled, a password rotated, a network path changed — and nobody noticed for five weeks because nothing was watching.

Use a dead-man's-switch service: the script pings a URL on success, and if the ping does not arrive on schedule you get an alert. This catches the case that log-based monitoring misses entirely, which is the job not running at all.

curl -fsS -m 10 --retry 3 https://hc.example.com/ping/your-uuid

Point-in-time recovery when nightly is not enough

If losing a day of data is unacceptable, continuous archiving is the answer. PostgreSQL can ship its write-ahead log continuously, letting you restore a base backup and then replay transactions up to any chosen moment — including the second before someone ran a destructive statement.

archive_mode = on
archive_command = 'restic backup --stdin --stdin-filename %f < %p'
wal_level = replica

It is more moving parts and more storage. Whether it is worth it comes straight from the recovery point objective you defined at the start.

Most restores are partial

Full disaster recovery is rare. Restoring one table after a bad migration, or one file a user deleted, is common — and a backup format that supports it saves you from restoring a whole database onto a scratch server to retrieve a single row.

pg_restore -U appuser -d appdb -t orders --data-only appdb.dump
restic restore latest --target /tmp/recover --include /srv/django-app/media/uploads/2026

The restore drill nobody runs

This is the step that separates a backup strategy from a backup folder. Once a quarter, restore to a scratch server and confirm the application actually runs against the restored data.

  1. Provision a temporary machine.
  2. Restore the latest backup: database, media, configuration.
  3. Start the application and exercise it — log in, open a record, generate a document.
  4. Write down how long the whole thing took.
  5. Destroy the machine.

That last-but-one step is the point. The measured duration is your real recovery time objective, as opposed to the one you hoped for. Teams routinely discover it is four times their estimate, and that discovery is much cheaper during a drill than during an outage.

Write the runbook while things are calm

During an incident, nobody reasons well. Write the recovery procedure down beforehand — exact commands, where the repository lives, where the password is kept, who to notify — and store it somewhere reachable when your infrastructure is not. A runbook that lives only on the server you are trying to recover is not a runbook.

Summary

Back up four things, not one: database, media, secrets, and the means to rebuild the machine. Decide how much data you can lose and how long you can be down, then choose tools that meet those numbers. Keep three copies across two kinds of storage with one genuinely elsewhere — a different provider, not just a different disk. Encrypt before upload. Use retention that survives a problem discovered late, and immutability where you can. Automate with a script that fails loudly and alerts when it does not run at all. And restore for real, on a schedule, because until you have done that you do not have backups — you have files you hope are backups.