Fix Django Redirecting to localhost or the Wrong Host

Fix Django redirects that send users to localhost, 127.0.0.1, or an internal hostname behind Nginx or a load balancer: forward the Host header, use USE_X_FORWARDED_HOST correctly, and fix LOGIN_REDIRECT_URL and absolute URLs.

Problem statement

After logging in, submitting a form, or visiting a URL without a trailing slash, the browser lands on http://localhost/..., http://127.0.0.1:8000/..., or http://web:8000/.... Admin "View on site" links and password reset emails may show the same internal address.

Django builds absolute redirect URLs from the request's host. Behind a reverse proxy, the host Django sees is whatever the proxy sent. If Nginx proxies to http://127.0.0.1:8000 without forwarding the original Host header, or a Docker setup proxies to http://web:8000, Django believes it is being served at that internal address and redirects users there.

How the internal host leaksBrowser asksexample.com/loginNginx proxiesHost not forwardedDjango seesHost: 127.0.0.1:8000Redirect builtto the internal hostDjango builds redirects from the Host it receives, so the proxy must pass the public one.
  1. In Nginx, add proxy_set_header Host $host; to the location that proxies to Django.
  2. Behind a second proxy or load balancer that rewrites Host, use X-Forwarded-Host with USE_X_FORWARDED_HOST = True.
  3. Make sure the public hostname is in ALLOWED_HOSTS and the internal one is not needed.
  4. Check settings like LOGIN_REDIRECT_URL and SITE_URL for hard-coded internal addresses.
  5. Set SECURE_PROXY_SSL_HEADER so redirects also keep https.

Step-by-step solution

1. See what host Django receives

Add a temporary log line or use the shell against a request:

import logging
log = logging.getLogger(__name__)

def debug_host(request):
    log.warning("host=%s forwarded_host=%s scheme=%s",
                request.get_host(), request.META.get("HTTP_X_FORWARDED_HOST"), request.scheme)

If host is 127.0.0.1:8000 or a container name, the proxy is not forwarding the public host.

2. Forward the Host header in Nginx

location / {
    proxy_pass http://127.0.0.1:8000;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}

Without proxy_set_header Host, Nginx sends the host from the proxy_pass URL. The proxy_params include file on Debian and Ubuntu already sets it; custom configs often forget. $host is the request's host without the port; use $http_host if your public URL includes a non-standard port.

what Django uses to build redirect URLsproxy_pass only✕Host: 127.0.0.1:8000 sent upstream✕redirect to http://127.0.0.1:8000/✕emails link to the internal hostproxy_set_header Host $host✓Host: example.com sent upstream✓redirect to https://example.com/✓ALLOWED_HOSTS checks the real nameOne proxy header decides the host in every absolute URL Django buildsdjango-deployment.com
Forwarding Host also makes ALLOWED_HOSTS protect the public name instead of an internal one.

3. Handle chains of proxies

With a load balancer or CDN in front of Nginx, the outer proxy might deliver the public host as X-Forwarded-Host while setting Host to its own target. Then tell Django to trust that header:

USE_X_FORWARDED_HOST = True

Only enable it when every request passes through a proxy you control that sets or overwrites X-Forwarded-Host. Otherwise a client can send its own header and make Django generate links to an attacker's domain, which is how password-reset poisoning works. Make Nginx overwrite it:

proxy_set_header X-Forwarded-Host $host;

4. Fix hard-coded URLs

Search settings and templates for internal addresses:

grep -rnE "localhost|127\.0\.0\.1|:8000|http://web" config/ templates/ */settings*.py

Common places: LOGIN_REDIRECT_URL = "http://localhost:8000/dashboard/" (use a path such as "/dashboard/"), CSRF_TRUSTED_ORIGINS, a custom SITE_URL used in emails, and the Sites framework domain (example.com or localhost in the django_site table).

5. Keep the scheme right too

If the host is right but redirects go to http://, Django does not know the original request was HTTPS:

SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")

with proxy_set_header X-Forwarded-Proto $scheme; in Nginx.

where the bad host comes fromRedirect goes toCause and fix127.0.0.1:8000 or web:8000Host not forwarded;proxy_set_header Host $hostload balancer's own nameouter proxy rewritesHost; X-Forwarded-Hostlocalhost in emails onlySites framework or SITE_URL settingright host, but http://scheme unknown;SECURE_PROXY_SSL_HEADERFix the headers at the proxy first, then look for hard-coded valuesdjango-deployment.com
Emails are generated outside requests, so they need configured domains, not headers.

Verification check:

curl -sI https://example.com/admin | grep -i location
curl -sI https://example.com/accounts/login/ -X POST | grep -i location

Every Location header should use https://example.com.

Explanation

HTTP redirects need absolute URLs in practice, and Django constructs them from request.get_host() and request.scheme. Both come from the incoming request: Host (or X-Forwarded-Host when trusted) and wsgi.url_scheme (or the header named in SECURE_PROXY_SSL_HEADER). A reverse proxy is a new client from Django's point of view, so unless it copies the browser's host and scheme into the request it sends upstream, Django sees the proxy's view of the world, which is an internal address.

Edge cases / notes

  • APPEND_SLASH redirects (/about to /about/) are a common first place to notice the problem.
  • Kubernetes ingress forwards Host by default; a problem there usually comes from a second Nginx inside the pod.
  • Port in redirects. If redirects include :8080, the proxy forwards $http_host with a port that is not public; use $host.
  • ALLOWED_HOSTS must contain the public name. Do not add internal names just to make broken forwarding work.

Next steps

FAQ

Why does Django redirect to 127.0.0.1?

Nginx proxies to 127.0.0.1 and does not forward the original Host header, so Django thinks that is its address.

Is USE_X_FORWARDED_HOST safe?

Only when a trusted proxy always sets or overwrites the header. Otherwise clients can spoof it.

Does proxy_params set the Host header?

Yes, on Debian and Ubuntu it includes proxy_set_header Host $http_host;. Check that your location includes it.

Why are only emails wrong?

Emails are often built without a request, from the Sites framework or a settings value. Fix those values.

Keep going