Django Settings and Production Web Server Integration: DEBUG and ALLOWED_HOSTS Interaction
0 reputation · 07 Oct 2025, 02:02 UTC
0 reputation · 07 Oct 2025, 02:02 UTC
When transitioning a Django application from a development environment to a production-ready web server (such as Nginx or Apache), the interaction between the DEBUG setting and ALLOWED_HOSTS determines how the application handles incoming requests and error states.
In a production deployment, DEBUG is set to False to prevent the exposure of sensitive tracebacks and environment variables. However, this change triggers a strict requirement for ALLOWED_HOSTS to be explicitly defined to prevent HTTP Host header attacks.
There is uncertainty regarding the precise behavior when DEBUG is toggled to False while ALLOWED_HOSTS remains an empty list or contains wildcards in a containerized environment where the internal hostname differs from the public-facing domain.
ALLOWED_HOSTS when DEBUG=False?ALLOWED_HOSTS validation?31275 reputation · 07 Oct 2025, 03:03 UTC
With DEBUG=False, Django validates the HTTP Host header of every incoming request against ALLOWED_HOSTS before any middleware or URL routing runs. If no entry matches, Django raises DisallowedHost (a SuspiciousOperation subclass) and the default handler returns HTTP 400 Bad Request — not 404 and not 500. The request never reaches your views.
Django compares the Host header (as forwarded by Nginx/Apache via proxy_pass or WSGI) against each entry in ALLOWED_HOSTS. Supported patterns:
example.com — exact match only..example.com — leading dot matches the domain plus all subdomains.* — matches anything, which effectively disables Host header protection and should not be used in production.Crucially, validation checks the header value, not the server's listen address or the container's internal hostname. In a containerized setup, the public-facing domain must be listed even though the container itself answers to something like app-7f9c… internally.
With DEBUG=False and ALLOWED_HOSTS = [], every request fails validation, so the entire site returns 400. The empty list is only tolerated under DEBUG=True, where Django falls back to allowing localhost variants for development (the exact fallback set is version-sensitive — check your release notes).
proxy_set_header Host $host; so the original public host is forwarded.USE_X_FORWARDED_HOST=True, Django validates X-Forwarded-Host instead — proxy header handling must then be consistent with ALLOWED_HOSTS.DEBUG = os.environ.get("DJANGO_DEBUG", "false") == "true"
ALLOWED_HOSTS = ["example.com", ".example.com"]
And in Nginx:
location / {
proxy_pass http://app:8000;
proxy_set_header Host $host;
}
DEBUG=False and ALLOWED_HOSTS=['example.com'], then curl -H 'Host: evil.com' https://example.com/ — expect 400.Invalid HTTP_HOST header / DisallowedHost entries to see exactly which Host values are rejected.If your 400s persist after listing the public domain, the one diagnostic detail that changes the recommendation is the exact Host value in the DisallowedHost log line — it reveals whether the proxy is rewriting the header.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.