Django Storages S3
Senior Django specialist for production-grade file storage on AWS S3 via django-storages and boto3 — public and private media, static files, presigned URLs, and CloudFront.
When to Use This Skill
- Serving static and/or media files from AWS S3 instead of the local filesystem
- Configuring the Django 4.2+
STORAGES dict or legacy DEFAULT_FILE_STORAGE
- Separating public (CDN-served) and private (presigned) file backends
- Generating presigned download or direct browser-to-S3 upload URLs
- Fronting S3 with CloudFront and writing a least-privilege IAM policy
- Migrating local
FileField/ImageField storage to S3 without code changes
- Testing storage code without hitting S3
Core Workflow
- Install & register —
pip install django-storages[s3] boto3; add "storages" to INSTALLED_APPS
- Configure credentials — Load from env vars or rely on an attached IAM role; never hardcode
- Wire the
STORAGES dict — Set default (media) and staticfiles backends with separate location prefixes
- Add named backends — Split public vs. private buckets/ACLs as additional
STORAGES entries when needed
- Verify & test — Run
collectstatic, confirm uploads land in S3, and mock S3 in tests with InMemoryStorage or moto
Reference Guide
Load detailed guidance based on context:
| Topic |
Reference |
Load When |
| Settings & STORAGES |
references/configuration.md |
Core settings, 4.2+ vs legacy, CloudFront |
| Custom backends |
references/custom-backends.md |
Public vs. private buckets, per-field storage |
| Presigned URLs |
references/presigned-urls.md |
Download links, direct browser uploads |
| Testing & IAM |
references/testing-storages.md |
Mocking S3, IAM policy, common pitfalls |
Minimal Working Example
The snippet below demonstrates the core MUST DO constraints: env-loaded credentials, STORAGES dict, separate media/static locations, and default_acl=None on the media backend.
# settings.py
import os
AWS_STORAGE_BUCKET_NAME = os.environ["AWS_STORAGE_BUCKET_NAME"]
AWS_S3_REGION_NAME = os.environ.get("AWS_S3_REGION_NAME", "us-east-1")
AWS_S3_CUSTOM_DOMAIN = f"{AWS_STORAGE_BUCKET_NAME}.s3.{AWS_S3_REGION_NAME}.amazonaws.com"
# On EC2/ECS/Lambda, omit keys entirely — boto3 uses the attached IAM role.
STORAGES = {
"default": { # media uploads
"BACKEND": "storages.backends.s3boto3.S3Boto3Storage",
"OPTIONS": {
"bucket_name": AWS_STORAGE_BUCKET_NAME,
"location": "media",
"default_acl": None, # rely on bucket policy, not per-object ACLs
"file_overwrite": False,
"querystring_auth": False, # public objects → clean URLs
},
},
"staticfiles": {
"BACKEND": "storages.backends.s3boto3.S3StaticStorage",
"OPTIONS": {
"bucket_name": AWS_STORAGE_BUCKET_NAME,
"location": "static",
},
},
}
MEDIA_URL = f"https://{AWS_S3_CUSTOM_DOMAIN}/media/"
STATIC_URL = f"https://{AWS_S3_CUSTOM_DOMAIN}/static/"
# models.py — uploads go straight to S3 on save()
from django.db import models
class Document(models.Model):
file = models.FileField(upload_to="docs/") # uses STORAGES["default"]
Auditing an Existing Configuration
When reviewing a project that already uses S3 (not greenfield), walk this
checklist — each item is a constraint below rephrased as "find X, confirm Y":
- Credentials —
grep -rn "AWS_SECRET_ACCESS_KEY\|aws_secret" settings/ → confirm values come from os.environ/django-environ or an IAM role, never literals committed to the repo.
- ACLs —
grep -rn "default_acl\|AWS_DEFAULT_ACL" . → on buckets created after April 2023, every value must be None. Any "public-read"/"private" will raise AccessControlListNotSupported; public access belongs in a bucket policy.
- Storage backend — confirm Django 4.2+ uses the
STORAGES dict, not DEFAULT_FILE_STORAGE/STATICFILES_STORAGE (removed in Django 5.1, so silently ignored on 5.1/5.2/6.0); confirm the static class is S3StaticStorage, not a fabricated name.
- Locations — confirm
default (media) and staticfiles have distinct location prefixes or buckets so collectstatic never collides with uploads.
- Region — confirm
region_name (or the global AWS_S3_REGION_NAME) matches the bucket's real region and that AWS_S3_CUSTOM_DOMAIN includes the region segment for non-us-east-1 buckets.
- Presigning — for private backends, confirm
querystring_auth=True and custom_domain=None; confirm presigned .url() results aren't cached past AWS_QUERYSTRING_EXPIRE.
- Overwrite cleanup — where
file_overwrite=False, confirm replaced files are explicitly deleted (otherwise superseded objects leak).
- IAM — confirm the policy grants only
Get/Put/Delete/ListBucket on the bucket ARN, not broader S3 access.
Constraints
MUST DO
- Load AWS credentials from environment variables or an attached IAM role
- Set
default_acl=None so bucket policies (not object ACLs) control access
- Give static and media files separate
location prefixes or separate buckets
- Use the
STORAGES dict on Django 4.2+ (same config through 5.2 LTS and 6.0); DEFAULT_FILE_STORAGE/STATICFILES_STORAGE were removed in 5.1, so reserve them for < 4.2 only
- Set
custom_domain=None on any backend that issues presigned URLs
- Mock S3 (
InMemoryStorage or moto) in tests instead of hitting real buckets
MUST NOT DO
- Hardcode
AWS_SECRET_ACCESS_KEY in settings.py or commit it
- Mix
querystring_auth=True with a custom_domain (presigning breaks)
- Mix static and media files under the same prefix
- Grant the IAM user broader than
Get/Put/Delete/ListBucket on the bucket ARN
- Rely on per-object ACLs on buckets created after April 2023 (ACLs disabled by default)
Knowledge Reference
django-storages, S3Boto3Storage, S3StaticStorage, boto3, STORAGES dict, presigned URLs, generate_presigned_post, CloudFront, IAM policy, InMemoryStorage, moto
Related Skills
django-expert — core Django models, DRF, and ORM that produce the files this skill persists to S3
fullstack-guardian — secure end-to-end upload flows and access control around stored files
devops-engineer — provisioning the S3 buckets, IAM roles, and CloudFront distributions this skill targets
Documentation
1---2name: django-storages-s33description: Use when configuring Django to store static and media files on AWS S3 with django-storages. Invoke when working with the STORAGES setting, S3 buckets, presigned URLs, CloudFront, or boto3-backed file storage in settings.py. Configures the Django 4.2+ STORAGES dict, public/private custom backends, presigned GET/POST URLs, IAM policies, and S3 mocking for tests. Trigger terms: django-storages, S3, boto3, S3Boto3Storage, STORAGES, presigned URL, CloudFront, media files, collectstatic, AWS_STORAGE_BUCKET_NAME.4license: MIT5---6
7# Django Storages S3
8
9Senior Django specialist for production-grade file storage on AWS S3 via `django-storages` and `boto3` — public and private media, static files, presigned URLs, and CloudFront.
10
11## When to Use This Skill
12
13- Serving static and/or media files from AWS S3 instead of the local filesystem
14- Configuring the Django 4.2+ `STORAGES` dict or legacy `DEFAULT_FILE_STORAGE`
15- Separating public (CDN-served) and private (presigned) file backends
16- Generating presigned download or direct browser-to-S3 upload URLs
17- Fronting S3 with CloudFront and writing a least-privilege IAM policy
18- Migrating local `FileField`/`ImageField` storage to S3 without code changes
19- Testing storage code without hitting S3
20
21## Core Workflow
22
231. **Install & register** — `pip install django-storages[s3] boto3`; add `"storages"` to `INSTALLED_APPS`
242. **Configure credentials** — Load from env vars or rely on an attached IAM role; never hardcode
253. **Wire the `STORAGES` dict** — Set `default` (media) and `staticfiles` backends with separate `location` prefixes
264. **Add named backends** — Split public vs. private buckets/ACLs as additional `STORAGES` entries when needed
275. **Verify & test** — Run `collectstatic`, confirm uploads land in S3, and mock S3 in tests with `InMemoryStorage` or `moto`
28
29## Reference Guide
30
31Load detailed guidance based on context:
32
33| Topic | Reference | Load When |
34|-------|-----------|-----------|
35| Settings & STORAGES | `references/configuration.md` | Core settings, 4.2+ vs legacy, CloudFront |
36| Custom backends | `references/custom-backends.md` | Public vs. private buckets, per-field storage |
37| Presigned URLs | `references/presigned-urls.md` | Download links, direct browser uploads |
38| Testing & IAM | `references/testing-storages.md` | Mocking S3, IAM policy, common pitfalls |
39
40## Minimal Working Example
41
42The snippet below demonstrates the core MUST DO constraints: env-loaded credentials, `STORAGES` dict, separate media/static locations, and `default_acl=None` on the media backend.
43
44```python
45# settings.py
46import os
47
48AWS_STORAGE_BUCKET_NAME = os.environ["AWS_STORAGE_BUCKET_NAME"]
49AWS_S3_REGION_NAME = os.environ.get("AWS_S3_REGION_NAME", "us-east-1")
50AWS_S3_CUSTOM_DOMAIN = f"{AWS_STORAGE_BUCKET_NAME}.s3.{AWS_S3_REGION_NAME}.amazonaws.com"
51# On EC2/ECS/Lambda, omit keys entirely — boto3 uses the attached IAM role.
52
53STORAGES = {
54 "default": { # media uploads
55 "BACKEND": "storages.backends.s3boto3.S3Boto3Storage",
56 "OPTIONS": {
57 "bucket_name": AWS_STORAGE_BUCKET_NAME,
58 "location": "media",
59 "default_acl": None, # rely on bucket policy, not per-object ACLs
60 "file_overwrite": False,
61 "querystring_auth": False, # public objects → clean URLs
62 },
63 },
64 "staticfiles": {
65 "BACKEND": "storages.backends.s3boto3.S3StaticStorage",
66 "OPTIONS": {
67 "bucket_name": AWS_STORAGE_BUCKET_NAME,
68 "location": "static",
69 },
70 },
71}
72
73MEDIA_URL = f"https://{AWS_S3_CUSTOM_DOMAIN}/media/"
74STATIC_URL = f"https://{AWS_S3_CUSTOM_DOMAIN}/static/"
75```
76
77```python
78# models.py — uploads go straight to S3 on save()
79from django.db import models
80
81class Document(models.Model):
82 file = models.FileField(upload_to="docs/") # uses STORAGES["default"]
83```
84
85## Auditing an Existing Configuration
86
87When reviewing a project that already uses S3 (not greenfield), walk this
88checklist — each item is a constraint below rephrased as "find X, confirm Y":
89
901. **Credentials** — `grep -rn "AWS_SECRET_ACCESS_KEY\|aws_secret" settings/` → confirm values come from `os.environ`/`django-environ` or an IAM role, never literals committed to the repo.
912. **ACLs** — `grep -rn "default_acl\|AWS_DEFAULT_ACL" .` → on buckets created after April 2023, every value must be `None`. Any `"public-read"`/`"private"` will raise `AccessControlListNotSupported`; public access belongs in a bucket policy.
923. **Storage backend** — confirm Django 4.2+ uses the `STORAGES` dict, not `DEFAULT_FILE_STORAGE`/`STATICFILES_STORAGE` (removed in Django 5.1, so silently ignored on 5.1/5.2/6.0); confirm the static class is `S3StaticStorage`, not a fabricated name.
934. **Locations** — confirm `default` (media) and `staticfiles` have distinct `location` prefixes or buckets so `collectstatic` never collides with uploads.
945. **Region** — confirm `region_name` (or the global `AWS_S3_REGION_NAME`) matches the bucket's real region and that `AWS_S3_CUSTOM_DOMAIN` includes the region segment for non-`us-east-1` buckets.
956. **Presigning** — for private backends, confirm `querystring_auth=True` **and** `custom_domain=None`; confirm presigned `.url()` results aren't cached past `AWS_QUERYSTRING_EXPIRE`.
967. **Overwrite cleanup** — where `file_overwrite=False`, confirm replaced files are explicitly deleted (otherwise superseded objects leak).
978. **IAM** — confirm the policy grants only `Get/Put/Delete/ListBucket` on the bucket ARN, not broader S3 access.
98
99## Constraints
100
101### MUST DO
102- Load AWS credentials from environment variables or an attached IAM role
103- Set `default_acl=None` so bucket policies (not object ACLs) control access
104- Give static and media files separate `location` prefixes or separate buckets
105- Use the `STORAGES` dict on Django 4.2+ (same config through 5.2 LTS and 6.0); `DEFAULT_FILE_STORAGE`/`STATICFILES_STORAGE` were removed in 5.1, so reserve them for < 4.2 only
106- Set `custom_domain=None` on any backend that issues presigned URLs
107- Mock S3 (`InMemoryStorage` or `moto`) in tests instead of hitting real buckets
108
109### MUST NOT DO
110- Hardcode `AWS_SECRET_ACCESS_KEY` in `settings.py` or commit it
111- Mix `querystring_auth=True` with a `custom_domain` (presigning breaks)
112- Mix static and media files under the same prefix
113- Grant the IAM user broader than `Get/Put/Delete/ListBucket` on the bucket ARN
114- Rely on per-object ACLs on buckets created after April 2023 (ACLs disabled by default)
115
116## Knowledge Reference
117
118django-storages, S3Boto3Storage, S3StaticStorage, boto3, STORAGES dict, presigned URLs, generate_presigned_post, CloudFront, IAM policy, InMemoryStorage, moto
119
120## Related Skills
121
122- `django-expert` — core Django models, DRF, and ORM that produce the files this skill persists to S3
123- `fullstack-guardian` — secure end-to-end upload flows and access control around stored files
124- `devops-engineer` — provisioning the S3 buckets, IAM roles, and CloudFront distributions this skill targets
125
126[Documentation](https://jeffallan.github.io/claude-skills/skills/backend/django-storages-s3/)