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.
- In Nginx, add
proxy_set_header Host $host;to the location that proxies to Django. - Behind a second proxy or load balancer that rewrites Host, use
X-Forwarded-HostwithUSE_X_FORWARDED_HOST = True. - Make sure the public hostname is in
ALLOWED_HOSTSand the internal one is not needed. - Check settings like
LOGIN_REDIRECT_URLandSITE_URLfor hard-coded internal addresses. - Set
SECURE_PROXY_SSL_HEADERso redirects also keephttps.
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.
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.
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 (
/aboutto/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_hostwith 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
- The client IP has the same proxy problem: Fix Django Wrong Client IP Behind a Proxy.
- Email links with
example.comare covered in Fix Django example.com Domain in the Sites Framework.
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.