Behind nginx¶
For most production deployments, running BlackBull behind a reverse proxy (nginx, Caddy) is the simplest topology — the proxy handles TLS termination, static files, and load balancing across multiple processes.
This page covers the nginx side of that setup and the
TrustedProxy middleware on the BlackBull side that recovers
client IP and scheme from the proxy headers.
Setup¶
Start BlackBull without TLS — nginx handles certificates:
python app.py --port 8000 # plain HTTP/1.1; no --cert / --key
nginx terminates TLS and HTTP/2 toward clients, then proxies to BlackBull over HTTP/1.1. Regular HTTP requests, WebSocket upgrades, and Server-Sent Events all work through this topology.
Complete nginx configuration¶
upstream blackbull {
server 127.0.0.1:8000;
keepalive 64;
}
server {
listen 443 ssl;
http2 on;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# ── Regular HTTP requests ─────────────────────────────────────────
location / {
proxy_pass http://blackbull;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Connection ""; # enable keep-alive to upstream
}
# ── WebSocket ─────────────────────────────────────────────────────
# WebSocket requires HTTP/1.1 Upgrade; match paths that need it explicitly.
location /ws {
proxy_pass http://blackbull;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s; # keep WS connection open
}
# ── Server-Sent Events ────────────────────────────────────────────
location /sse {
proxy_pass http://blackbull;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Connection "";
proxy_buffering off; # flush SSE events immediately
proxy_cache off;
proxy_read_timeout 3600s;
chunked_transfer_encoding on;
}
}
# Redirect plain HTTP to HTTPS
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}
Trusted-proxy headers¶
By default, scope['client'] is the raw TCP peer (nginx's
loopback address) and scope['scheme'] is 'http' even when
the client connected via HTTPS. Enable TrustedProxy to
rewrite both from the proxy's forwarded headers:
# Shortcut on BlackBull — registers TrustedProxy automatically
app = BlackBull(trusted_proxies=['127.0.0.1', '::1'])
# Or register explicitly for more control (e.g. private subnet):
from blackbull import TrustedProxy
app.use(TrustedProxy(['127.0.0.1', '::1', '10.0.0.0/8']))
With trusted-proxy support active:
scope key |
Without middleware | With middleware |
|---|---|---|
scope['client'] |
nginx's loopback IP | real client IP (from X-Forwarded-For) |
scope['scheme'] |
'http' |
'https' (from X-Forwarded-Proto) |
Supported headers, in precedence order:
- RFC 7239
Forwarded—for=<ip>; proto=<scheme> X-Forwarded-For— comma-separated chain; leftmost non-trusted IP winsX-Forwarded-Proto
Headers are only applied when the direct TCP peer is in the
trusted set, preventing clients from spoofing their IP by
forging X-Forwarded-For.
Docker¶
FROM python:3.13-slim
WORKDIR /app
COPY . .
RUN pip install .
EXPOSE 8000
CMD ["python", "app.py", "--port", "8000"]
Environment variables for secrets (never hardcode):
import os
DB_URL = os.environ['DATABASE_URL']
SECRET = os.environ['SECRET_KEY']
PORT = int(os.environ.get('PORT', 8000))
Going one step further — AF_UNIX¶
When the reverse proxy and BlackBull are on the same host, an
AF_UNIX socket between them removes TCP overhead and avoids
exposing a port on 0.0.0.0. See
Unix and fd inheritance.
The nginx upstream then looks like:
upstream blackbull { server unix:/run/blackbull.sock; }
Next¶
- Workers — multi-worker is a natural fit behind nginx; each worker can saturate one core while the proxy load-balances.
- Unix and fd inheritance —
AF_UNIXbind pattern. - TLS — if you decide to terminate TLS in BlackBull instead of upstream.