Do not let DEBUG=True take down a container that has no dev deps

The image installs with --no-dev, so django_browser_reload is absent. Settings and
urls both assumed DEBUG implied it was installed, so DJANGO_DEBUG=True in a
deployed container did not merely turn on debugging: the app refused to start, with
a ModuleNotFoundError that says nothing about the actual mistake.

Both now guard on the module being importable. Reproduced the failure locally by
hiding the package with DEBUG on, and confirmed the urlconf loads afterwards.

DEPLOYMENT.md says the obvious thing out loud: a test server is still a deployment
-- real TLS, real domain, real passkeys -- so DEBUG stays off there. The crash is
fixed; the reason to keep it off was never the crash.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 18:14:53 +02:00
parent 5d42691a10
commit c42963c447
4 changed files with 38 additions and 5 deletions

10
.idea/pySourceRootDetection.xml generated Normal file
View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PySourceRootDetectionService">
<option name="sourcePathsSet">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</component>
</project>

View File

@@ -67,6 +67,16 @@ docker compose run --rm web python manage.py check --deploy
**off** in code, because defaulting them to `not DEBUG` would redirect every test request to
https and break the suite anywhere `DEBUG` is unset.
### Keep DJANGO_DEBUG=False, even on the test server
A test box is still a deployment: it is behind TLS, on a real domain, with real passkeys.
`DEBUG=True` there leaks tracebacks and settings to anyone who can reach a 500, and turns off
several of the protections in this document. Use it locally, not on a server.
The app no longer *crashes* if you set it — `django_browser_reload` is a dev dependency that
the image installs with `--no-dev`, so settings guard on the module being importable rather
than assuming DEBUG implies it is there — but the reason to keep it off is not the crash.
### One dependency comes from git
`django-lucide` is our fork (`[tool.uv.sources]` in `pyproject.toml`, pinned by `uv.lock` to a

View File

@@ -10,6 +10,7 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/6.0/ref/settings/
"""
from importlib.util import find_spec
from pathlib import Path
from decouple import Csv, config
@@ -92,10 +93,17 @@ MIDDLEWARE = [
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
# Reload the browser when templates, static files or Python change. Dev only: it
# injects a script tag into every HTML response and serves an open event stream,
# neither of which belongs in production.
if DEBUG:
# Reload the browser when templates, static files or Python change. Dev only: it injects a
# script tag into every HTML response and serves an open event stream, neither of which
# belongs in production.
#
# Guarded on the module being *importable*, not just on DEBUG: it is a dev dependency, and the
# production image installs with --no-dev. Without the guard, DEBUG=True in a deployed
# container does not merely turn on debugging — it stops the app from starting at all, with a
# ModuleNotFoundError that says nothing about the actual mistake.
BROWSER_RELOAD_AVAILABLE = find_spec("django_browser_reload") is not None
if DEBUG and BROWSER_RELOAD_AVAILABLE:
INSTALLED_APPS += ["django_browser_reload"]
MIDDLEWARE += ["django_browser_reload.middleware.BrowserReloadMiddleware"]

View File

@@ -30,6 +30,11 @@ urlpatterns = [
]
if settings.DEBUG:
urlpatterns += [path("__reload__/", include("django_browser_reload.urls"))]
# Only when the app is actually installed. It is a dev dependency, and the production
# image installs with --no-dev, so DEBUG=True in a container must not take the whole
# site down over a package that is only there to refresh a browser tab.
if settings.BROWSER_RELOAD_AVAILABLE:
urlpatterns += [path("__reload__/", include("django_browser_reload.urls"))]
# Club logos are uploads: runserver has to serve MEDIA_ROOT itself.
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)