Skip to content

Tailscale Serve and reverse proxies

Keep EdgeWatch bound to loopback when exposing it through Tailscale Serve or a reverse proxy. Serve the public hostname over HTTPS and add the hostname that users open to web.allowed_hosts:

web:
listen: 127.0.0.1:8080
allowed_hosts:
- edgewatch.example.ts.net
forwarded_header: x-forwarded-for
trusted_proxies:
- 127.0.0.1/32
- ::1/128

Replace edgewatch.example.ts.net with your tailnet or proxy hostname. Use the bare hostname only; do not include https://, a path, or a port. EdgeWatch checks the forwarded Host value before authentication, so an unlisted proxy hostname is rejected with 421 Misdirected Request even when the loopback listener is healthy. Approved non-loopback hostnames receive Secure session cookies; direct loopback HTTP remains available for local administration and SSH tunnels. The example trusts a local proxy and its sanitized X-Forwarded-For client address; replace these networks with the addresses that actually connect to EdgeWatch when the proxy runs elsewhere. If a TLS-terminating proxy rewrites the upstream Host to a loopback address, list the proxy address or network in web.trusted_proxies so EdgeWatch can trust its X-Forwarded-Proto: https (or RFC 7239 Forwarded: ...;proto=https) signal and keep the session cookie Secure. Trust only proxies you control, and configure them to sanitize forwarding headers.

After changing the bind-mounted configuration, recreate the container:

Terminal window
docker compose up -d --force-recreate edgewatch

You can verify both paths from the Docker host (replace the example hostname with the one configured above):

Terminal window
curl -i http://127.0.0.1:8080/api/v1/setup/status
curl -i -H 'Host: edgewatch.example.ts.net:8443' \
http://127.0.0.1:8080/api/v1/setup/status

Both requests should return a successful response. If the direct request works but the request with the proxy Host returns 421, correct web.allowed_hosts. The listener remains loopback-only; this setting approves the public name, not a new network bind address.

The console receives live updates over one long-lived Server-Sent Events response from /api/v1/stream. EdgeWatch writes a heartbeat comment every 25 seconds and sends X-Accel-Buffering: no, which nginx honors by default. Make sure the proxy:

  • passes each event through as it is written instead of buffering the response;
  • keeps an idle upstream read open for longer than the 25-second heartbeat;
  • does not compress or cache text/event-stream responses.

A minimal nginx location:

location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
proxy_read_timeout 120s;
}

Overwriting X-Forwarded-For with $remote_addr keeps a client-supplied header from reaching EdgeWatch. Tailscale Serve and Caddy stream responses without extra settings. When a proxy buffers the stream anyway, the console shows Reconnecting… or updates only after a reload, while ordinary pages keep working.

GET /healthz answers only {"status":"ready"}, {"status":"starting"}, or {"status":"unhealthy"}, needs no session, and accepts any Host, as the public status pages do, so an uptime checker can reach it through the proxy or on the loopback listener. The daemon answers it while it migrates the database too; until the console is up, the proxy gets 503 for every other path. It is rate limited to 120 requests a minute for each client; behind a proxy that web.trusted_proxies does not list, every client shares the proxy’s budget.

Keep the opt-in GET /metrics off the public proxy, even though it needs a bearer token. Let Prometheus scrape it on the loopback listener or a private network, and refuse the path at the proxy, for example in nginx:

location = /metrics {
return 404;
}

With Tailscale Serve, publish only the console’s path and scrape the listener from the host. EdgeWatch never adds the token to a response or a log; anyone who holds it can read the deployment’s aggregates, but nothing else.