Static and Media Files in Production
Introduction
Chapter Templates and Static Files covered {% static %} in development. Production adds user uploads (media), collectstatic to gather assets for deployment, and Nginx (or WhiteNoise) to serve files efficiently. This chapter configures STATIC_* and MEDIA_*, handles file uploads safely, and wires a minimal Nginx config.
Prerequisites
- Templates and Static Files
- Settings
- Forms and CSRF — file upload forms
- Optional: Nginx static files
Static vs Media
| Type | Source | Served in prod by |
|---|---|---|
| Static | Your CSS/JS/images in repo | collectstatic → Nginx or WhiteNoise |
| Media | User uploads at runtime | Nginx alias to MEDIA_ROOT |
Never commit MEDIA_ROOT user files to Git—add to .gitignore.
Development Settings
config/settings/base.py:
STATIC_URL = "static/"
STATICFILES_DIRS = [
BASE_DIR / "static",
]
STATIC_ROOT = BASE_DIR / "staticfiles"
MEDIA_URL = "media/"
MEDIA_ROOT = BASE_DIR / "media"Layout:
myproject/
├── static/
│ └── css/site.css
├── media/ # created at runtime
└── blog/static/blog/ # app-level static (optional)blog/static/blog/css/style.css is found automatically via django.contrib.staticfiles.
Serve Media in Development Only
config/urls.py:
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path("admin/", admin.site.urls),
path("", include("blog.urls")),
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)Warning
Never Use Django to Serve Media in Production
static() helper is for local dev. Production must serve MEDIA_ROOT via Nginx or object storage (S3)—Django workers are not a CDN.
File Upload Model and Form
blog/models.py:
def upload_to_post_image(instance, filename):
return f"posts/{instance.pk or 'new'}/{filename}"
class Post(models.Model):
...
cover = models.ImageField(upload_to=upload_to_post_image, blank=True)Install Pillow:
pip install Pillowblog/forms.py:
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ["title", "slug", "body", "cover"]Template:
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Save</button>
</form>View must not parse files manually—request.FILES is bound into ModelForm(request.POST, request.FILES).
Validate uploads in production:
- Max size in settings or form
clean_cover - Allow only image MIME types
- Scan with antivirus in high-security environments
collectstatic
Before deploy, gather all static files into STATIC_ROOT:
python manage.py collectstatic --noinputOutput lands in staticfiles/—deploy that directory to the server or bake into Docker image.
ManifestStaticFilesStorage (optional) hashes filenames for cache busting:
STORAGES = {
"staticfiles": {
"BACKEND": "django.contrib.staticfiles.storage.ManifestStaticFilesStorage",
},
}Django 4.2+ uses STORAGES dict; older projects used STATICFILES_STORAGE.
WhiteNoise (Small Projects)
Serve static from the same Gunicorn process without Nginx for static only:
pip install whitenoiseMIDDLEWARE — insert after SecurityMiddleware:
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
...
]settings/production.py:
STORAGES = {
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
}Media uploads still need Nginx or S3—WhiteNoise is for static only.
Nginx: Static + Media + Gunicorn
Minimal server block (adapt paths):
Run collectstatic on the server after each deploy. Ensure Nginx user can read staticfiles/ and write is not needed for static.
Full reverse-proxy patterns — Nginx reverse proxy and Django deployment.
Object Storage (Awareness)
At scale, store media on S3 with django-storages—Django saves the URL; files never touch local disk. Same pattern for CDN-backed static.
Post-Chapter Checklist
-
STATIC_URL,STATIC_ROOT,MEDIA_URL,MEDIA_ROOTdefined - Dev
urls.pyserves media only whenDEBUG - Upload form uses
enctype="multipart/form-data" - You ran
collectstaticand know where files land
FAQ
Static file 404 in production?
Forgot collectstatic, wrong alias path, or permissions.
Duplicate static in app and project?
staticfiles finders merge STATICFILES_DIRS and app static/ folders.
Serve user avatars?
ImageField on User profile model + Nginx /media/ alias.
DEBUG=True static works, prod broken?
Dev uses runserver static finder; prod needs collectstatic + Nginx/WhiteNoise.
Compare Flask?
Flask send_from_directory for uploads in dev; prod same Nginx pattern — Flask file uploads.