> For the complete documentation index, see [llms.txt](https://wiki.crosswatch.app/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://wiki.crosswatch.app/crosswatch/advanced-and-api/reverse-proxy-setup.md).

# Reverse proxy setup

Use a reverse proxy for TLS and clean URLs.

Keep CW on HTTP behind the proxy.

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

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

### Proxy requirements

CW 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 CW is behind a reverse proxy, configure **Trusted reverse proxies** in **Settings → Security**.

This makes CW 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 CW runs in Docker

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

Example:

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

Use:

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

### Webhooks and Watcher (important)

CW exposes inbound webhook endpoints.

See:

* [Webhook or Watcher](/crosswatch/settings/scrobbler/webhook-or-watcher.md)
* [Watcher](/crosswatch/settings/scrobbler/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 CW 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 CW 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 CW 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, CW should still behave as HTTPS to clients.

### Related topics

* [HTTPS/TLS](/crosswatch/advanced-and-api/https-and-tls.md) (TLS options and why the proxy approach is recommended)
* [Installation](/getting-started/installation.md) (ports, volumes, restart policy)


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## 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, and the optional `goal` query parameter:

```
GET https://wiki.crosswatch.app/crosswatch/advanced-and-api/reverse-proxy-setup.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

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.
