Fix Uploaded Files Disappearing After a Django Redeploy
Fix Django media uploads that vanish after a redeploy: ephemeral container and PaaS filesystems, MEDIA_ROOT inside release directories, missing volumes, and moving uploads to object storage.
Problem statement
Users upload avatars, documents, or product images, and everything works, until the next deploy. Afterwards, the database still references the files (uploads/2026/09/photo.jpg), but requesting them returns 404, and image fields render broken images.
Uploaded files are state, like database rows, but they are stored on a filesystem. If that filesystem belongs to something your deploy replaces (a container, a PaaS dyno or instance, a release directory), the files are replaced with it. This guide identifies where your uploads were actually stored and moves them somewhere that outlives deploys.
- Print
MEDIA_ROOTandSTORAGES["default"]with production settings to see where uploads go. - On containers and PaaS (Heroku, Render, Railway, Cloud Run, App Platform, ECS), the local filesystem is ephemeral: store media in object storage (S3, R2, Spaces, GCS) with django-storages.
- On Docker hosts, mount a named volume or host directory at
MEDIA_ROOT. - On VPS deploys with release directories, point
MEDIA_ROOTat a shared directory outside the releases. - Recover lost files from backups or old releases if they still exist, and identify orphaned database references.
- Back up media separately from the database.
Step-by-step solution
1. Find where files are stored
python manage.py shell -c "
from django.conf import settings
print('MEDIA_ROOT =', settings.MEDIA_ROOT)
print('default storage =', settings.STORAGES['default'])
"
Then compare with what the deploy replaces:
| Deployment style | What a deploy replaces | Uploads survive if |
|---|---|---|
| Docker / Compose | the container filesystem | MEDIA_ROOT is a mounted volume |
| Kubernetes | the pod filesystem | a PersistentVolume or object storage is used |
| Heroku, Render, Railway, Cloud Run, App Platform | the whole instance | object storage is used (local disk is always ephemeral) |
VPS with releases/<timestamp> and a current symlink | the release directory | MEDIA_ROOT is outside the releases |
VPS with git pull in place | nothing, usually | files are not in Git-tracked paths removed by git clean |
A setting like MEDIA_ROOT = BASE_DIR / "media" puts uploads inside the code directory. With release directories or containers, BASE_DIR is new on every deploy.
2. Move uploads to object storage (containers and PaaS)
pip install "django-storages[s3]"
STORAGES = {
"default": {
"BACKEND": "storages.backends.s3.S3Storage",
"OPTIONS": {
"bucket_name": os.environ["MEDIA_BUCKET"],
"region_name": os.environ.get("AWS_S3_REGION_NAME", "eu-central-1"),
"default_acl": None,
"querystring_auth": True,
"file_overwrite": False,
},
},
"staticfiles": {"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage"},
}
Existing FileField values keep working as long as the object keys match the stored names (uploads/2026/09/photo.jpg). Copy any surviving files into the bucket with the same relative paths:
aws s3 sync /path/to/surviving/media/ s3://myapp-media/ --exclude "*.tmp"
Object storage is the only option that also works with several app instances, because every instance sees the same files.
3. Mount a volume (single Docker host)
services:
web:
volumes:
- media:/app/media
volumes:
media:
Named volumes survive docker compose up -d recreations and image changes. They are lost with docker compose down -v or docker volume rm, so never use -v casually on production. Make sure the non-root container user can write to the volume.
4. Use a shared directory (VPS with release directories)
/srv/myapp/
releases/20260925T101500/
current -> releases/20260925T101500
shared/media/ <- uploads live here
MEDIA_ROOT = os.environ.get("DJANGO_MEDIA_ROOT", "/srv/myapp/shared/media")
Or symlink media inside each release to the shared directory as a deploy step. Point Nginx's /media/ location at the shared path as well:
location /media/ {
alias /srv/myapp/shared/media/;
}
Also check deploy scripts for rm -rf of old releases or git clean -fdx, which delete untracked files, including uploads kept inside the working tree.
5. Recover what you can
- Old release directories or previous container images may still contain the files (for releases on disk, check
releases/*/media). - A stopped (not removed) container still has its filesystem:
docker cp <old-container>:/app/media ./recovered-media. - Restore from media backups if you have them.
Then list database references whose files are missing:
# python manage.py shell
from django.core.files.storage import default_storage
from catalog.models import Product
missing = [p.pk for p in Product.objects.exclude(image="") if not default_storage.exists(p.image.name)]
print(len(missing), missing[:20])
Decide per model whether to clear the field, show a placeholder, or ask users to re-upload.
6. Back up media
Database backups do not include media. Once uploads live in a volume or bucket, add a separate backup: bucket versioning and replication for object storage, or scheduled rsync/snapshot jobs for volumes and shared directories.
Verification check:
- Upload a test file.
- Deploy a new release (or recreate the container).
- Request the file URL again and confirm
200:
curl -sI https://example.com/media/uploads/test/check.txt | head -1
For object storage, check the object exists: aws s3 ls s3://myapp-media/uploads/test/.
Explanation
A FileField stores only a relative name in the database; the bytes live in the configured storage. Deploys are designed to replace code, and on many platforms they replace the whole filesystem along with it. When MEDIA_ROOT sits inside that replaceable area, each deploy silently discards every upload while the database keeps pointing at them. Putting media in storage whose lifecycle is independent of deploys (object storage, a volume, or a shared directory) fixes it, and object storage additionally makes horizontal scaling possible.
Edge cases / notes
Serving private files
Moving to object storage changes how files are served. Public files can use a CDN or public bucket URLs; private files need signed URLs (querystring_auth=True) or a permission-checking view.
Temporary files
Large uploads are streamed to temporary files before being saved. FILE_UPLOAD_TEMP_DIR can stay on local disk; only the final storage location must be persistent.
When to automate this
Add a post-deploy check that requests a known test file from media storage. Keep storage configuration (bucket, volume, shared directory) in infrastructure code so a new environment cannot be created with media on an ephemeral disk.
Next steps
- Configure object storage with How to Store Django Media Files on Amazon S3.
- Understand the difference with Django Static vs Media Files in Production.
- Protect uploads with Back Up Django Media Files.
- If files exist but are not served, see Fix Django Media Files Not Serving (Uploads Broken in Production).
FAQ
Why do my Django uploads disappear after deploying?
They were saved to a filesystem the deploy replaces, such as a container, a PaaS instance, or a release directory. Store media in object storage, a volume, or a shared directory.
Can I store Django media files on Heroku or Render's disk?
Not persistently. Their filesystems are ephemeral. Use object storage.
Do Docker named volumes survive a redeploy?
Yes, across container recreation and image changes. They are deleted by docker compose down -v or docker volume rm.
Do database backups include uploaded files?
No. The database stores only file names. Back up media storage separately.