Troubleshooting
Settings & secrets

Django Works with DEBUG=True but Fails with DEBUG=False

Fix Django apps that break when DEBUG is turned off: ALLOWED_HOSTS, static files, manifest errors, error templates, email handlers, and hidden exceptions.

Problem statement

The project works locally. You set DEBUG = False to prepare for production, and suddenly:

  • every page returns 400 Bad Request
  • CSS, JavaScript, and images disappear, including in the admin
  • some pages return Server Error (500) that never happened before
  • custom 404 pages or error emails misbehave

This is not random. DEBUG changes several distinct Django behaviors at once, and code or configuration can silently depend on the debug versions. Setting DEBUG = True "fixes" the symptoms but is not an option for production, because debug mode exposes settings and source code in error pages and keeps a record of every SQL query in memory.

This guide lists what changes when DEBUG is False, how each change shows up, and how to fix the underlying dependency.

What DEBUG changesDEBUG ondev conveniencesHost validationALLOWED_HOSTSStatic servingrunserver onlyError pages404.html, 500.htmlProduction replaces each development convenience with an explicit mechanism.

Quick answer

  1. 400 Bad Request everywhere: add your real hostnames to ALLOWED_HOSTS. With DEBUG = True and an empty list, Django allows localhost variants automatically.
  2. Static files missing: runserver only serves static files when DEBUG = True. Set STATIC_ROOT, run collectstatic, and serve files with Nginx or WhiteNoise.
  3. 500 on pages with {% static %}: manifest storage raises ValueError: Missing staticfiles manifest entry; run collectstatic and fix wrong paths.
  4. Media uploads missing: the static() helper for MEDIA_URL in urls.py does nothing when DEBUG = False; serve media from the web server or object storage.
  5. Error pages or handlers failing: make 404.html and 500.html self-contained, and configure logging so exceptions are visible.
  6. Test with DEBUG = False locally before deploying.

Step-by-step solution

1. Understand what DEBUG changes

BehaviorDEBUG = TrueDEBUG = False
Error pagesdetailed technical page with tracebackgeneric 404.html / 500.html
ALLOWED_HOSTS emptyallows localhost, 127.0.0.1, [::1]rejects every host
runserver static filesserved by django.contrib.staticfilesnot served
static() helper in URLconfadds a media/static URL patternreturns an empty list
SQL query log (connection.queries)recordednot recorded
mail_admins logging handlerfiltered outsends error emails if configured
what DEBUG = False changesDEBUG = TrueDEBUG = FalseALLOWED_HOSTSlocalhost allowed!enforcedStatic filesserved by runserver!not served by DjangoManifest storagemissing entries tolerated✕ValueError: 500Error pagestechnical tracebacks404.html and 500.htmlMedia via static()works!returns no patternsEvery row is a behaviour DEBUG was quietly providingdjango-deployment.com
Test with DEBUG off locally before it reaches a server.

Each row can break an app that implicitly relied on the debug behavior.

2. Fix 400 Bad Request: ALLOWED_HOSTS

With DEBUG = False, Django validates the Host header of every request against ALLOWED_HOSTS. An invalid host raises DisallowedHost, which returns 400.

ALLOWED_HOSTS = ["www.example.com", "example.com"]

For local testing with debug off:

ALLOWED_HOSTS = ["localhost", "127.0.0.1", "[::1]"]

Behind a reverse proxy, make sure the proxy forwards the original host (proxy_set_header Host $host; in Nginx); otherwise Django sees the upstream name, such as the socket path or 127.0.0.1.

Avoid ALLOWED_HOSTS = ["*"]. It disables host header validation, which protects against cache poisoning and poisoned password reset links.

3. Fix missing static files

runserver serves static files from app directories only when DEBUG = True. With debug off, nothing serves them until you set up production static handling:

STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
runserver was serving your static filesDEBUG = Truestaticfiles app serves from app dirsno collectstatic needed✕not for productionDEBUG = False✓collectstatic into STATIC_ROOT✓WhiteNoise or Nginx serves it✓hashed names, long cachingrunserver --insecure exists, but it is still not for productiondjango-deployment.com
This is the single most common DEBUG=False surprise.
python manage.py collectstatic --noinput

Then serve STATIC_ROOT:

  • with WhiteNoise: add whitenoise.middleware.WhiteNoiseMiddleware after SecurityMiddleware
  • with Nginx: location /static/ { alias /srv/myapp/staticfiles/; }

To test production static handling locally with runserver, WhiteNoise is the simplest option. The --insecure flag (python manage.py runserver --insecure) forces runserver to serve static files with debug off, but it is for local testing only and does not test your production setup.

4. Fix 500 errors from the static manifest

If you use ManifestStaticFilesStorage or CompressedManifestStaticFilesStorage, every {% static "path" %} looks up the hashed name in staticfiles.json. With DEBUG = False, a missing entry raises:

ValueError: Missing staticfiles manifest entry for 'img/logo.png'

Causes:

  • collectstatic has not been run since the file was added
  • the path in the template is wrong (case differences matter on Linux)
  • the file is referenced in CSS with a path that does not exist, which makes collectstatic itself fail during post-processing

Fix the path, run collectstatic, and restart the app so it reloads the manifest.

5. Fix media files in development-style URLconfs

A common urls.py pattern:

from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    # ...
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

static() returns an empty list when DEBUG = False, so uploaded files return 404. In production, serve media from Nginx or object storage:

location /media/ {
    alias /srv/myapp/media/;
}

Never serve untrusted uploads from the same origin with permissive content types; set X-Content-Type-Options: nosniff and consider a separate domain for user content.

6. Fix hidden exceptions that DEBUG pages were masking

Sometimes the page seemed to work with debug on because you only looked at it after the debug page appeared, or because an exception happened in a code path that the debug toolbar or debug-only middleware changed. With debug off, you need logs to see them:

LOGGING = {
    "version": 1,
    "disable_existing_loggers": False,
    "handlers": {"console": {"class": "logging.StreamHandler"}},
    "loggers": {
        "django.request": {"handlers": ["console"], "level": "ERROR", "propagate": False},
    },
}

Also look for code guarded by if settings.DEBUG: that sets up something required, such as URL patterns for a debug toolbar that templates still reference with {% url %}. A template referencing a URL name that only exists in debug mode raises NoReverseMatch in production.

7. Fix error templates

When DEBUG = False, Django renders 404.html and 500.html from your template directories if they exist. Problems:

  • 404.html extends base.html, which calls a template tag that queries the database or requires a logged-in user
  • 500.html uses {% static %} with a missing manifest entry, or a context variable from a context processor (the default 500 handler renders without request context)
error templates that cannot failtemplates/404.html   {%extends"base.html" %} …templates/500.htmlplain HTML, inline stylestemplates/400.html,403.html1231Fine for 404: the request context works.2500 renders with no context; keep it simple.3Optional; CSRF failures use their own view.A 500 template that raises turns one error into twodjango-deployment.com
Test them by requesting a missing URL and a view that raises.

Keep 500.html minimal:

<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><title>Server error</title></head>
<body>
  <h1>Something went wrong</h1>
  <p>The error has been logged. Please try again later.</p>
</body>
</html>

Test both templates with debug off:

from django.test import Client, override_settings


@override_settings(DEBUG=False, ALLOWED_HOSTS=["testserver"])
def test_404_page():
    response = Client().get("/definitely-not-a-page/")
    assert response.status_code == 404

8. Check for DEBUG-dependent code in your project

Search for places that branch on debug mode:

grep -rn "settings.DEBUG\|DEBUG =" --include="*.py" --include="*.html" .

Review each one. Debug-only behavior should be limited to developer tooling, never to functionality users need.

9. Test with production-like settings locally

DJANGO_DEBUG=0 DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1 python manage.py collectstatic --noinput
DJANGO_DEBUG=0 DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1 python manage.py check --deploy
DJANGO_DEBUG=0 DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1 gunicorn config.wsgi:application --bind 127.0.0.1:8000
reproduce DEBUG = False locallyDEBUG=Falseand real ALLOWED_HOSTScollectstatic, then manifest storageas in productionGunicorn, not runserversame entry pointVisit key pages and a 404watch the logsA CI job doing the same catches regressions automaticallydjango-deployment.com
Reading DEBUG from the environment as a string is its own trap; parse it.

Then browse key pages, the admin, a 404 URL, and a form submission. Running Gunicorn locally instead of runserver exercises the same static and error behavior as production.

Verification check:

  • pages return 200 with debug off and correct hostnames
  • admin and site CSS load
  • an unknown URL returns your 404.html with status 404
  • uploads display from their production location
  • no ValueError or NoReverseMatch errors in logs

Explanation

DEBUG is a convenience switch for development that bundles host validation leniency, static file serving, detailed errors, and query logging. Production needs every one of those behaviors replaced by an explicit production mechanism: a real host list, a static file server, logging, and error templates. When an app "only works with debug on", it is depending on at least one development convenience that has no production replacement yet.

Edge cases / notes

Django admin CSS missing only in production

This is the static files issue in step 3; the admin's CSS comes from django.contrib.admin's static directory and must be collected like any other static file.

Different behavior between runserver and Gunicorn

runserver auto-reloads and serves static files; Gunicorn does neither. Always test the production server command before deploying.

DEBUG read from environment as a string

DEBUG = os.environ.get("DEBUG") makes DEBUG the string "False", which is truthy. Parse booleans explicitly, or you may think debug is off while it is on.

Add a CI check that runs with DEBUG off

The most reliable prevention is to exercise production behavior before every deploy. A small test module catches the common regressions:

# core/tests/test_production_mode.py
from django.conf import settings
from django.core.management import call_command
from django.test import TestCase, override_settings


@override_settings(
    DEBUG=False,
    ALLOWED_HOSTS=["testserver"],
    STORAGES={
        **settings.STORAGES,
        "staticfiles": {
            "BACKEND": "django.contrib.staticfiles.storage.ManifestStaticFilesStorage",
        },
    },
)
class ProductionModeTests(TestCase):
    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        call_command("collectstatic", interactive=False, verbosity=0)

    def test_home_page_renders(self):
        response = self.client.get("/")
        self.assertEqual(response.status_code, 200)

    def test_admin_login_renders(self):
        response = self.client.get("/admin/login/")
        self.assertEqual(response.status_code, 200)

    def test_unknown_url_returns_404_template(self):
        response = self.client.get("/this-page-does-not-exist/")
        self.assertEqual(response.status_code, 404)

Run it in CI with STATIC_ROOT pointing at a temporary directory. Rendering templates with manifest storage and debug off fails the test when a {% static %} path is wrong, a debug-only URL name is referenced, or an error template is broken, which are exactly the problems that otherwise appear only in production.

Also add python manage.py check --deploy --fail-level WARNING with production-like settings to the pipeline. It flags DEBUG = True, missing HSTS and secure cookie settings, and a weak SECRET_KEY.

A quick production-mode checklist

Before switching a project to DEBUG = False for the first time:

  1. ALLOWED_HOSTS lists every real hostname, including www and apex variants you serve.
  2. STATIC_ROOT is set and collectstatic runs during deploy.
  3. A server (Nginx, WhiteNoise, or a CDN) serves STATIC_URL.
  4. MEDIA_URL is served by the web server or object storage, not static() in the URLconf.
  5. 404.html and 500.html exist and render without database access or context processors.
  6. LOGGING sends django.request errors somewhere you read.
  7. ADMINS and SERVER_EMAIL are configured if you rely on error emails, or an error tracker is set up.

Common mistakes

  • Leaving DEBUG = True because "everything works" in production.
  • Using ALLOWED_HOSTS = ["*"] to silence 400 errors.
  • Running collectstatic once and never again after adding static files.
  • Serving media through the static() helper and assuming it works in production.

Next steps

FAQ

Why does Django return 400 Bad Request when DEBUG is False?

ALLOWED_HOSTS is empty or does not contain the host the request uses. With debug on, Django allows localhost names automatically; with debug off, you must list every hostname.

Why do static files stop loading when I set DEBUG to False?

runserver only serves static files in debug mode. In production, run collectstatic and serve STATIC_ROOT with WhiteNoise or your web server.

Why do I get 500 errors only with DEBUG=False?

Common causes are missing static manifest entries, error templates that fail to render, and URL names that only exist in debug mode. Configure logging to see the traceback.

Can I test DEBUG=False locally?

Yes. Set DEBUG off, add localhost to ALLOWED_HOSTS, run collectstatic, and serve the app with Gunicorn and WhiteNoise to mirror production behavior.

Keep going