Running Jarvis behind a proxy
Jarvis works behind nginx, Traefik, Caddy or a Kubernetes ingress, but two things have to be right or the UI loads and then never updates: the allowed origins and the WebSocket upgrade. Both fail quietly, which is why they get their own page.
- The one setting people miss
- WebSocket passthrough
- nginx
- Traefik
- Caddy
- Kubernetes ingress
- Jarvis must own the root path
The one setting people miss
JARVIS_ALLOWED_ORIGINS must contain the URL the browser uses, not the address the proxy talks to.
environment:
JARVIS_ALLOWED_ORIGINS: https://jarvis.example.comUnset, Jarvis accepts same-origin requests only: an Origin header is compared against the request's own Host, with either scheme. That is exactly right when you reach Jarvis directly, and always wrong behind a proxy, because the browser sends https://jarvis.example.com while the backend sees its own internal host.
Set, an Origin must match the list (case-insensitively); the server's own host is always accepted in addition, for the WebSocket and for writes alike. Three consequences worth knowing before debugging for an hour:
- Scheme, host and port must all match.
https://jarvis.example.comdoes not matchhttp://jarvis.example.com, andhttp://jarvis.example.com:8080is a different origin thanhttp://jarvis.example.com. - No trailing slash. An origin is
scheme://host[:port]— nothing else. - No wildcards.
*is not accepted, by design. List every hostname you serve Jarvis under, separated by commas.
The same list governs HTTP CORS, the WebSocket upgrade and the check on state-changing requests (see below), so getting it wrong breaks the live updates and the API calls in one go.
Cross-origin writes are rejected. POST, PUT, PATCH and DELETE requests whose Origin is neither this server's own host nor in the list get 403 cross-origin request rejected; so do requests without an Origin that the browser marked Sec-Fetch-Site: cross-site. Requests without either header (curl, scripts) are not affected. If every write in the UI fails with this 403 behind a proxy, the proxy rewrites Host and JARVIS_ALLOWED_ORIGINS is missing the browser's URL — the same fix as for a dead WebSocket.
Optional hardening. JARVIS_ALLOWED_HOSTS rejects any Host header you did not list (421) — set it to the names users reach Jarvis under, and mind that the proxy must forward the original Host. JARVIS_TRUSTED_PROXIES lists the proxy addresses (CIDRs) whose X-Forwarded-For Jarvis believes for the client IP in its logs; without it the logs show the proxy's address, never a forged one. Both are off by default; see Configuration.
WebSocket passthrough
Jarvis pushes alert updates over a WebSocket at /ws. The proxy has to:
- speak HTTP/1.1 upstream (HTTP/1.0 has no upgrade mechanism),
- forward the
UpgradeandConnectionheaders untouched, - allow a long-lived connection — the read timeout must be generous, or the connection is cut every few seconds and the UI reconnects in a loop.
If live updates do not arrive but reloading the page shows current data, this is the cause.
nginx
server {
listen 443 ssl;
server_name jarvis.example.com;
location / {
proxy_pass http://jarvis:8080;
proxy_http_version 1.1;
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;
# WebSocket upgrade
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
}With JARVIS_ALLOWED_ORIGINS=https://jarvis.example.com on the Jarvis container.
Traefik
Traefik forwards WebSocket upgrades without extra configuration; the part that still needs attention is the origin list.
labels:
- traefik.enable=true
- traefik.http.routers.jarvis.rule=Host(`jarvis.example.com`)
- traefik.http.routers.jarvis.entrypoints=websecure
- traefik.http.routers.jarvis.tls=true
- traefik.http.services.jarvis.loadbalancer.server.port=8080
environment:
JARVIS_ALLOWED_ORIGINS: https://jarvis.example.comCaddy
jarvis.example.com {
reverse_proxy jarvis:8080
}Caddy handles the upgrade and the forwarded headers on its own. Set JARVIS_ALLOWED_ORIGINS=https://jarvis.example.com and you are done.
Kubernetes ingress
Same two requirements, expressed in chart values:
config:
allowedOrigins: "https://jarvis.example.com"
ingress:
enabled: true
hosts:
- host: jarvis.example.com
paths:
- path: /
pathType: PrefixMost ingress controllers pass WebSockets through untouched. ingress-nginx does not — it needs explicit annotations. The worked example, including the configuration-snippet and the timeouts, is in the chart README.
Jarvis must own the root path
The frontend requests /api/v1/... and connects to /ws as absolute paths on whatever host it was loaded from. Serving Jarvis under a sub-path — https://ops.example.com/jarvis/ — therefore does not work: the browser would ask for https://ops.example.com/api/v1/alerts and get whatever else lives there.
Give it its own hostname or subdomain. A sub-path deployment would need path rewriting on both the assets and the WebSocket, which Jarvis does not support.
Still not working?
Troubleshooting lists the symptoms — "the UI loads but nothing updates", "WebSocket connection failed" — with what to check for each.
