Fix Nginx 404 Not Found for Django URLs
Fix an Nginx 404 Not Found page for Django URLs that work with runserver: route requests to Gunicorn instead of the filesystem, fix try_files and location ordering, root versus alias, and trailing-slash mismatches.
Problem statement
The home page loads but /admin/, /accounts/login/, or any other Django URL returns a plain white page:
404 Not Found
nginx/1.24.0
The nginx footer is the clue. A Django 404 looks different (your template, or the yellow debug page with DEBUG = True). An Nginx-branded 404 means the request never reached Django: Nginx tried to find a file on disk for that path, found none, and answered itself. The config is serving the filesystem where it should be proxying to Gunicorn.
- Confirm who answered:
curl -sI https://example.com/admin/ | grep -i serverand look at the body. - Make
location /proxy to Gunicorn withproxy_pass, notrootortry_files $uri $uri/ =404. - If you use
try_filesfor static files, end it with a named location that proxies to Django:try_files $uri @django;. - Use
aliascorrectly inlocation /static/(with matching trailing slashes). - Check that the request reaches the intended
serverblock, not another site or the default.
Step-by-step solution
1. Find out who generated the 404
curl -s https://example.com/admin/ | head -5
sudo tail -n 5 /var/log/nginx/error.log
Nginx 404s for missing files log lines like:
open() "/var/www/html/admin/index.html" failed (2: No such file or directory)
That path shows which root Nginx used, and therefore which server and location block handled the request. If the error log is empty and the response has Django's styling, the 404 came from Django and the problem is in urls.py or DEBUG settings instead.
2. Proxy the catch-all location to Django
A common broken config, often adapted from a static-site template:
location / {
try_files $uri $uri/ =404;
}
This serves only files. For Django, the catch-all must proxy:
location / {
proxy_pass http://unix:/run/gunicorn.sock;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
3. Fall back to Django when serving files first
If you want Nginx to serve any matching file and pass everything else to Django, use a named location:
location / {
root /srv/app/public;
try_files $uri @django;
}
location @django {
proxy_pass http://unix:/run/gunicorn.sock;
include proxy_params;
}
The last argument of try_files is the fallback. =404 returns Nginx's own 404; @django hands the request to Django.
4. Fix root and alias in static locations
Static and media 404s are the other half of this problem:
location /static/ {
alias /srv/app/staticfiles/; # trailing slash on both
}
With alias, the location prefix is replaced by the path, so /static/css/site.css becomes /srv/app/staticfiles/css/site.css. With root, the prefix is appended: root /srv/app/staticfiles; would look for /srv/app/staticfiles/static/css/site.css. A missing trailing slash on one side produces paths like /srv/app/staticfilescss/site.css. The error log shows the exact path Nginx tried.
5. Check location precedence
Nginx picks exact matches (=) first, then the longest prefix, but regex locations (~) win over prefixes unless the prefix uses ^~. A regex such as:
location ~* \.(css|js|png|jpg)$ {
root /var/www/html;
}
catches /static/admin/css/base.css before location /static/ and looks in the wrong directory. Remove broad regex locations, or mark the static prefix with ^~ so it wins.
Verification check:
curl -sI https://example.com/admin/ | head -1 # 302 to login
curl -sI https://example.com/static/admin/css/base.css | head -1 # 200
curl -sI https://example.com/no-such-page/ | grep -i server # Django's 404, behind nginx
Explanation
Nginx is a file server first. Unless a location explicitly proxies a request, Nginx maps the URL to a path under root and returns 404 if nothing is there. Django URLs do not correspond to files, so every one of them needs to reach a proxy_pass. Many tutorials for PHP or static sites end try_files with =404 or /index.php, and copying those into a Django config produces exactly this symptom.
Edge cases / notes
- Wrong server block. If a request matches a different
server(another site, or the default), you get that site's 404. Checkserver_nameand theHostheader. - Subpath deployments. If Django is mounted under
/app/, theproxy_passURL andFORCE_SCRIPT_NAMEmust agree or every URL 404s inside Django. - Media files need their own
location /media/withalias, plus correct permissions. A 403 instead of 404 means permissions, not paths. - Uppercase and encoded paths are matched literally; a URL with
%20maps to a file name with a space.
Next steps
- Use a complete config from Configure Nginx for Django in Production.
- If the 404 only affects static files, follow Fix Django Static Files Not Loading.
FAQ
How do I know if the 404 came from Nginx or Django?
An Nginx 404 has an nginx footer and logs open() ... failed in the error log. A Django 404 uses your template or the debug page.
Why does the home page work but other URLs fail?
/ may match an index file on disk or a separate exact location, while other paths fall into a files-only location.
Should I use root or alias for static files?
alias with trailing slashes on both the location and the path is clearer for /static/ and /media/.
Do I need try_files at all for Django?
No. A simple location / with proxy_pass is enough when Nginx serves static files from its own /static/ location.