Troubleshooting
#django

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:

  1. the real hostname is missing from ALLOWED_HOSTS
  2. the reverse proxy is not forwarding the original Host header correctly
  3. 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 www if both are in use
  • make sure Nginx or Caddy forwards the original Host header
  • 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.com and www.example.com if 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=False in 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_name does 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 www both 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.

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.

2026 ยท django-deployment.com - Django Deployment knowledge base