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.

Who answered the 404?Request /admin/reaches NginxLocation matchwhich block winsFilesystem lookupno file foundNginx 404Django never askedAn Nginx-branded 404 means the request was handled as a file instead of proxied to Django.
  1. Confirm who answered: curl -sI https://example.com/admin/ | grep -i server and look at the body.
  2. Make location / proxy to Gunicorn with proxy_pass, not root or try_files $uri $uri/ =404.
  3. If you use try_files for static files, end it with a named location that proxies to Django: try_files $uri @django;.
  4. Use alias correctly in location /static/ (with matching trailing slashes).
  5. Check that the request reaches the intended server block, 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.

what happens to /admin/try_files $uri $uri/ =404✕looks for /admin/ on disk✕no file, returns nginx 404✕Django never sees the requesttry_files $uri @django✓serves real files directly✓anything else goes to Gunicorn✓Django routes /admin/The last try_files argument decides what happens when no file existsdjango-deployment.com
Replace =404 with a named location that proxies 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.

location matching order1Exact match location = /pathused immediately2Longest prefix with ^~stops regex checks3First matching regex ~ or ~*in file order4Longest plain prefixlocation /static/ or /A broad regex for file extensions can steal requests from /static/django-deployment.com
If a regex matches, it wins over a plain prefix location, which surprises many configs.

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. Check server_name and the Host header.
  • Subpath deployments. If Django is mounted under /app/, the proxy_pass URL and FORCE_SCRIPT_NAME must agree or every URL 404s inside Django.
  • Media files need their own location /media/ with alias, plus correct permissions. A 403 instead of 404 means permissions, not paths.
  • Uppercase and encoded paths are matched literally; a URL with %20 maps to a file name with a space.

Next steps

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.

Keep going