How to Fix DisallowedHost in Django Production
A DisallowedHost error in Django production means Django rejected the incoming Host header. This is a security check, not a random app failure.
Problem statement
A DisallowedHost error in Django production means Django rejected the incoming Host header. This is a security check, not a random app failure.
Common symptoms include:
Invalid HTTP_HOST header: 'example.com'- HTTP
400 Bad Request - the app works locally but fails on the real domain
- the error starts after adding Nginx, Gunicorn, Docker, a load balancer, or a CDN
In production, this usually comes down to one of three problems:
- the real hostname is missing from
ALLOWED_HOSTS - the reverse proxy is not forwarding the original
Hostheader correctly - DNS or routing is sending traffic somewhere unexpected
The fix is to identify the exact host Django is rejecting, allow only the hostnames you actually serve, and verify that your proxy and DNS match that configuration.
Quick answer
To apply a practical Django DisallowedHost fix in production:
- add your real production domains to
ALLOWED_HOSTS - include both apex and
wwwif both are in use - make sure Nginx or Caddy forwards the original
Hostheader - verify DNS points to the correct server or load balancer
- restart or reload the Django app process after changing settings
- test with both browser and
curl
Do not set ALLOWED_HOSTS = ['*'] unless you fully understand the security tradeoff.
Step-by-step solution
Step 1 - Confirm the exact host Django is rejecting
Check your Django, Gunicorn, or application logs first.
If you use systemd with Gunicorn:
sudo journalctl -u gunicorn -n 100 --no-pager
If you want to follow logs live:
sudo journalctl -u gunicorn -f
Typical error:
Invalid HTTP_HOST header: 'www.example.com'. You may need to add 'www.example.com' to ALLOWED_HOSTS.
If running in Docker:
docker compose logs web --tail=100
What to confirm:
- is the rejected host the apex domain, like
example.com - is it
www.example.com - is it a subdomain like
api.example.com - is it the server IP address
- does it happen for all requests or only one hostname
Verification check:
Step 2 - Configure ALLOWED_HOSTS correctly
Add every intended production hostname explicitly.
Example for one domain with www:
ALLOWED_HOSTS = [
"example.com",
"www.example.com",
]
If you also serve an app subdomain:
ALLOWED_HOSTS = [
"example.com",
"www.example.com",
"app.example.com",
]
Avoid broad catch-all patterns unless your architecture truly requires them.
A safe environment-variable pattern:
import os
ALLOWED_HOSTS = [
host.strip()
for host in os.environ.get("DJANGO_ALLOWED_HOSTS", "").split(",")
if host.strip()
]
Production environment value:
DJANGO_ALLOWED_HOSTS=example.com,www.example.com
If you use systemd, this might live in an environment file or unit override. If you use Docker Compose:
services:
web:
environment:
DJANGO_ALLOWED_HOSTS: "example.com,www.example.com"
Important:
- include both
example.comandwww.example.comif both should work - do not add random internal names unless they are genuinely used
- do not rely on local development values in production
- assume
DEBUG=Falsein production and verify you are editing the settings actually used by the running process
Verification check:
Step 3 - Check reverse proxy host forwarding
A common Django Invalid HTTP_HOST header fix is correcting proxy headers. Django can only validate the host it receives.
Nginx
Your Nginx server block should match the domain and pass the original host upstream.
server {
listen 80;
server_name example.com www.example.com;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Test config before reload:
sudo nginx -t
Reload safely:
sudo systemctl reload nginx
Common Nginx mistakes that trigger DisallowedHost:
- missing
proxy_set_header Host $http_host - proxying with a hardcoded upstream host header
server_namedoes not include the actual domain being served
If TLS terminates at Nginx and Django receives plain HTTP upstream, set this in Django so secure requests are detected correctly:
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
Only do this when Django is behind a trusted proxy that sets X-Forwarded-Proto correctly.
Caddy
A basic Caddy setup is usually straightforward, but still verify the site label matches the real hostnames.
example.com, www.example.com {
reverse_proxy 127.0.0.1:8000
}
Caddy usually preserves expected host behavior for standard reverse proxy use, but you should still confirm the request reaching Django uses the public hostname.
If TLS terminates at Caddy and Django sees plain HTTP on the upstream side, the same Django setting applies:
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
Verification checks:
Rollback note: if proxy edits break traffic, restore the previous working config from backup or version control, run nginx -t, and reload again.
Step 4 - Verify DNS and domain routing
Make sure the hostname actually points where you think it does.
Check DNS:
dig example.com +short
dig www.example.com +short
Or:
nslookup example.com
nslookup www.example.com
If you want to test the app path through a server or proxy while sending the expected host header:
curl -I -H "Host: example.com" http://SERVER_IP/
This test only works if the reverse proxy is listening on that IP and port and is intended to receive direct traffic. If the site is only reachable through a load balancer or CDN, test against that layer instead.
What to verify:
- the domain resolves to the correct server or load balancer
- the request reaches the expected Nginx or Caddy instance
- apex and
wwwboth route correctly if both are supported
Step 5 - Reload services and apply changes safely
Django settings changes do not apply until the app process reloads.
Gunicorn
Restart:
sudo systemctl restart gunicorn
Check status:
sudo systemctl status gunicorn
Check logs:
sudo journalctl -u gunicorn -n 50 --no-pager
Uvicorn under systemd
sudo systemctl restart uvicorn
sudo systemctl status uvicorn
Docker Compose
If you changed environment values:
docker compose up -d --force-recreate web
Then inspect logs:
docker compose logs web --tail=100
Verification checks:
Step 6 - Validate the fix end to end
Test the public domain:
curl -I https://example.com
curl -I https://www.example.com
If relevant, also test the proxy or edge layer with an explicit host header:
curl -I -H "Host: example.com" http://SERVER_IP/
Then verify in a browser over HTTPS.
Finally, confirm the bad requests stopped appearing:
sudo journalctl -u gunicorn -n 100 --no-pager
You should also confirm unexpected hosts are still rejected. That is part of a correct ALLOWED_HOSTS Django production setup.
Explanation
Django validates the request host to prevent host header attacks. In production, that means every valid public hostname must be declared in ALLOWED_HOSTS, and your reverse proxy must pass that hostname through correctly.
This is why a Django app may work locally but fail behind Nginx or Gunicorn: local development often bypasses the exact production request path. Once a proxy, load balancer, or container runtime is introduced, the final host seen by Django can change.
ALLOWED_HOSTS should stay restrictive. It is better to list the exact domains you serve than to weaken host validation globally.
Also note that CSRF_TRUSTED_ORIGINS is separate. If forms or admin POST requests fail after fixing DisallowedHost, you may also need:
CSRF_TRUSTED_ORIGINS = [
"https://example.com",
"https://www.example.com",
]
That does not replace ALLOWED_HOSTS; it solves a different validation layer.
Edge cases and notes
Running behind a load balancer or CDN
The public hostname must still be the one Django receives, directly or through correct proxy forwarding. Check each layer:
- CDN or load balancer hostname rules
- reverse proxy forwarding behavior
- Django
ALLOWED_HOSTS - secure proxy header behavior if HTTPS terminates before Django
If one layer serves app.example.com but Django only allows example.com, the request will still fail.
Docker Compose and stale environment values
Changing .env or Compose config does not always update a running container. Recreate the service after changing DJANGO_ALLOWED_HOSTS, then confirm the new environment is actually active.
Kubernetes ingress hostname mismatch
If ingress serves app.example.com but Django only allows example.com, you will still get DisallowedHost. Match ingress host rules to Django settings.
Requests hitting the server by IP address
If users or probes access http://SERVER_IP, Django will reject that unless the IP is in ALLOWED_HOSTS. Usually it is better to fix the caller to use the real hostname.
Multi-tenant or wildcard subdomains
These need more careful design than a simple static ALLOWED_HOSTS list. Validate exactly which host patterns are necessary before widening host acceptance.
Secure proxy header note
If your reverse proxy terminates TLS and forwards plain HTTP to Django, X-Forwarded-Proto on the proxy side should match SECURE_PROXY_SSL_HEADER on the Django side:
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
Do not enable this unless requests reach Django through a trusted proxy path.
When manual host fixes become repetitive
If you manage multiple Django services, standardize two things first: environment-driven ALLOWED_HOSTS parsing and a known-good proxy snippet that forwards Host correctly. CI/CD can also run smoke tests against apex, www, and required subdomains after each deploy. That reduces configuration drift without changing the underlying security model.
Internal links
For background, see What Is ALLOWED_HOSTS in Django and How Does It Work?.
If you are building the full stack around this fix, follow How to Deploy Django with Gunicorn and Nginx and How to Configure Django Production Settings Safely.
If the symptom is broader than host validation, use How to Debug 400 Bad Request Errors in Django Production.
FAQ
Why do I still get DisallowedHost after updating ALLOWED_HOSTS?
Usually because the running app did not reload, the wrong settings file is active, or the proxy is forwarding a different host than the one you added. Check logs again and verify the exact host Django sees.
Should I add my server IP address to ALLOWED_HOSTS?
Only if the application is intentionally accessed by IP address. In most production setups, users should access the app by domain, and health checks should do the same if possible.
What is the difference between ALLOWED_HOSTS and CSRF_TRUSTED_ORIGINS?
ALLOWED_HOSTS controls which request hosts Django accepts. CSRF_TRUSTED_ORIGINS controls which origins are trusted for unsafe requests like POST. They solve different problems.
Can Nginx or a load balancer cause Django DisallowedHost errors?
Yes. If the proxy does not forward the original Host header, or if traffic reaches the app with an unexpected hostname, Django may reject it even when your domain looks correct externally.
Is it safe to set ALLOWED_HOSTS = ['*'] in production?
Generally no. It disables Django's normal host validation and increases exposure to host header abuse. Use explicit hostnames unless you have a specific, reviewed reason not to.