Django Static vs Media Files in Production
Many Django deployments break because static files and media files are treated as the same thing.
Problem statement
Many Django deployments break because static files and media files are treated as the same thing. In development, Django can make both seem simple. In production, they have different lifecycles, permissions, storage rules, caching behavior, and backup requirements.
If you mix them together, common failures follow:
- CSS and JavaScript do not load after deploy
- user uploads disappear on release cleanup
collectstaticoverwrites or collides with uploaded files- containers lose uploads on restart
- reverse proxy rules expose files incorrectly
- app processes get unnecessary write access to asset directories
A safe production setup separates build-time assets from runtime user data.
Quick answer
For Django static vs media files in production, the rule is simple:
- Static files are application assets such as CSS, JavaScript, Django admin assets, and bundled frontend files. They are collected during deploy with
collectstatic, served read-only, and can be rebuilt from source. - Media files are user-uploaded or runtime-generated files. They must be stored separately, remain writable by the app where needed, survive redeploys, and be included in backup and recovery plans.
In normal production traffic, Django should not serve either directly. Use Nginx, Caddy, object storage, or another dedicated file-serving path.
Step-by-step solution
1. Define static and media separately in Django settings
Use distinct URLs and filesystem paths.
# settings.py
from pathlib import Path
import os
BASE_DIR = Path(__file__).resolve().parent.parent
STATIC_URL = "/static/"
STATIC_ROOT = os.environ.get("DJANGO_STATIC_ROOT", "/var/www/myapp/shared/static")
MEDIA_URL = "/media/"
MEDIA_ROOT = os.environ.get("DJANGO_MEDIA_ROOT", "/var/www/myapp/shared/media")
Production rules:
STATIC_ROOTis the output directory forcollectstaticMEDIA_ROOTis the runtime storage directory for uploads- these paths must not overlap
Verification:
python manage.py shell -c "from django.conf import settings; print(settings.STATIC_ROOT); print(settings.MEDIA_ROOT)"
Rollback note: if you already use one shared directory for both, separate them before the next deploy. Do not move files blindly in production without backing up current uploads first.
2. Use a directory layout that survives releases
A common non-Docker Linux layout looks like this:
/var/www/myapp/releases/2026-04-24/
/var/www/myapp/shared/static/
/var/www/myapp/shared/media/
Recommended pattern:
- release code lives under
releases/... - static files are collected into a shared static directory or a release-specific static directory that your proxy points to
- media lives in a shared persistent directory outside release cleanup
If you use symlink-based releases, make sure deploy cleanup only removes old release directories, not shared/media.
Verification:
ls -lah /var/www/myapp/shared/static/
ls -lah /var/www/myapp/shared/media/
3. Run collectstatic during deploy, not ad hoc
Static assets should be built as part of the release process.
python manage.py collectstatic --noinput
Run this in CI, a release script, or a controlled deploy step after code is in place and before switching traffic.
Why:
- static output must match the current app version
- admin assets and frontend bundles need a predictable location
- you avoid manual production drift
Verification:
ls -lah /var/www/myapp/shared/static/
find /var/www/myapp/shared/static/admin -maxdepth 2 | head
Browser or HTTP check:
curl -I https://example.com/static/admin/css/base.css
You should get 200 OK.
Rollback note: if a release changes asset names or manifest output, keep the previous release available until the new static files are verified. If needed, switch traffic back to the previous app version and previous static mapping together.
4. Keep media writable and persistent
Media files are runtime data. The application may need write access to MEDIA_ROOT, but it should not need write access to STATIC_ROOT.
Example permission approach:
- app user can write to
/var/www/myapp/shared/media - app user cannot modify static files after deploy if you can avoid it
Check path permissions:
namei -l /var/www/myapp/shared/static
namei -l /var/www/myapp/shared/media
Upload a test file through the application, then verify it exists:
ls -lah /var/www/myapp/shared/media/
# Replace with the real uploaded file URL returned by your app
curl -I https://example.com/media/<actual-upload-path>
Restart or redeploy the app and confirm the file still exists. If it disappears, your media storage is tied to the release or container filesystem.
5. Configure Nginx to serve static and media separately
In production, the reverse proxy should usually serve both file types directly.
This example shows only file and proxy routing. In production, serve the site over HTTPS, either in this Nginx config or with TLS terminated by a load balancer or CDN.
server {
listen 80;
server_name example.com;
location /static/ {
alias /var/www/myapp/shared/static/;
expires 1h;
add_header Cache-Control "public";
access_log off;
}
location /media/ {
alias /var/www/myapp/shared/media/;
expires 1h;
add_header Cache-Control "public";
}
location / {
proxy_pass http://unix:/run/gunicorn-myapp.sock;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Important details:
aliaspath should end with/when mapping directory locations- static and media locations must point to different directories
- do not accidentally proxy
/static/or/media/to Gunicorn unless you intentionally need protected media - use
immutablecache headers only when your static filenames are versioned or hashed, such as with Django manifest-based static file storage
Test config and reload:
sudo nginx -t
sudo systemctl reload nginx
Verification:
curl -I https://example.com/static/admin/css/base.css
curl -I https://example.com/media/<actual-upload-path>
If TLS is terminated upstream, make sure your Django production settings also trust the proxy correctly for secure requests and redirects.
6. Handle containers differently from traditional servers
For Docker or other container deployments:
- static files can be built into the image or collected in a release step
- media files must go to a mounted volume or external object storage
- do not rely on container-local storage for uploads
A simple Compose pattern:
services:
app:
image: myapp:latest
environment:
DJANGO_STATIC_ROOT: /app/static_collected
DJANGO_MEDIA_ROOT: /app/media
volumes:
- media_data:/app/media
volumes:
media_data:
If you run collectstatic inside the container, make sure your reverse proxy serves the collected static path from the image or from a mounted or shared location. Do not assume container-local static output is automatically available to Nginx.
Practical container patterns for static files usually look like one of these:
- collect static during image build and let the proxy serve that path from the container image or a copied artifact
- collect static in a dedicated release job and publish it to shared storage or object storage
- mount a shared static volume only if that fits your deployment model
For multi-instance deployments, media should use shared storage or object storage. A single local Docker volume is only suitable for a single-host deployment.
Verification after container restart:
- upload a file
- restart the container
- confirm the file is still available
7. Secure media more carefully than static
Static files are trusted application assets. Media files are untrusted user input.
Production media precautions:
- store uploads outside the application code directory
- validate file type and size in application logic
- do not allow uploaded files to be executed as code
- consider private media patterns for invoices, reports, or user-specific documents
Static file precautions:
- keep static read-only after deployment where possible
- use long cache headers only for versioned assets
- do not allow runtime writes to static directories
A common mistake is serving uploaded content from a path where executable behavior could be enabled by another service. Keep the upload path simple and isolated.
8. Add backups and recovery for media
You can regenerate static files from source. You usually cannot regenerate user uploads.
Your backup plan should include:
- media directory or media bucket
- retention policy
- restore procedure
- test restore, not just backup creation
A restore runbook matters more for media than for static. If you lose STATIC_ROOT, rerun collectstatic. If you lose MEDIA_ROOT, you may lose customer data.
Explanation
The key difference between Django static files vs media files is not just file type. It is operational ownership.
- Static belongs to the application release.
- Media belongs to the running system and its users.
That distinction drives the right production architecture:
- separate paths
- separate permissions
- separate caching rules
- separate backup policy
- separate scaling strategy
For single-server deployments, local filesystem storage is often enough if media is outside the release directory and included in backups. For multi-server deployments, local media usually stops working because uploads land on only one node. In that case, use shared storage or object storage for media, while static can still be distributed through the reverse proxy, image build, shared storage, or a CDN.
If your deployment process keeps repeating the same checks, this is a good place to standardize. The first things worth automating are path validation, collectstatic, proxy config generation, and post-deploy checks for known static and media URLs. That reduces drift without changing the underlying architecture.
Edge cases / notes
- Do not use the same directory for
STATIC_ROOTandMEDIA_ROOT. This creates collisions, bad cleanup behavior, and security confusion. - Do not depend on Django development helpers in production. URL patterns added for
static()ormedia()serving are for development convenience. - Be careful with release cleanup scripts. If media lives under a release directory, a normal cleanup can delete uploads.
- Watch proxy path mismatches. An incorrect Nginx
aliasis a common reason static or media returns 404. - For private files, do not expose
/media/publicly by default. Route access through application authorization or a protected file-serving pattern. - If using hashed static assets, app version and collected static files must stay aligned during rollback.
- This page covers file handling only. Production deployments also need correct Django security and proxy settings such as
DEBUG = False,ALLOWED_HOSTS, CSRF configuration, and secure proxy handling.
Internal links
For broader production configuration, see Django Production Settings Checklist (DEBUG, ALLOWED_HOSTS, CSRF).
If you need the full reverse proxy and app server setup, continue with Deploy Django with Gunicorn and Nginx.
For a deployment checklist around release safety, see Django Deployment Checklist for Production.
If you are deciding what server interface to run, see Django WSGI vs ASGI: Which One Should You Deploy?.
FAQ
What is the difference between STATIC_ROOT and MEDIA_ROOT in Django?
STATIC_ROOT is the destination directory for collected application assets after running collectstatic. MEDIA_ROOT is the runtime directory where uploaded files are stored. Static can be rebuilt; media must be preserved.
Should Django serve static and media files in production?
Usually no. In production, Nginx, Caddy, object storage, or a CDN should serve them. Django should handle application logic, not routine file delivery.
Can I store static files and user uploads in the same directory?
No. That is a common production mistake. It creates permission problems, deploy risks, cache issues, and a higher chance of deleting uploads during releases.
What happens to media files when I redeploy a Django app?
They should remain unchanged if media is stored in a persistent location outside the release directory or in external storage. If media is inside the app release path or container filesystem, redeploys may delete it.
How should I handle media files on Docker or multi-server deployments?
Use a mounted persistent volume for single-host containers. For multi-server or autoscaled deployments, use shared storage or object storage so every app instance can access the same uploaded files.