Reverse proxy & HTTPS¶
This guide covers putting BamDude behind nginx so external access goes over HTTPS while you keep raw http:// on the LAN if you want it. The most common reason to bother: hitting BamDude from outside the workshop without exposing plain HTTP to the internet.
The same instructions adapt to Caddy / Traefik / HAProxy — the headers + WebSocket bits are what BamDude actually cares about, the proxy product is irrelevant.
Three access models¶
| Mode | URL | nginx? | TLS? | When |
|---|---|---|---|---|
| LAN-only HTTP | http://192.168.1.10:8000 |
no | no | Home network, single user, no external access |
| External HTTPS only | https://bamdude.example.com |
yes | yes | Always go through the proxy, even on LAN |
| Hybrid (recommended for farms) | LAN: http://192.168.1.10:8000 and external: https://bamdude.example.com |
yes (external only) | external | Direct LAN access for low-latency camera streams + HTTPS from outside |
The hybrid model is what most operators end up wanting. Skip to Hybrid setup once you've read the basics — the env vars + nginx config are the same as "External HTTPS only" with one extra hostname rule.
How BamDude detects HTTPS¶
The backend has to know whether a given request arrived over HTTPS so it can set the Secure flag on the refresh-token cookie correctly. Browsers refuse to send Secure cookies over plain HTTP, so getting this wrong locks users out.
BamDude resolves the Secure flag in this order:
- Hard override —
AUTH_REFRESH_COOKIE_SECUREenv var.true→ always Secure.false→ never Secure. Unset → auto-detect (recommended; it's what makes the hybrid model possible). - Auto-detect, request scheme —
request.url.scheme == "https"→ Secure=True. - Auto-detect, trusted proxy — when the immediate caller's IP is listed in
TRUSTED_PROXY_IPS, BamDude readsX-Forwarded-Protoand uses that scheme instead. This is what nginx termination relies on. - Otherwise → Secure=False. Plain LAN HTTP works fine; the cookie just isn't HTTPS-only.
TRUSTED_PROXY_IPS is required for HTTPS via nginx to work
Without TRUSTED_PROXY_IPS=<nginx-ip>, BamDude sees X-Forwarded-Proto: https from an untrusted source and ignores it. Every request looks like plain HTTP, refresh cookies get Secure=False, and login works once (until refresh) but refresh always fails — users get bumped to /login mid-session.
BamDude env vars for proxy setups¶
Set these in .env (or your Docker environment: block) before starting BamDude.
# IPs of every reverse proxy that's allowed to set X-Forwarded-* headers.
# Comma-separated. Use the IP nginx uses to reach BamDude — i.e. on the
# same host this is usually 127.0.0.1; in compose it's the proxy's
# container IP / network alias. NOT the public IP.
TRUSTED_PROXY_IPS=127.0.0.1
# Optional hard override. Leave UNSET for the hybrid mode. Set to "true"
# only when EVERY request reaches BamDude over HTTPS (i.e. nginx is the
# only entrypoint). Set to "false" only on plain-HTTP LAN-only installs.
# AUTH_REFRESH_COOKIE_SECURE=true
# Used by login emails / "click to open BamDude" links. Should be the
# externally-reachable URL — even on hybrid, point this at the HTTPS one
# so links shared via Telegram / email work from anywhere.
APP_URL=https://bamdude.example.com
# Optional. Comma-separated list of origins (scheme://host[:port]) allowed
# to embed BamDude inside an <iframe>. Default = empty (strict: no cross-
# origin embedding). Set this when you want to embed BamDude inside Home
# Assistant's Webpage panel — see "Home Assistant Webpage panel" below.
# TRUSTED_FRAME_ORIGINS=http://homeassistant.local:8123
The Settings → System → External URL field in the UI is the same value as APP_URL env. Whichever is set takes precedence in the order: DB setting > env var > http://localhost:5173 fallback.
nginx config¶
Drop this in /etc/nginx/sites-available/bamdude and ln -s it into sites-enabled/. Ports + paths assume BamDude listens on 127.0.0.1:8000 on the same host as nginx.
# HTTP → HTTPS redirect for the public hostname.
server {
listen 80;
listen [::]:80;
server_name bamdude.example.com;
return 301 https://$host$request_uri;
}
# HTTPS terminator for external access.
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name bamdude.example.com;
# Standard certbot output. Replace with whatever you use.
ssl_certificate /etc/letsencrypt/live/bamdude.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/bamdude.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
# 3MF / camera frames / archive bundles can be big. Default 1m is too low.
client_max_body_size 512m;
# MJPEG camera streams + WebSocket pushes are long-lived; keep the
# tunnel open. 1h covers any reasonable print state observation.
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_buffering off; # camera streams need real-time bytes
proxy_request_buffering off;
# Let BamDude see the original scheme + client IP. The Forwarded-Proto
# is what flips the Secure-cookie auto-detect to True. Without it
# /auth/refresh fails over HTTPS even though TLS is working.
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
# WebSocket upgrade — BamDude pushes live status / dispatch progress /
# archive events over /api/v1/ws. Without these headers the upgrade
# silently fails and the UI just shows stale data.
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
location / {
proxy_pass http://127.0.0.1:8000;
}
}
After saving:
Hybrid: LAN HTTP + external HTTPS¶
The single proxy block above already does external HTTPS. Two extra steps for hybrid:
1. Don't force AUTH_REFRESH_COOKIE_SECURE. Leave it unset so auto-detect picks the right polarity per request:
- LAN visitor on
http://192.168.1.10:8000→ cookie not Secure → browser sends it back. Works. - External visitor on
https://bamdude.example.com→ nginx addsX-Forwarded-Proto: https, BamDude trusts the header (nginx is inTRUSTED_PROXY_IPS), cookie Secure → browser sends it only on HTTPS. Works.
2. Use a different hostname for HTTPS than for HTTP. BamDude (correctly) sends Strict-Transport-Security on HTTPS responses; the browser caches that for the hostname and refuses HTTP for it afterwards. If both modes share bamdude.local, the first HTTPS visit poisons LAN access permanently.
The pragmatic split most operators use:
| Use case | Hostname | Reachable via |
|---|---|---|
| LAN | http://192.168.1.10:8000 |
direct, IP-based — never gets HSTS |
| External | https://bamdude.example.com |
nginx, public DNS — HSTS is fine |
If you really want a hostname (not IP) on the LAN too, use a different one — bamdude.lan or bamdude.home — and make sure no client ever sees HTTPS at that name.
Path-prefixed deployments (subpath)¶
If you serve BamDude under a subpath — https://example.com/bamdude/ (Traefik with a PathPrefix(/bamdude) rule, nginx location /bamdude/, Cloudflare Tunnel with path-routed services) — the SPA bundle since 0.4.3 emits relative asset URLs (./assets/..., ./manifest.json, ./img/..., ./sw-register.js), so the browser resolves every script / stylesheet / icon against whatever path the document loaded from. No BASE_URL env var to tweak.
What this means in practice:
- Strip the prefix at the proxy. BamDude itself always sees the unprefixed path — your reverse proxy must rewrite
/bamdude/footo/foobefore forwarding (Traefik:StripPrefixmiddleware; nginx:proxy_pass http://bamdude:8000/with the trailing slash). - Service worker auto-scopes.
sw-register.jsregisterssw.jsrelatively, so the SW scope pins to the subpath you loaded the SPA from. - Push subscriptions and PWA install work as long as the prefix is stable across reloads. Don't rotate the prefix per-deploy; the SW caches the URLs it was served at.
- API client uses an absolute origin.
api/client.tsissues/api/v1/...relative to the page origin, so the same prefix-strip rule applies to API calls — your proxy needs to forward both/<prefix>/apiand/<prefix>/assets. Don't try to host BamDude's API under a different path than its SPA.
If you load the page and see a blank white screen with a MIME type console error, your proxy isn't stripping the prefix correctly — the browser is hitting your proxy's outer routing for /<prefix>/assets/... instead of BamDude's /assets/... mount.
Home Assistant Webpage panel embedding¶
By default BamDude blocks every cross-origin iframe — X-Frame-Options: SAMEORIGIN plus Content-Security-Policy: frame-ancestors 'none'. That's the safe choice for internet-exposed deployments, but it also means embedding the BamDude UI inside Home Assistant's Webpage dashboard panel always fails: HA on :8123 and BamDude on :8000 are different origins to the browser, and SAMEORIGIN is port-strict.
To opt into iframe embedding from your HA instance, set TRUSTED_FRAME_ORIGINS to the origin (scheme://host[:port]) HA serves itself from:
When set:
X-Frame-Optionsis dropped entirely (the legacyALLOW-FROM <url>syntax is deprecated and inconsistent across browsers — modern browsers honour CSPframe-ancestors, which takes precedence).- The CSP directive becomes
frame-ancestors 'self' <list>on every CSP-bearing route.'self'is always included, so same-origin embedding never breaks even if you forget your own origin in the list.
Multiple origins are comma-separated:
Validation rules (invalid entries are dropped at startup with a warning, not failing the boot):
- Only
http(s)schemes —ftp://,file://,javascript:are rejected. - No paths, query strings, or fragments — only scheme + host + port.
- No wildcards in the host (
*.example.comwould defeat the allowlist).
In Home Assistant, configure the Webpage card with the BamDude URL it can reach (your reverse-proxy URL or the LAN URL) and you're done — no other HA-side knobs needed.
Common issues¶
Login works but refresh keeps failing → user gets bounced to /login¶
X-Forwarded-Proto isn't being honoured because the proxy IP isn't in TRUSTED_PROXY_IPS. Check what BamDude sees:
# Look at the access log inside the container/process
# Should show your nginx host IP, not your laptop IP
Set TRUSTED_PROXY_IPS to that exact IP and restart BamDude.
"WebSocket connection failed"¶
You forgot the Upgrade / Connection: upgrade headers in nginx. The UI loads but live updates (printer status, queue progress, archive events) are stale until reload.
Camera stream stops after ~60 s¶
proxy_read_timeout defaults to 60 s in nginx. Bump it (3600s in the config above) and add proxy_buffering off; so MJPEG bytes flow as they arrive instead of being chunked into nginx's buffer.
"Mixed content blocked" in browser console¶
A relative URL somewhere is resolving to http://. Most often Settings → System → External URL is set to http://... while you're accessing over HTTPS. Set it to the https:// URL or empty (BamDude falls back to APP_URL env var).
LAN HTTP stopped working after I visited HTTPS once¶
That's HSTS doing its job — your browser locked the hostname to HTTPS. Two fixes:
- Use different hostnames (recommended) — see the table above. IP-based LAN URL solves it cleanly.
- Clear HSTS in the browser — Chrome:
chrome://net-internals/#hsts, "Delete domain security policies". Firefox: clear site data including history. This works only until you visit HTTPS again.
Big 3MF uploads return 413¶
client_max_body_size 512m; in the server block — default is 1 MB, way too small.
/auth/refresh 401s but the user just logged in¶
The refresh cookie made it back to nginx but not to BamDude. Either:
- nginx isn't forwarding cookies — usually a misconfigured
proxy_passthat stripsCookie:. The config above doesn't strip headers; check for explicitproxy_set_header Cookie ""somewhere upstream. - Cookie path mismatch. The refresh cookie has
Path=/api/v1/auth— your nginx must proxy that path to BamDude (the catch-alllocation /does). If you split routes, make sure/api/v1/auth/*lands on the same backend.
Sanity checklist¶
Before declaring victory:
-
TRUSTED_PROXY_IPSset to the IP nginx uses to reach BamDude. -
APP_URL(env) or External URL (Settings → System) points at the public HTTPS URL. -
proxy_set_header X-Forwarded-Proto $scheme;present. -
proxy_http_version 1.1;+Upgrade+Connection: upgradepresent. -
proxy_read_timeoutbumped (≥600 s; 3600 s for camera streams). -
client_max_body_sizeraised (≥256 m; 512 m for swap-mode batches). - HSTS hostname is different from the LAN hostname if you keep both modes.
- Logged in over HTTPS, hit refresh, F5 the page → still logged in.
- Open a camera stream, leave it running 10 minutes → still streaming.
- LAN HTTP URL still works (if hybrid mode).