Deployment
#django
#aws-s3
#django-storages

How to Store Django Media Files on Amazon S3

Local disk storage for Django media files works in development, but it breaks quickly in production.

Problem statement

Local disk storage for Django media files works in development, but it breaks quickly in production.

If your app stores user uploads on the application server filesystem, you will run into problems such as:

  • uploads disappearing after container replacement
  • missing files after deploying to a new server
  • inconsistent media across multiple app instances
  • operational inconsistencies during rollback because uploaded files are not versioned with your application code
  • no simple way to share media between web and worker processes

This is especially common with Docker, autoscaling, platform-managed deployments, and multi-server setups. User uploads need persistent storage outside the app container or VM. For many Django deployments, Amazon S3 is a practical production-ready option.

Quick answer

To store Django media files on Amazon S3 in production:

  1. Create a dedicated S3 bucket for media.
  2. Keep media storage separate from static files.
  3. Install django-storages and boto3.
  4. Configure Django to use S3 for media storage.
  5. Give the app least-privilege IAM access to the bucket.
  6. Migrate existing media files before switching production writes.
  7. Verify uploads, reads, and old file paths after deployment.

For the safest default, keep the bucket private and use signed access rather than exposing raw public S3 object URLs.

Step-by-step solution

1) Decide what should go to S3

Media files vs static files in Django

In Django:

  • static files are build-time assets like CSS, JS, and images shipped with your code
  • media files are user-uploaded files stored through FileField or ImageField

Do not mix them in the same storage path. You can use S3 for both, but configure them separately.

Why user uploads should not depend on local server disk

User uploads must survive:

  • redeploys
  • restarts
  • scaling to multiple app instances
  • instance replacement
  • storage moves between hosts

S3 gives you durable object storage without requiring a shared filesystem.

When S3 is the right choice

S3 is usually the right choice when you run Django in containers, on multiple servers, or on any platform with ephemeral local storage. Local disk or shared volumes can still be acceptable for small single-server deployments, but they are harder to scale, back up, and recover.

2) Create and prepare the S3 bucket

Create a bucket in the AWS region closest to your app.

Recommended bucket setup:

  • bucket name dedicated to one environment, such as myapp-prod-media
  • Block Public Access enabled by default
  • Versioning enabled
  • Default encryption enabled
  • optional prefix such as media/

If files are intended to be public, expose them later with an explicit policy decision. Start private unless you are sure public access is correct.

Verification checks:

  • confirm bucket exists in the intended region
  • confirm versioning is enabled
  • confirm encryption is enabled
  • confirm public access block matches your intended access model

3) Create IAM credentials with least privilege

Use an IAM role if your app runs on AWS infrastructure that supports instance or task roles. Otherwise use a dedicated IAM user for the Django app.

Do not use root credentials.

Example policy scoped to one bucket and the media/ prefix:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ListMediaPrefix",
      "Effect": "Allow",
      "Action": ["s3:ListBucket"],
      "Resource": "arn:aws:s3:::myapp-prod-media",
      "Condition": {
        "StringLike": {
          "s3:prefix": ["media/*"]
        }
      }
    },
    {
      "Sid": "MediaObjects",
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject",
        "s3:AbortMultipartUpload"
      ],
      "Resource": "arn:aws:s3:::myapp-prod-media/media/*"
    }
  ]
}

For larger uploads, multipart-related permissions may also be required depending on your upload path and client behavior.

Store credentials outside the repository:

AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...
AWS_STORAGE_BUCKET_NAME=myapp-prod-media
AWS_S3_REGION_NAME=us-east-1

If you use IAM roles, you usually do not need to set access keys in Django environment variables.

4) Install Django S3 storage dependencies

Install the required packages in the same environment used by production:

pip install django-storages boto3

Example requirements.txt entry:

django-storages>=1.14,<2.0
boto3>=1.34,<2.0

Add storages to INSTALLED_APPS:

INSTALLED_APPS = [
    # ...
    "storages",
]

Verification check:

python -c "import storages, boto3; print('ok')"

5) Configure Django media storage for Amazon S3

For Django 4.2+, use STORAGES.

import os

AWS_STORAGE_BUCKET_NAME = os.environ["AWS_STORAGE_BUCKET_NAME"]
AWS_S3_REGION_NAME = os.environ["AWS_S3_REGION_NAME"]

AWS_DEFAULT_ACL = None
AWS_S3_FILE_OVERWRITE = False
AWS_LOCATION = "media"

# Keep media private by default. Do not hardcode a public MEDIA_URL unless
# you intentionally serve public objects.
STORAGES = {
    "default": {
        "BACKEND": "storages.backends.s3.S3Storage",
        "OPTIONS": {
            "bucket_name": AWS_STORAGE_BUCKET_NAME,
            "region_name": AWS_S3_REGION_NAME,
            "default_acl": AWS_DEFAULT_ACL,
            "file_overwrite": AWS_S3_FILE_OVERWRITE,
            "location": AWS_LOCATION,
            "querystring_auth": True,
        },
    },
    "staticfiles": {
        "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
    },
}

If you are on older Django versions, the media storage setting is typically:

DEFAULT_FILE_STORAGE = "storages.backends.s3.S3Storage"

Keep static storage separate. Do not point both static and media at the same path unless you have explicitly designed that layout.

If media should remain private, do not set a public MEDIA_URL to raw S3 objects. Use the S3 storage backend with signed URLs (querystring_auth=True) or generate presigned or authenticated download URLs in application code.

Optional public-media variant

If your media files are intentionally public, configure that explicitly rather than using it as the default model.

Example public-media settings:

import os

AWS_STORAGE_BUCKET_NAME = os.environ["AWS_STORAGE_BUCKET_NAME"]
AWS_S3_REGION_NAME = os.environ["AWS_S3_REGION_NAME"]

AWS_DEFAULT_ACL = None
AWS_S3_FILE_OVERWRITE = False
AWS_LOCATION = "media"

STORAGES = {
    "default": {
        "BACKEND": "storages.backends.s3.S3Storage",
        "OPTIONS": {
            "bucket_name": AWS_STORAGE_BUCKET_NAME,
            "region_name": AWS_S3_REGION_NAME,
            "default_acl": AWS_DEFAULT_ACL,
            "file_overwrite": AWS_S3_FILE_OVERWRITE,
            "location": AWS_LOCATION,
            "querystring_auth": False,
        },
    },
    "staticfiles": {
        "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
    },
}

If you choose this model, make sure your bucket policy and public access settings are intentionally configured for public reads.

6) Secure the media storage configuration

Recommended settings and practices:

  • AWS_DEFAULT_ACL = None to avoid relying on legacy ACL behavior
  • AWS_S3_FILE_OVERWRITE = False to reduce accidental filename collisions
  • validate file size and content type in Django forms, serializers, or views
  • keep access keys in secret storage or use IAM roles
  • keep bucket public access blocked unless you intentionally serve public media

If uploads contain private or sensitive files, keep the bucket private and use presigned URLs or an authenticated download view. Do not make the whole bucket public just to simplify file delivery.

7) Migrate existing media files to S3

Audit where your current media files live, usually under MEDIA_ROOT.

Dry-run the sync first:

aws s3 sync /path/to/current/media s3://myapp-prod-media/media/ --dryrun

If the output looks correct, run the real sync:

aws s3 sync /path/to/current/media s3://myapp-prod-media/media/

Verify objects were copied:

aws s3 ls s3://myapp-prod-media/media/ --recursive | head

Cutover notes:

  • copy existing files before enabling S3-backed writes
  • avoid deployment during high upload activity if possible
  • if uploads continue during migration, require either a short upload freeze during cutover or a final sync pass immediately before switching writes to S3

Rollback safety:

  • keep the original local media directory intact until verification is complete
  • do not delete local files immediately after cutover
  • remember that files uploaded after S3 cutover may exist only in S3; a rollback to local storage can orphan those uploads unless you freeze uploads or sync them back

8) Deploy the configuration safely

Update environment variables in your deployment system, then restart Django processes.

Typical sequence:

  1. add bucket and credential settings
  2. deploy code with S3 storage config
  3. restart web and worker processes
  4. test uploads and reads
  5. check logs

Smoke tests:

  • upload a file through Django admin or your app UI
  • open the returned file URL
  • confirm the object appears in the expected S3 prefix
  • confirm an older uploaded file still resolves

You can also test from the Django shell:

python manage.py shell
from django.core.files.base import ContentFile
from myapp.models import MyModel

obj = MyModel.objects.create()
obj.file.save("s3-test.txt", ContentFile(b"storage test"), save=True)

print(obj.file.name)
print(obj.file.url)

Then verify the object exists in S3:

aws s3api head-object --bucket myapp-prod-media --key media/s3-test.txt

If your model requires other fields, use a model that already contains a file field and can be created safely in your environment.

If URLs or access fail, stop here and fix configuration before continuing traffic changes.

9) Validate the production setup

After deployment, verify all of the following:

  • new uploads succeed
  • uploaded objects appear under media/
  • generated URLs match your intended access model
  • old database records still resolve to valid files
  • application logs show no AccessDenied, credential, or region errors

Common AWS CLI check:

aws s3api head-object --bucket myapp-prod-media --key media/example.jpg

If this fails for a known uploaded object, check IAM policy scope, region, and key path.

10) Rollback and recovery plan

If new uploads fail after deployment:

  1. revert Django storage settings to the previous backend
  2. revert related environment variable changes
  3. redeploy and restart application processes
  4. continue serving existing local media while investigating

Before rolling back, decide what to do with files uploaded after S3 cutover. Those files may exist only in S3. If you switch back to local storage without an upload freeze or reverse sync plan, users may lose access to newly uploaded files.

Keep a backup of local media until S3 storage has been stable in production.

If S3 versioning is enabled, you can recover from accidental overwrite or deletion from the bucket history. That does not replace application-level rollback, but it helps recover objects.

Explanation

This setup works because Django stops treating the app server filesystem as the source of truth for user uploads. Instead, uploaded files are stored in object storage that all app instances can access consistently.

For most production deployments, S3 is a better fit than local disk because it survives instance replacement and scales without shared filesystem management. The tradeoff is that object storage configuration, IAM, and access patterns must be correct. That is why least-privilege IAM, explicit media prefixes, and post-deploy verification matter.

Use alternatives such as shared volumes only when you have a clear operational reason and understand the scaling and recovery limits.

Edge cases / notes

  • Static and media mixing: keep them separate in settings and bucket paths.
  • Wrong region: a mismatched AWS_S3_REGION_NAME can cause failed requests or bad URLs.
  • Access denied on file URL: your bucket policy, object access model, or signed URL configuration does not match your intended setup.
  • Filename collisions: without AWS_S3_FILE_OVERWRITE = False, uploads with the same name may replace earlier files.
  • Existing local files missing: switching storage does not automatically copy old uploads.
  • Direct browser uploads: if you later move to browser-to-S3 uploads, you will also need CORS, signed upload policies, and a different validation flow.

When manual setup becomes repetitive

If you repeat this setup across projects, the same pieces are usually duplicated: bucket config, IAM policy, Django settings, environment variables, and migration checks. Those are good candidates for reusable templates or deployment scripts. The manual setup is still worth understanding first so you can verify what automation is doing.

If you need the underlying distinction first, read Django static files vs media files in production.

Related implementation guides:

For broader production rollout checks, see Django Deployment Checklist for Production.

If uploads fail after deployment, see Why Django file uploads fail in production and how to fix them.

FAQ

Should Django static files and media files use the same S3 bucket?

They can, but separate buckets or at least separate prefixes are usually safer. The important rule is not to mix static and media into the same storage path or policy model.

Do I need public bucket access for Django media files?

No. Public access is only needed if you want public object URLs. For private files, keep the bucket private and use signed URLs or authenticated download views.

How do I move existing media files to S3 without breaking uploads?

Sync existing files first, verify object paths, then deploy the new storage settings. If uploads are active during migration, use a short freeze window or run a final sync pass immediately before switching writes.

Can I use S3 with Docker-based Django deployments?

Yes. This is one of the most common reasons to use S3 for media. Containers should not be trusted as persistent media storage.

What should come from IAM roles instead of environment variables?

If your app runs on AWS infrastructure that supports roles, prefer IAM roles for S3 access and avoid long-lived access keys. Keep non-secret values like bucket name and region in environment variables.

2026 ยท django-deployment.com - Django Deployment knowledge base