Django Static Files Not Loading in Production: The Five Usual Causes
Django Static Files Not Loading in Production: The Five Usual Causes
Locally it is perfect. In production the page arrives with no styling, the console shows a row of
404s, and nothing in your code changed. This is the single most common "it worked on my machine"
moment in Django deployment, and it happens because static files work fundamentally differently once
DEBUG is off.
Why turning off DEBUG breaks them
With DEBUG = True, Django's development server finds and serves static files for you, searching
your apps automatically. It is a convenience for development and explicitly not for production.
With DEBUG = False, Django serves nothing. Files must be collected into one directory and served by
something built for the job — your web server, a middleware designed for it, or a CDN. If you have
not arranged that, every static request returns 404. The application is fine; nothing is delivering
the assets.
Cause 1: collectstatic was never run
python manage.py collectstatic --noinput
This copies every static file from every app into STATIC_ROOT. It must run as part of every
deployment, after any change to assets. Forgetting it in a new pipeline is the most common cause of
all, and the giveaway is that the directory is empty or stale.
Cause 2: STATIC_ROOT and STATIC_URL are confused
They do different things and the names invite mixing them up.
STATIC_URL = "/static/" # the URL prefix browsers request
STATIC_ROOT = BASE_DIR / "staticfiles" # the directory collectstatic writes to
STATICFILES_DIRS = [BASE_DIR / "assets"] # extra places to collect from
STATIC_ROOT must not be one of STATICFILES_DIRS — collecting a directory into itself produces
confusing results and, on some versions, an error.
Cause 3: the web server points at the wrong directory
Your web server needs a rule mapping the URL prefix to the collected directory, and the path must
match exactly:
location /static/ {
alias /srv/django-app/staticfiles/;
expires 30d;
}
Note alias rather than root: with root the location is appended to the path, which produces a
directory that does not exist. It is a small distinction that costs people an hour.
Cause 4: permissions
The web server runs as its own account and must be able to read the files — and traverse every
directory above them. A collected directory inside a home directory with restrictive permissions
produces 403s rather than 404s, which at least tells you the path is right.
sudo -u www-data test -r /srv/django-app/staticfiles/css/app.css && echo readable
Running the check as the actual account is worth more than reading the permission bits.
Cause 5: the manifest cannot find a file
If you use a manifest-based storage backend, filenames get a content hash and a manifest maps the
original names to the hashed ones. If a referenced file is missing at collect time, the whole thing
fails:
ValueError: The file 'css/app.css' could not be found with ManifestStaticFilesStorage
Usually a template references a file that no longer exists, or a CSS file imports something outside
the collected tree. The error names the file; the fix is to correct the reference or add the missing
asset.
Media files are a different problem
Static files ship with your code. Media files are uploaded by users, live in MEDIA_ROOT, and need
their own serving rule. If images uploaded through the admin are missing while your CSS loads
correctly, you are looking at media configuration, not static.
A quick check after every deploy
curl -s -o /dev/null -w "%{http_code}\n" https://example.com/static/css/app.css
Anything other than 200 tells you now, rather than after a customer mentions the site looks broken.
If you are serving several applications from one machine, static files need a little more thought —
see Hosting Multiple Django Apps on One
Server.