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.

Where uploads get lostUpload savedMEDIA_ROOT pathInside releasecontainer or releaseRedeploynew filesystem404DB row, no fileThe row survives the deploy; the file survives only if MEDIA_ROOT is outside what gets replaced.
  1. Print MEDIA_ROOT and STORAGES["default"] with production settings to see where uploads go.
  2. 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.
  3. On Docker hosts, mount a named volume or host directory at MEDIA_ROOT.
  4. On VPS deploys with release directories, point MEDIA_ROOT at a shared directory outside the releases.
  5. Recover lost files from backups or old releases if they still exist, and identify orphaned database references.
  6. 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'])
"
where uploads should liveHow is the app deployed?containers or PaaSObject storageS3-compatible bucketone Docker hostNamed volumemounted at MEDIA_ROOTVPS with releasesShared directoryoutside every releaseWhatever the choice, MEDIA_ROOT must not be inside replaceable codedjango-deployment.com
Object storage also works for the other two, and scales further.

Then compare with what the deploy replaces:

Deployment styleWhat a deploy replacesUploads survive if
Docker / Composethe container filesystemMEDIA_ROOT is a mounted volume
Kubernetesthe pod filesystema PersistentVolume or object storage is used
Heroku, Render, Railway, Cloud Run, App Platformthe whole instanceobject storage is used (local disk is always ephemeral)
VPS with releases/<timestamp> and a current symlinkthe release directoryMEDIA_ROOT is outside the releases
VPS with git pull in placenothing, usuallyfiles 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
uploads trapped in an old releaseRelease A liveuploads in A/mediaDeploy release Bfresh B/media, emptycurrent → Bold uploads look lostOld releases prunedA deletedFixshared/media symlinked inBefore pruning, the files are still in the old release directorydjango-deployment.com
Recover from old release directories before they are pruned.
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.

media backups that restoreNightly sync off the serveror bucket versioningSame schedule as the databaserows point at filesRetention matches the DB backupsrestorable togetherRestore testedfiles open from the appA database restore without matching media gives broken linksdjango-deployment.com
Versioning with a lifecycle rule is the simplest backup for buckets.

Verification check:

  1. Upload a test file.
  2. Deploy a new release (or recreate the container).
  3. 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

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.

Keep going