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.
Quick answer
- 400 Bad Request everywhere: add your real hostnames to
ALLOWED_HOSTS. WithDEBUG = Trueand an empty list, Django allowslocalhostvariants automatically. - Static files missing:
runserveronly serves static files whenDEBUG = True. SetSTATIC_ROOT, runcollectstatic, and serve files with Nginx or WhiteNoise. - 500 on pages with
{% static %}: manifest storage raisesValueError: Missing staticfiles manifest entry; runcollectstaticand fix wrong paths. - Media uploads missing: the
static()helper forMEDIA_URLinurls.pydoes nothing whenDEBUG = False; serve media from the web server or object storage. - Error pages or handlers failing: make
404.htmland500.htmlself-contained, and configure logging so exceptions are visible. - Test with
DEBUG = Falselocally before deploying.
Step-by-step solution
1. Understand what DEBUG changes
| Behavior | DEBUG = True | DEBUG = False |
|---|---|---|
| Error pages | detailed technical page with traceback | generic 404.html / 500.html |
ALLOWED_HOSTS empty | allows localhost, 127.0.0.1, [::1] | rejects every host |
runserver static files | served by django.contrib.staticfiles | not served |
static() helper in URLconf | adds a media/static URL pattern | returns an empty list |
SQL query log (connection.queries) | recorded | not recorded |
mail_admins logging handler | filtered out | sends error emails if configured |
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"
python manage.py collectstatic --noinput
Then serve STATIC_ROOT:
- with WhiteNoise: add
whitenoise.middleware.WhiteNoiseMiddlewareafterSecurityMiddleware - 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:
collectstatichas 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
collectstaticitself 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.htmlextendsbase.html, which calls a template tag that queries the database or requires a logged-in user500.htmluses{% static %}with a missing manifest entry, or a context variable from a context processor (the default 500 handler renders without request context)
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
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
200with debug off and correct hostnames - admin and site CSS load
- an unknown URL returns your
404.htmlwith status404 - uploads display from their production location
- no
ValueErrororNoReverseMatcherrors 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:
ALLOWED_HOSTSlists every real hostname, includingwwwand apex variants you serve.STATIC_ROOTis set andcollectstaticruns during deploy.- A server (Nginx, WhiteNoise, or a CDN) serves
STATIC_URL. MEDIA_URLis served by the web server or object storage, notstatic()in the URLconf.404.htmland500.htmlexist and render without database access or context processors.LOGGINGsendsdjango.requesterrors somewhere you read.ADMINSandSERVER_EMAILare configured if you rely on error emails, or an error tracker is set up.
Common mistakes
- Leaving
DEBUG = Truebecause "everything works" in production. - Using
ALLOWED_HOSTS = ["*"]to silence400errors. - Running
collectstaticonce and never again after adding static files. - Serving media through the
static()helper and assuming it works in production.
Next steps
- Read tracebacks with Fix Django 500 Internal Server Error in Production.
- Complete static file setup with Django Static Files Not Loading in Production.
- Configure hosts with How to Fix DisallowedHost in Django Production.
- Resolve manifest failures with Fix collectstatic Errors in Django Deployments.
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.