# Reverse proxies

Use a reverse proxy for TLS and clean URLs.

Keep CrossWatch on HTTP behind the proxy.

{% hint style="warning" %}
Do not expose CrossWatch directly to the public internet.

Use a VPN for remote access (WireGuard, Tailscale).
{% endhint %}

### Proxy requirements

CrossWatch runs on FastAPI + Uvicorn.

Your proxy must:

* Forward client headers (`X-Forwarded-*`).
* Support WebSocket upgrades.
* Pass `/webhook/*` requests through unchanged.
* Use longer timeouts for long-lived connections.

If you add proxy auth, keep `/webhook/*` reachable.

#### Trusted reverse proxies (important)

If CrossWatch is behind a reverse proxy, configure **Trusted reverse proxies** in **Settings → Security**.

This makes CrossWatch trust the forwarded client headers (`X-Forwarded-*`).

It improves secure cookie behavior and rate limiting accuracy.

{% hint style="warning" %}
Only trust proxy IPs you control.
{% endhint %}

{% hint style="warning" %}
Captures and some Tools can take **minutes** on large libraries.

If your proxy uses default timeouts (often `60–120s`), you’ll hit **504 Gateway Timeout**.

Set proxy timeouts to **at least 10 minutes** (`600s`).
{% endhint %}

### NGINX example

Typical setup:

* Public: `https://crosswatch.example.com`
* Upstream: `http://127.0.0.1:8787`

{% hint style="info" %}
Use a subdomain.

Avoid hosting under a sub-path like `/crosswatch/`.
{% endhint %}

{% code title="nginx.conf (server block)" %}

```nginx
server {
    listen 443 ssl http2;
    server_name crosswatch.example.com;

    # TLS config omitted. Use your normal Let's Encrypt / cert config here.

    # Large POSTs are not typical, but this avoids random 413s.
    client_max_body_size 25m;

    # CrossWatch (UI and API)
    location / {
        proxy_pass http://127.0.0.1:8787;

        # Preserve original request details
        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;

        # WebSockets (safe even if the endpoint is plain HTTP)
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        # Captures abd Tools can take minutes.
        # Use 600s (10m) minimum. Raise it if you still see 504s.
        proxy_read_timeout 600s;
        proxy_send_timeout 600s;

        # Don't let NGINX buffer long responses.
        proxy_buffering off;
    }

    # Webhooks should be fast and lossless.
    # If you use proxy auth, exempt this location.
    location ^~ /webhook/ {
        proxy_pass http://127.0.0.1:8787;

        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;

        # Avoid webhook edge-cases with buffering.
        proxy_request_buffering off;
        proxy_buffering off;

        proxy_read_timeout 120s;
        proxy_send_timeout 120s;
    }
}
```

{% endcode %}

#### If CrossWatch runs in Docker

Point `proxy_pass` to the container on a user-defined Docker network.

Example:

* NGINX container: `nginx`
* CrossWatch container: `crosswatch`
* Network: `proxy`

Use:

* `proxy_pass http://crosswatch:8787;`

### Webhooks and Watcher (important)

CrossWatch exposes inbound webhook endpoints.

See:

* [Webhook or Watcher](/getting-started/first-time-setup/what-do-you-need/webhook-or-watcher.md)
* [Watcher](/crosswatch/scrobble/watcher.md)

#### Webhooks (inbound)

Your proxy must:

* Allow `POST` to `/webhook/*`.
* Not rewrite the path.
* Not block unknown content types.
  * Plex can send form-style payloads.
  * Jellyfin/Emby plugins usually send JSON.

{% hint style="warning" %}
If you publish webhook endpoints as `https://...`, use a publicly trusted certificate.

Self-signed TLS certificates commonly break webhook delivery for Plex, Emby, and Jellyfin.

Use Let’s Encrypt (or similar) at the reverse proxy.
{% endhint %}

If you put CrossWatch behind login, do one of these:

1. Exempt `/webhook/*` from auth.
2. IP-allowlist your media server's IP for `/webhook/*`.
3. Keep webhooks LAN-only. Do not proxy them.

{% hint style="warning" %}
Your media server must reach your public DNS name from inside your LAN.

If it cannot, webhooks will fail.

Fix hairpin NAT. Or use a LAN hostname/IP.
{% endhint %}

#### Watcher (outbound)

Watcher mostly makes outbound calls to Plex/Jellyfin/Emby.

Reverse proxying CrossWatch is not required for Watcher itself.

One exception:

* Plex "ratings" helper uses: `POST /webhook/plexwatcher?uniqueID`

If you enable Plex ratings, your proxy must allow that endpoint. It must also preserve the query string (the `?uniqueID` token).

### Common proxy problems

#### 413 Request Entity Too Large

Increase `client_max_body_size`.

#### 502 / 504 / random disconnects

This happens most often when running:

* **Captures** (create/restore/compare)
* Analyzer / Exporter on large state

Fix: increase timeouts to **10 minutes** (`600s`) or more.

On NGINX, set:

* NGINX: `proxy_read_timeout` / `proxy_send_timeout`

{% hint style="info" %}
If you’re behind an upstream proxy/CDN (Cloudflare, etc.), it may have its own hard timeout.

In that case, bypass the CDN for your CrossWatch hostname.
{% endhint %}

#### WebSocket errors (UI features not updating)

Forward upgrade headers:

* `proxy_set_header Upgrade $http_upgrade;`
* `proxy_set_header Connection "upgrade";`

#### Wrong scheme (HTTP/HTTPS) in callbacks or redirects

Send:

* `X-Forwarded-Proto $scheme`

If you terminate TLS at the proxy, CrossWatch should still behave as HTTPS to clients.

### Related topics

* [HTTPS/TLS](/crosswatch/navigation/ui-settings/user-interface/https-tls.md) (TLS options and why the proxy approach is recommended)
* [Docker setup](/getting-started/docker-setup.md) (ports, volumes, restart policy)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://wiki.crosswatch.app/related-information/reverse-proxies.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
