Back Up Django Media Files
Back up Django uploads reliably: MEDIA_ROOT with restic, S3 versioning and replication, consistency with database backups, retention, and restores.
Problem statement
Many Django projects back up the database carefully and forget uploaded files. After a disk failure, an accidental rm -rf, a compromised server, or a bug that deletes objects in a bucket, the database still references avatars/2026/09/photo.jpg, invoices/1042.pdf, and thousands of other files that no longer exist. Restoring only the database leaves broken images and missing documents everywhere.
Media backups have their own challenges:
- files change continuously and can be large in total
- database rows and files must correspond, but they are backed up at different times
- files in object storage feel durable, but durability protects against hardware failure, not against deletion by your own code or an attacker with credentials
- a backup nobody has restored is an assumption, not a backup
This guide covers backups for media stored on local disk and in S3-compatible object storage, how to keep them consistent with database backups, and how to test restores.
Quick answer
- Identify where media lives:
MEDIA_ROOTon disk, or a bucket through django-storages. - Local disk: back up
MEDIA_ROOTwith an incremental, encrypted, deduplicating tool such as restic to storage in a different location, on a schedule. - Object storage: enable bucket versioning, add lifecycle rules for old versions, and replicate to a second bucket (different region or account) with restricted delete permissions.
- Take media backups right after database backups, and retain both for the same period.
- Monitor backup jobs with heartbeats, and check backup integrity regularly.
- Test restores: restore files and a matching database into a staging environment and verify pages load.
Step-by-step solution
1. Inventory media storage
python manage.py shell -c "
from django.conf import settings
from django.core.files.storage import default_storage
print(settings.STORAGES['default']['BACKEND'])
print(getattr(settings, 'MEDIA_ROOT', None))
print(type(default_storage).__name__)
"
Also check models with their own storage= arguments; private files often use a separate bucket or directory. List every location so nothing is forgotten.
Estimate size and change rate:
du -sh /srv/myapp/media
find /srv/myapp/media -type f -mtime -1 | wc -l
2. Local MEDIA_ROOT: back up with restic
restic creates encrypted, deduplicated snapshots. Only changed data is uploaded after the first run, which suits media directories that grow gradually.
Initialize a repository in object storage in another location or provider:
export RESTIC_REPOSITORY="s3:s3.eu-central-1.amazonaws.com/myapp-media-backups"
export RESTIC_PASSWORD_FILE=/etc/myapp/restic-password
export AWS_ACCESS_KEY_ID=...
export AWS_SECRET_ACCESS_KEY=...
restic init
Store the repository password in a password manager too. Without it, backups cannot be restored.
Create the backup script /srv/myapp/bin/backup-media:
#!/bin/bash
set -euo pipefail
set -a
. /etc/myapp/backup.env
set +a
restic backup /srv/myapp/media --tag media --host "$(hostname)"
restic forget --tag media --keep-daily 14 --keep-weekly 8 --keep-monthly 12 --prune
curl -fsS -m 10 --retry 3 "$MEDIA_BACKUP_HEARTBEAT_URL" > /dev/null
forget --pruneapplies retention and removes unreferenced data.- The heartbeat ping at the end only runs if backup and retention succeeded.
Schedule it with a systemd timer after the nightly database backup, and run an integrity check weekly:
restic check --read-data-subset=5%
--read-data-subset verifies a sample of stored data, which catches storage corruption without downloading the whole repository each time.
3. Object storage: versioning
If media lives in S3, enable versioning on the bucket. Overwrites and deletes then keep previous versions instead of destroying data:
aws s3api put-bucket-versioning --bucket myapp-media \
--versioning-configuration Status=Enabled
Add a lifecycle rule so noncurrent versions do not accumulate forever:
{
"Rules": [
{
"ID": "expire-old-versions",
"Status": "Enabled",
"Filter": {},
"NoncurrentVersionExpiration": { "NoncurrentDays": 90 },
"AbortIncompleteMultipartUpload": { "DaysAfterInitiation": 7 }
}
]
}
aws s3api put-bucket-lifecycle-configuration --bucket myapp-media \
--lifecycle-configuration file://lifecycle.json
Versioning protects against accidental deletion by application code, as long as the application's credentials cannot also delete old versions. Grant the app s3:DeleteObject (which creates delete markers) but not s3:DeleteObjectVersion.
4. Object storage: replication or copies elsewhere
Versioning within one bucket does not protect against losing the bucket, account compromise, or region-wide problems. Add a second copy:
- S3 Replication to a bucket in another region, ideally in another AWS account with separate credentials. Replication requires versioning on both buckets.
- Scheduled copies with
rcloneto another provider:
rclone copy s3-primary:myapp-media backup-provider:myapp-media-backup \
--checksum --transfers 16 --log-level INFO
Use copy, not sync, for backups: sync deletes files in the destination that were deleted at the source, which propagates accidental deletions into the backup. Combine with versioning or object lock on the destination for protection against tampering.
For compliance or ransomware protection, S3 Object Lock in governance or compliance mode prevents deletion of object versions for a retention period, even by administrators in compliance mode.
5. Keep database and media consistent
A database backup taken at 02:00 and a media backup at 04:00 do not describe exactly the same moment:
- files uploaded between 02:00 and 04:00 exist in the media backup but not in the database (orphans, harmless)
- files deleted between 02:00 and 04:00 exist in the database backup but not in the media backup (broken references)
Practical approach:
- run the media backup after the database backup, so the media snapshot contains every file the database backup could reference, plus a few extra
- avoid deleting media files immediately when records are deleted; soft-delete or delay physical deletion by a period longer than the gap between backups
- with S3 versioning, you can restore object versions as of the database backup time if needed
6. Restore procedures
Write down and test the steps.
restic restore to a staging path:
restic snapshots --tag media
restic restore latest --tag media --target /srv/restore-test
ls /srv/restore-test/srv/myapp/media | head
S3 restore of a deleted object: list versions and copy the previous version back:
aws s3api list-object-versions --bucket myapp-media --prefix invoices/1042.pdf
aws s3api copy-object --bucket myapp-media --key invoices/1042.pdf \
--copy-source "myapp-media/invoices/1042.pdf?versionId=EXAMPLEVERSIONID"
Full restore test: restore the database backup and media into staging, then check that pages with images and downloads work, and run a script comparing file references with storage:
# core/management/commands/check_media_references.py
from django.apps import apps
from django.core.files.storage import default_storage
from django.core.management.base import BaseCommand
from django.db.models import FileField
class Command(BaseCommand):
help = "Report FileField values that do not exist in storage."
def handle(self, *args, **options):
missing = 0
for model in apps.get_models():
fields = [f for f in model._meta.get_fields() if isinstance(f, FileField)]
for field in fields:
names = (
model._default_manager.exclude(**{field.name: ""})
.values_list(field.name, flat=True)
.iterator()
)
for name in names:
if name and not default_storage.exists(name):
missing += 1
self.stdout.write(f"{model._meta.label}.{field.name}: {name}")
self.stdout.write(f"missing files: {missing}")
For large media collections, run this on a sample or in batches, because each exists() call is a filesystem check or storage API request. Fields with custom storage= arguments need their own storage instance instead of default_storage.
7. Monitor
- heartbeat monitors for each backup job
- alerts on failed
restic checkruns - replication status metrics for S3 replication
- periodic restore tests on a calendar, with results recorded
Verification check:
- a snapshot or replicated copy of media exists outside the primary server or bucket
- application credentials cannot delete backups or old object versions
- a restore test into staging shows images and downloads working with the matching database backup
- retention policies keep backups for at least as long as database backups
Explanation
Uploaded files are part of your application's data, just stored outside the database. A useful media backup lives somewhere a single failure or compromised credential cannot reach, retains history long enough to recover from mistakes discovered late, and can be restored consistently with a database backup. Incremental snapshot tools handle local directories efficiently; for object storage, versioning plus an independent copy provides both undo and disaster protection.
Edge cases / notes
Very large media libraries
For terabytes of media, initial backups take days. Seed the backup from a snapshot or use provider-side replication, and plan restore time as part of your recovery objectives.
Generated files
Thumbnails and derived files can often be regenerated. Excluding them reduces backup size, but regenerating everything after a disaster takes time; decide consciously.
Private and sensitive files
Encrypt backups (restic does by default) and restrict access to backup credentials as tightly as production credentials.
Sizing and scheduling
Media backups behave differently from database dumps because the data set grows monotonically and individual files rarely change.
- First run transfers everything; plan for it to take hours or days on large libraries, and run it outside peak hours with bandwidth limits (
rclone --bwlimit 20M,restic --limit-upload). - Later runs transfer only new files, so they are fast and cheap.
- Frequency can be lower than database backups for slow-growing media (daily is typical), but keep the ordering: database first, then media.
- Cost is dominated by storage volume and, for object storage, request counts; deduplicating tools such as restic reduce storage but make many small requests.
Track the backup size trend. A sudden jump can mean a bug generating files (for example thumbnails regenerated on every request), and a sudden drop can mean files were deleted.
Restore time matters as much as backup time
A backup that takes three days to restore does not meet a one-day recovery objective. Measure restore time on a realistic subset:
time restic restore latest --tag media --target /srv/restore-test --include "/srv/myapp/media/avatars"
Extrapolate from the subset, and if the full restore exceeds your target, plan a faster path: provider-side replication for immediate availability, restoring critical prefixes first, or serving from the backup bucket temporarily while the main one is rebuilt.
Common mistakes
- Backing up the database but not uploads.
- Using
rclone syncoraws s3 sync --deleteas a backup, propagating deletions. - Giving the application permission to delete object versions or backup data.
- Never restoring a backup to check it works.
Next steps
- Pair with database backups in Backup and Restore PostgreSQL for Django Apps.
- Put backups into a recovery plan with Write a Disaster Recovery Plan for a Django App.
- Move uploads to object storage with How to Store Django Media Files on Amazon S3.
- Review everything with the Django Backup and Recovery Checklist.
FAQ
How should I back up Django media files?
Back up local MEDIA_ROOT with an incremental, encrypted tool such as restic to storage in another location. For S3, enable versioning and replicate or copy to a separate bucket or account.
Is S3 storage already a backup for Django uploads?
No. S3 durability protects against hardware failure, not against deletion by your code, compromised credentials, or misconfiguration. Enable versioning and keep an independent copy.
Should media backups run before or after database backups?
After. The media snapshot then contains every file the database backup can reference, and extra files are harmless orphans.
How do I check that restored media matches the database?
Restore both into staging, browse pages with images and downloads, and run a script that checks each FileField value exists in storage.