Watchflare docs
Sur cette page
Cette page n'est pas encore disponible en français. Vous lisez la version anglaise.

Reverse proxy configuration

Configure Traefik, Nginx, or Caddy in front of the Watchflare Hub. Covers HTTP reverse proxy for the dashboard and TCP passthrough for the gRPC port.

The Hub exposes two ports with different proxying requirements:

PortProtocolProxy type
8080HTTPStandard reverse proxy — TLS termination here
50051gRPC / TLS 1.3TCP passthrough — no TLS termination

Warning

The gRPC port must be proxied at the TCP level, without TLS termination. Agents pin the Hub’s CA certificate at registration. If the proxy presents a different certificate, every agent will refuse to connect.


Traefik

Tested: ✅ Traefik v3

Traefik needs two routes: an HTTP reverse proxy for the dashboard, and a TCP passthrough for gRPC.

Static config

traefik.yml yaml
entryPoints:
  web:
    address: ":80"
  websecure:
    address: ":443"
  grpc:
    address: ":50051"

certificatesResolvers:
  letsencrypt:
    acme:
      email: you@example.com
      storage: /letsencrypt/acme.json
      httpChallenge:
        entryPoint: web

Restart Traefik after editing the static config. Dynamic config changes are hot-reloaded.

Dynamic config

rules.d/watchflare.yml yaml
http:
  routers:
    watchflare-http:
      entryPoints: [web]
      rule: "Host(`watchflare.example.com`)"
      middlewares: [redirect-https]
      service: watchflare-http

    watchflare-https:
      entryPoints: [websecure]
      rule: "Host(`watchflare.example.com`)"
      tls:
        certResolver: letsencrypt
      service: watchflare-http

  middlewares:
    redirect-https:
      redirectScheme:
        scheme: https
        permanent: true

  services:
    watchflare-http:
      loadBalancer:
        servers:
          - url: "http://HUB_IP:8080"

tcp:
  routers:
    watchflare-grpc:
      entryPoints: [grpc]
      rule: "HostSNI(`*`)"
      tls:
        passthrough: true
      service: watchflare-grpc

  services:
    watchflare-grpc:
      loadBalancer:
        servers:
          - address: "HUB_IP:50051"

Replace HUB_IP with the IP or hostname of the server running the Hub, and watchflare.example.com with your domain.

Docker Compose labels

If Traefik and the Hub run in the same Compose stack:

docker-compose.yml yaml
services:
  traefik:
    image: traefik:v3
    ports:
      - "80:80"
      - "443:443"
      - "50051:50051"
    command:
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--entrypoints.grpc.address=:50051"
      - "--certificatesresolvers.letsencrypt.acme.email=you@example.com"
      - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
      - "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"

  watchflare:
    labels:
      - "traefik.enable=true"
      # HTTP → HTTPS
      - "traefik.http.routers.watchflare.rule=Host(`watchflare.example.com`)"
      - "traefik.http.routers.watchflare.entrypoints=websecure"
      - "traefik.http.routers.watchflare.tls.certresolver=letsencrypt"
      - "traefik.http.services.watchflare.loadbalancer.server.port=8080"
      # gRPC TCP passthrough
      - "traefik.tcp.routers.watchflare-grpc.entrypoints=grpc"
      - "traefik.tcp.routers.watchflare-grpc.rule=HostSNI(`*`)"
      - "traefik.tcp.routers.watchflare-grpc.tls.passthrough=true"
      - "traefik.tcp.services.watchflare-grpc.loadbalancer.server.port=50051"

Nginx

Tested: ⚠️ Not tested — configuration based on Nginx documentation

Nginx requires the stream module for TCP passthrough on the gRPC port (--with-stream at compile time, included in most Linux distributions).

nginx.conf nginx
# HTTPS reverse proxy (dashboard)
server {
    listen 443 ssl;
    server_name watchflare.example.com;

    ssl_certificate     /etc/letsencrypt/live/watchflare.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/watchflare.example.com/privkey.pem;

    location / {
        proxy_pass http://HUB_IP:8080;
        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;

        # Required for SSE (real-time dashboard updates)
        proxy_http_version 1.1;
        proxy_set_header   Connection "";
        proxy_buffering    off;
        proxy_cache        off;
        proxy_read_timeout 86400s;
    }
}

server {
    listen 80;
    server_name watchflare.example.com;
    return 301 https://$host$request_uri;
}

# TCP passthrough (gRPC — do not terminate TLS)
stream {
    server {
        listen 50051;
        proxy_pass HUB_IP:50051;
    }
}

The stream {} block must be at the top level of nginx.conf, not inside an http {} block.

Warning

proxy_http_version 1.1, proxy_set_header Connection "", and proxy_buffering off are all required in the HTTP block. Without them, the SSE stream that drives real-time host status and metrics updates will be buffered and the dashboard will not update live.


Caddy

Tested: ⚠️ Not tested — configuration based on Caddy documentation

Caddy handles HTTPS and certificate renewal automatically. For the gRPC TCP passthrough, the caddy-l4 plugin is required.

HTTP (dashboard) — standard Caddyfile, no plugin needed:

Caddyfile plaintext
watchflare.example.com {
    reverse_proxy HUB_IP:8080
}

gRPC TCP passthrough — requires caddy-l4. Add a layer4 block in a JSON config or via the xcaddy build with the plugin. Refer to the caddy-l4 documentation for syntax.


Why HostSNI("*")

During the TLS handshake, the agent sends an SNI equal to tls_server in agent.conf — this defaults to watchflare (the certificate CN generated by the Hub). A rule like HostSNI("watchflare") would need to match this value exactly.

Using HostSNI("*") avoids mismatches if the certificate CN changes (e.g. when switching to TLS_MODE=custom with a different CN). It is safe because port 50051 is dedicated to Watchflare gRPC — security is enforced by TLS 1.3 and per-request HMAC authentication, not by SNI filtering.


Rate limiting

The Hub’s dashboard is a single-page app: a cold page load fetches dozens of static assets from /_app/immutable/* in parallel, more so on heavier pages. If your proxy enforces a request rate limit, the burst must be high enough to absorb a full page load. A burst that is too low returns 429 for some of those assets, which the browser reports as NS_ERROR_CORRUPTED_CONTENT and a failed page load (the dashboard then shows an “Internal Error” page).

Warning

Rate limiting immutable static assets by request count rarely adds value and easily breaks the dashboard. Use a generous burst (a few hundred), or scope the rate limit to API paths only.

The values shown below are only examples: size the burst to your own page weight and client count, there is no required number.

For Traefik, this is the burst value on the rateLimit middleware:

rules.d/watchflare.yml yaml
http:
  middlewares:
    watchflare-ratelimit:
      rateLimit:
        average: 300 # example, tune to your setup
        burst: 300   # example, tune to your setup

For Nginx, use limit_req with a burst large enough for a full page load (add nodelay so the burst is served immediately, not queued):

nginx.conf nginx
# http {} context
limit_req_zone $binary_remote_addr zone=watchflare:10m rate=300r/s; # example

# inside the location block
location / {
    limit_req zone=watchflare burst=300 nodelay; # example, tune to your setup
    proxy_pass http://HUB_IP:8080;
    # ... your other proxy_set_header / SSE settings
}

Caddy has no built-in rate limiting, so there is nothing to tune by default. If you add rate limiting through a plugin, size it generously or scope it to API paths only, following the same principle.


After setup

Once HTTPS is working, update your .env to ensure session cookies are correctly marked Secure:

.env bash
COOKIE_DOMAIN=watchflare.example.com

See HTTPS setup for details on cookie security and how to verify it works.