Skip to content

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 ​

JARVIS_ALLOWED_ORIGINS must contain the URL the browser uses, not the address the proxy talks to.

yaml
environment:
  JARVIS_ALLOWED_ORIGINS: https://jarvis.example.com

Unset, 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.com does not match http://jarvis.example.com, and http://jarvis.example.com:8080 is a different origin than http://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 Upgrade and Connection headers 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 ​

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.

yaml
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.com

Caddy ​

text
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:

yaml
config:
  allowedOrigins: "https://jarvis.example.com"

ingress:
  enabled: true
  hosts:
    - host: jarvis.example.com
      paths:
        - path: /
          pathType: Prefix

Most 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.

Released under the Apache 2.0 License. Jarvis is not affiliated with Prometheus or Alertmanager.