Django Integration
bext fronts Django as a reverse-proxy edge. Django keeps every bit of
its own stack — ORM, admin, forms, middleware, management commands —
and bext handles TLS, HTTP/2, HTTP/3, the WAF, caching, and static
file delivery. The upstream is a regular Gunicorn or Uvicorn process
listening on 127.0.0.1; bext never runs Python in-process.
Architecture
client ──TLS──▶ bext edge ──HTTP/1.1──▶ Gunicorn / Uvicorn ──▶ Django
│ │
│ serves /static/** │
│ serves /media/** │
│ WAF, cache, tracing │
│ HTTP/2, HTTP/3 │
└── upstream health checks ────┘
Everything after the upstream boundary is stock Django. Everything before it is stock bext. The two meet over localhost HTTP/1.1.
Scaffold
bext new my-app --template @bext/starter-django
cd my-app
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
python manage.py migrate
python manage.py collectstatic --noinput
Gunicorn (sync WSGI)
Gunicorn is the default for classic Django deployments: sync views,
ORM queries, templated responses. Pick it if you're not writing
async def view functions.
gunicorn myproject.wsgi \
--bind 127.0.0.1:8000 \
--workers 4 \
--threads 2 \
--timeout 30
Rule of thumb: workers = 2 * cores + 1 for IO-bound Django work;
bext absorbs cold-start jitter and queues bursts at the edge, so you
usually don't need as many workers as you would behind nginx.
Uvicorn (async ASGI)
Uvicorn is the right choice when any view is async def, when you
stream responses, or when you want to use Django Channels for
websockets. It runs on uvloop and handles ASGI lifespan events
cleanly.
uvicorn myproject.asgi:application \
--host 127.0.0.1 \
--port 8000 \
--workers 4
bext doesn't care which one you pick — it's a reverse proxy
destination. Switching is systemctl stop django-gunicorn then
systemctl start django-uvicorn; no bext config change needed.
Static files served by bext
Django's staticfiles view is fine in development but should never
run in production. Bext serves static content directly from
STATIC_ROOT with content-addressed URLs, pre-compressed brotli /
gzip variants, and cache-control: immutable — an order of
magnitude less latency than asking Django for a PNG.
In bext.config.toml:
[[route_rules]]
pattern = "/static/**"
render = "static"
headers = { "cache-control" = "public, max-age=31536000, immutable" }
[[route_rules]]
pattern = "/media/**"
render = "static"
And in myproject/settings.py:
STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
After deploying new assets run python manage.py collectstatic; bext
picks up the new files without a reload because static routes are
filesystem-backed.
bext.config.toml
[server]
port = 3000
app_dir = "."
[framework]
type = "proxy"
[upstreams.django]
keepalive = 32
connect_timeout_ms = 2000
read_timeout_ms = 30000
[[upstreams.django.servers]]
url = "http://127.0.0.1:8000"
[upstreams.django.health]
enabled = true
path = "/"
[[route_rules]]
pattern = "/static/**"
render = "static"
[[route_rules]]
pattern = "/media/**"
render = "static"
[[route_rules]]
pattern = "/**"
proxy = "django"
[cache]
enabled = true
[waf]
enabled = true
The starter ships with this file pre-wired. Point STATIC_ROOT at
./staticfiles in settings so the directory layout matches.
systemd units
Gunicorn
# /etc/systemd/system/django-gunicorn.service
[Unit]
Description=Gunicorn for my Django app
After=network.target
[Service]
Type=notify
User=django
Group=django
WorkingDirectory=/srv/my-app
Environment="PATH=/srv/my-app/.venv/bin"
Environment="DJANGO_SETTINGS_MODULE=myproject.settings"
ExecStart=/srv/my-app/.venv/bin/gunicorn \
--bind 127.0.0.1:8000 \
--workers 4 \
--threads 2 \
--timeout 30 \
myproject.wsgi
Restart=always
[Install]
WantedBy=multi-user.target
Uvicorn
# /etc/systemd/system/django-uvicorn.service
[Unit]
Description=Uvicorn for my Django app (ASGI)
After=network.target
[Service]
Type=simple
User=django
Group=django
WorkingDirectory=/srv/my-app
Environment="PATH=/srv/my-app/.venv/bin"
Environment="DJANGO_SETTINGS_MODULE=myproject.settings"
ExecStart=/srv/my-app/.venv/bin/uvicorn \
--host 127.0.0.1 \
--port 8000 \
--workers 4 \
myproject.asgi:application
Restart=always
[Install]
WantedBy=multi-user.target
Pair either unit with bext's own systemd unit. systemctl reload bext re-reads bext.config.toml without dropping connections —
Django deploys are independent of bext deploys.
Trust the X-Forwarded-Proto header
bext sets X-Forwarded-Proto and X-Forwarded-For on every proxied
request. Django needs a one-line setting to honour them:
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
USE_X_FORWARDED_HOST = True
With that in place, request.is_secure() correctly returns True
when the client came in over TLS even though the upstream leg is
plain HTTP.
Caching layer
Pair bext's fragment cache with Django's @vary_on_headers /
@cache_control decorators. Django sets Cache-Control and bext
honours it automatically; there's no bext-specific annotation.
For tag-based invalidation from inside a Django view, POST to the bext admin endpoint:
requests.post(
"http://127.0.0.1:3000/__bext/cache/invalidate",
json={"tags": ["products"]},
)
Database connections and health checks
bext's upstream health checks ping / by default. Point them at a
lightweight view (not /health unless you define it, to avoid a 404)
or add a dedicated health endpoint in urls.py that returns a small
JSON body without touching the DB.
Catalog detection
Projects are detected automatically by bext site autoconfigure
when all three of the following hold:
- manage.py exists at the project root
- requirements.txt or pyproject.toml lists django
- no other framework rule fires first
The detection rule lives in
crates/bext-import/catalog/frameworks.yaml under django.
Known gaps
- Async-only cache invalidation: Django's async cache backends are still evolving; bext cache invalidation from Channels consumers uses a thread-pool fallback. - Channels websockets: route LiveView-style WS traffic through a dedicated upstream (or a second bext upstream group) rather than the same one that serves request-response traffic, to avoid starving worker slots during long-held connections. - Multi-site deployments: for a single bext process fronting multiple Django apps, give each app its own upstream group and its own hostname-bound route rules.