Django Database Backup: Restore-Proof Operations
Build a Django database backup plan with RPO/RTO, PostgreSQL PITR, encrypted dumps, restore drills, tenant-safe recovery, and evidence.
A production Django database backup is a recoverable database state plus evidence that you can restore it within agreed loss and downtime limits. Use provider snapshots or PostgreSQL point-in-time recovery for routine recovery, keep portable logical dumps when they solve a distinct job, test restores in an isolated environment, and validate application invariants before declaring success.
The file is only the first link in the chain. A restore-proof operation also needs a known recovery point, a clean target, the right database roles and extensions, application-safe settings, integrity checks, and a measured restore time. If any link is missing, you have backup inventory rather than recovery evidence.
This guide builds that evidence chain for a Django SaaS using PostgreSQL. It
covers recovery objectives, provider backups, pg_dump, point-in-time recovery
(PITR), retention, encryption, restore drills, tenant safety, media consistency,
monitoring, and incident handoff.
On this page: define recovery targets · choose backup layers · create portable dumps · run a restore drill · validate Django · recover safely · operate the policy
Define RPO and RTO before tools
Backup frequency is a business decision expressed as two recovery targets:
- Recovery point objective (RPO): the point in time to which data must be recovered after an outage, using the NIST definition. A 15-minute RPO means the recovery design must limit acceptable data loss to 15 minutes; a nightly dump cannot meet it.
- Recovery time objective (RTO): how long the system can remain in recovery before the outage harms the business process, using the NIST definition. A four-hour RTO includes provisioning, restoring, validation, DNS or connection changes, and controlled reopening. Downloading the archive is not the whole clock.
Set both targets per data class. A customer and entitlement database may need a tighter RPO than a derived analytics store. Audit evidence may need longer retention than short-lived job rows. The policy should name who can approve a recovery point and who can reconnect production traffic.
Use a recovery contract rather than a vague promise:
| Field | Example decision | Evidence to retain |
|---|---|---|
| Protected scope | primary PostgreSQL database and required cluster globals | database identifier, schemas, roles strategy |
| RPO | at most 15 minutes | latest restorable time or archived-WAL freshness |
| RTO | restore and validate within two hours | drill timestamps and measured duration |
| Retention | daily, weekly, and legal tiers defined by policy | object versions and deletion records |
| Recovery authority | incident commander plus database operator | approved incident or drill record |
| Validation | migrations, row invariants, critical workflows | machine-readable check results |
| Reopen criteria | checks pass and side effects are controlled | signed recovery decision |
Do not copy those example numbers into a real policy. Derive them from how much data the product can lose, how long users can wait, and what the provider can actually restore.
Choose backup layers by failure mode
PostgreSQL documents three broad approaches: SQL dumps, file-system-level backups, and continuous archiving. They solve different recovery jobs; no one format covers every failure.
| Layer | Best at | Important limitation |
|---|---|---|
| Managed snapshots and PITR | routine production recovery with a short RPO | provider-, account-, region-, and retention-dependent |
| PostgreSQL base backup plus WAL archive | self-managed point-in-time recovery | operationally complex and version-specific |
pg_dump archive |
portable logical copy, selective inspection, migration rehearsal | one database at a time; no PITR; slower at large scale |
Django dumpdata fixture |
selected application data for tests or initial data | model serialization, not a complete database backup |
| Media/object backup | user uploads and other objects outside PostgreSQL | not transactionally consistent with the database by default |
For a managed PostgreSQL service, start with automated backups and PITR. Confirm
that they are enabled, inspect the latest restorable time, set retention, and
test the provider's restore path. Amazon RDS, for example,
documents that PITR creates a new database instance
without changing the source. Its transaction logs are uploaded every five
minutes, so the console's LatestRestorableTime is the evidence to inspect
rather than assuming “continuous” means zero loss.
For self-managed PostgreSQL, PITR combines a base backup with archived write-ahead log (WAL) files. PostgreSQL's continuous-archiving documentation explains how recovery replays WAL and can stop at a chosen point after the base backup. That flexibility depends on retaining every required WAL segment and keeping recovery configuration compatible with the server version.
Keep a logical archive when it provides independent value: portability to a
new PostgreSQL major version, selective inspection, a migration rehearsal, or
a second recovery path outside the provider control plane. The current
PostgreSQL 18 pg_dump reference
explicitly warns that, outside simple cases, pg_dump is generally not the
right primary mechanism for regular production backups. Treat it as one layer,
not a reason to skip provider recovery.
Why dumpdata is not a full database backup
Django's dumpdata
serializes model records for fixtures and later loaddata. It does not capture
PostgreSQL roles, grants, extensions, functions, triggers outside Django's
model view, or a point-in-time log. Django also documents that dumpdata uses
each model's default manager unless --all is supplied, so a filtering manager
can omit rows.
Use dumpdata for a deliberate subset, test fixtures, or application-level
data exchange. Do not label its JSON output as a recoverable production
database without proving every omitted database object and relationship is
recreated elsewhere.
Audit the Django deployment boundary
Django configures a database connection; it does not own the storage engine's recovery policy. Your repository should make that boundary visible.
On the current Djass main branch, generated-project configuration exposes a
DATABASE_URL path or explicit POSTGRES_* values. The public
environment-variable reference
helps you identify the connection inputs, while the
generated repository structure
shows where deployment and application settings live. Neither connection
configuration nor a successful migration proves that provider backups, WAL
archiving, retention, encryption, or restore drills exist.
Record these answers before adding a script:
- Is PostgreSQL managed or self-managed, and who owns backup configuration?
- Which databases, roles, extensions, and tablespaces must be recreated?
- Where do user uploads live, and how are they recovered with database rows?
- Which scheduled process creates or verifies logical archives?
- Which credential can read the source, write the backup, and restore a clean target? Those should not automatically be one all-powerful credential.
- What event proves the latest recovery point is usable?
Djass gives generated projects an explicit module and deployment shape, but backup policy remains an operator responsibility. That separation is useful: the application can emit checks and evidence without pretending it controls a provider snapshot service.
Create a portable PostgreSQL dump
Use PostgreSQL's custom archive format for a portable logical layer. It is
compressed by default, can be inspected with pg_restore, and supports
selective or parallel restoration.
Run the dump from a controlled job host with the PostgreSQL client version you support. Supply connection credentials through a secret manager, a restricted PostgreSQL service file, or the platform's ephemeral environment. Do not put a password in a command, log line, archive name, or monitoring payload.
set -euo pipefail
umask 077
archive="django-db-$(date -u +%Y%m%dT%H%M%SZ).dump"
pg_dump \
--format=custom \
--file="$archive" \
"$PGDATABASE"
sha256sum "$archive" > "$archive.sha256"
pg_restore --list "$archive" > "$archive.toc"
The command needs an explicit completion contract:
- capture
pg_dump's exit status and standard-error warnings; - upload the archive, checksum, and minimal metadata to protected storage;
- verify the uploaded object's size, checksum, encryption state, and retention class from the destination, not only from the job host;
- delete the local temporary copy after the remote verification succeeds;
- emit a success event only after those steps complete.
PostgreSQL states that pg_dump creates an internally consistent snapshot and
normally allows concurrent reads and writes. It only exports one database,
however. Cluster-wide roles and tablespaces require a separate strategy such
as pg_dumpall --globals-only, provider-managed recreation, or infrastructure
code. Decide whether restoring original ownership is desired before using
--no-owner or --no-privileges; those flags trade fidelity for portability.
Tools such as django-dbbackup can wrap native database utilities and add compression, encryption, remote storage, and Django management commands. That reduces scripting, but it does not choose your RPO, prove provider PITR, validate a restored application, or define incident authority. Evaluate the wrapper and the underlying recovery path separately.
Store backups outside the source failure domain
A backup that shares the production database account, region, encryption key, and deletion authority can disappear in the same incident as the source. Separate at least one recovery copy by a meaningful fault and credential boundary.
For every stored copy, record:
- immutable identifier and source database;
- start time, completion time, and represented recovery point;
- PostgreSQL server and client versions;
- archive format, size, and checksum;
- encryption method and key identifier, never the key itself;
- retention class and scheduled deletion time;
- latest successful restore drill using that class of backup.
Encrypt in transit and at rest. If the application adds client-side encryption, test key recovery with the same rigor as database recovery. A perfectly intact archive encrypted under a lost or disabled key is not recoverable.
Retention needs deletion tests too. Confirm that expiry removes the expected generation without erasing protected legal or incident copies. Confirm that a compromised application credential cannot shorten retention or delete every recovery point.
Run the restore in an isolated target
An archive listing and checksum prove that bytes arrived; they do not prove that PostgreSQL can restore them or Django can use the result. Restore into a new, isolated target on a fixed schedule and after meaningful database changes.
For a custom-format logical archive, a drill can begin like this:
set -euo pipefail
createdb --template=template0 restore_drill
pg_restore \
--exit-on-error \
--single-transaction \
--no-owner \
--no-privileges \
--dbname=restore_drill \
django-db-20260819T063000Z.dump
psql --dbname=restore_drill --command='ANALYZE;'
Create the database from template0, as PostgreSQL recommends, so local
customizations in template1 do not conflict with objects in the archive.
--exit-on-error prevents a long restore from silently continuing after a
failure. --single-transaction avoids a partially restored target, but it
cannot be combined with every performance option; large restores may instead
use parallel jobs and discard the entire target after any failure.
Never restore an untrusted archive into an environment with production credentials. PostgreSQL warns that restoring a dump executes code chosen by source superusers. Treat the archive as privileged executable input, restrict who can write it, and inspect the source or generated SQL when trust is in question.
For provider PITR, the drill is different but the proof is the same:
- Choose a recovery point inside the advertised retention window.
- Restore to a new instance or cluster with no production traffic.
- Apply the intended parameter, network, role, and secret configuration.
- record when provisioning begins and when the database accepts connections;
- run the application validation suite;
- destroy the drill target through the normal reviewed path.
Test the provider path and the logical path independently. One can succeed while the other fails because of stale permissions, unsupported extensions, missing globals, expired keys, or a changed PostgreSQL client version.
Prove the restored Django application is correct
Database availability is not application correctness. Validate the restored target from the outside in.
Start with structural checks:
- expected schemas, extensions, tables, constraints, and indexes exist;
- required roles can connect with the permissions production expects;
- Django reports no pending migration mismatch for the restored release;
- the restored code version is compatible with the schema at the chosen recovery point.
Then run domain invariants. The exact checks belong to the product, but useful categories include:
| Domain | Example invariant |
|---|---|
| Identity | each active account has one valid identity owner |
| Billing | local entitlement state has a traceable provider reference |
| Jobs | terminal jobs have completion evidence; runnable jobs are not duplicated |
| Artifacts | database metadata points to an existing object and expected checksum |
| Audit | required security events remain ordered and attributable |
| Tenancy | foreign keys and ownership prevent cross-tenant references |
Run read-only smoke tests through Django using drill credentials. If a test must mutate data, use a clearly marked synthetic tenant and delete the drill target afterward. Do not run production webhooks, email, analytics, queues, or object-deletion jobs to “see whether they work.”
The current Djass architecture makes several useful checks explicit. Generated artifacts have persisted metadata and checksums, while queued generation has durable statuses. A restore drill should verify those relationships, not only count rows. See the project generation pipeline and background-task reliability guide for the state and worker boundaries.
Measure the full drill:
recovery_requested_at
target_available_at
database_restore_completed_at
application_validation_completed_at
reopen_ready_at
The difference between the first and last timestamps is the evidence against the RTO. Record the represented recovery point too, so you can compare actual data loss with the RPO.
Make the recovery tenant-safe
Production data restored outside production creates a security event unless the environment is deliberately constrained. The drill target should begin with no public ingress and no outbound path to customer-facing systems.
Before Django connects, replace or disable:
- email and SMS delivery;
- Stripe and other payment credentials;
- webhooks and third-party API tokens;
- PostHog, Sentry, and support-widget production destinations;
- Redis queues and scheduled workers;
- object-storage delete or overwrite permissions;
- social-login callback domains and session-signing secrets where appropriate.
Use a separate drill secret set and deny production network routes. If people need to inspect the restored data, apply the organization's masking and access policy before broadening access. Deleting obvious email addresses is not a complete anonymization method; free text, audit payloads, filenames, IP addresses, and provider identifiers can still carry personal data.
For a tenant-specific incident, prefer a selective repair procedure only when
the data model and audit trail can prove its boundaries. Restoring one tenant
from a full logical archive into production is not a normal pg_restore
operation. A safer pattern is to restore the full database into isolation,
derive a reviewed repair set, preserve referential integrity, apply it through
an idempotent tool, and record before/after evidence.
Coordinate database and object recovery
Many Django products store file metadata in PostgreSQL and bytes in S3-compatible storage. Those systems do not share one transaction. A database recovery point can reference an object version created later, or omit a row for an object that still exists.
Define the consistency policy explicitly:
- retain object versions long enough to cover the database recovery window;
- preserve immutable object identifiers or version IDs in durable metadata;
- reconcile restored rows against object existence and checksum;
- quarantine unexplained objects instead of deleting them during the drill;
- document how customer-visible gaps are repaired after a point-in-time recovery.
The Django S3 storage guide covers public/private boundaries and object migration. The file-upload pipeline shows why accepted bytes, database state, scanning, and cleanup need distinct states. Use the same model during recovery: a row and an object are related evidence, not an atomic pair.
Operate backups as a measured system
Schedule backup and verification jobs with an owner, expected cadence, missed-run policy, overlap control, and useful-work evidence. The Django scheduled-tasks guide covers intended slots and run claims; apply that contract to logical dumps and restore drills.
Monitor outcomes, not only process starts:
| Signal | Alert condition |
|---|---|
| Latest provider restorable time | older than the RPO allows |
| Last verified archive | missing, too small, checksum mismatch, or late |
| WAL archive continuity | missing segment or increasing archive delay |
| Restore drill | overdue, failed, or slower than RTO |
| Retention | expected generation missing or unexpected deletion blocked |
| Encryption | key unavailable, disabled, or outside policy |
| Validation | invariant failure or migration incompatibility |
A Django health check should not perform a database restore or fail user traffic because yesterday's drill is late. Publish backup freshness as an operational metric and alert it to the owner. Reserve request readiness for dependencies that determine whether this process can serve safe traffic now.
Run at least four drill scenarios:
- latest provider recovery point;
- a specific earlier point before a known synthetic write;
- the oldest retained logical archive you promise to support;
- recovery after a PostgreSQL or application-version change.
Failures become changes to the runbook, permissions, retention, code, or recovery target. Do not mark a drill green after manually improvising an undocumented fix.
Write the incident runbook around decisions
During an incident, the difficult question is usually not “what is the restore command?” It is “which recovery point loses the least valid data without reintroducing corruption, and when is it safe to reopen writes?”
The runbook should name these decisions:
- freeze destructive automation and preserve incident evidence;
- identify the failure time and candidate recovery points;
- choose provider PITR, snapshot restore, logical restore, or selective repair;
- restore into a new target unless the provider procedure explicitly requires an in-place operation;
- apply production-equivalent configuration without enabling side effects;
- run structural and domain validation;
- compare actual RPO and RTO with the contract;
- approve traffic and worker reopening in stages;
- reconcile writes or external events that occurred after the recovery point;
- retain the incident timeline and update the next drill.
Keep database recovery separate from application rollback. A code rollback may not understand data written by a newer release, and a database restore may remove migration history the running code expects. The feature-flag release guide explains the same compatibility boundary for data-changing releases.
Use a restore-proof completion checklist
A Django database backup policy is ready only when you can answer yes to every item below:
- [ ] RPO and RTO are written for each protected data class.
- [ ] Provider backup, PITR, and retention settings are verified from the control plane.
- [ ] A second recovery layer exists for a specific justified job.
- [ ] Database roles, extensions, and cluster globals have a recreation plan.
- [ ] Archives have remote checksum, encryption, and retention evidence.
- [ ] At least one copy is outside the source deletion and credential boundary.
- [ ] Restore drills use clean, isolated targets and production-safe secrets.
- [ ] Django migrations and product-specific invariants are checked.
- [ ] Database rows and object-storage versions are reconciled.
- [ ] The latest measured recovery meets the RPO and RTO.
- [ ] Monitoring detects stale recovery points, failed archives, and overdue drills.
- [ ] The incident runbook names recovery and reopen authorities.
Djass can generate the maintained Django repository shape you use to implement these checks, with explicit configuration, background jobs, storage options, and agent-readable instructions. Review the available generator modules and current Djass pricing when you want a repeatable starting point. The backup provider and recovery policy remain yours; the generated codebase makes their application boundaries easier to state and test.