# Reverse proxy cookbook

Source: https://docs.qiick.io/externa/reverse-proxy

Operator cookbook for TLS termination and WebSocket upgrades in front of Externa (Docker Compose or bare metal).

> **Related**
> [Deploy with Docker](https://docs.qiick.io/externa/deploy-with-docker) · [Deployment](https://docs.qiick.io/externa/deployment) · [Reverb & Echo](https://docs.qiick.io/externa/reverb-and-echo) · [Passkeys](https://docs.qiick.io/externa/passkeys) · [Environment variables](https://docs.qiick.io/externa/environment-variables)

## App env (always)

```env
APP_URL=https://externa.example.com
TRUSTED_PROXIES=*
# or comma-separated proxy CIDRs
SESSION_SECURE_COOKIE=true
```

> Before **1.3.0**, `TRUSTED_PROXIES` was documented in Docker samples but not read (`config/trustedproxy.php` was missing). From **1.3.0** ([core PR #295](https://github.com/qiick-io/externa-core/pull/295) / [#271](https://github.com/qiick-io/externa-core/issues/271)), Laravel’s `TrustProxies` middleware reads `config('trustedproxy.proxies')` from that env. Behind Caddy / Traefik / nginx this fixes **client IP**, **HTTPS detection** (secure cookies / generated URLs), **per-IP rate limits**, and **API key IP allowlists**. Unset still trusts **no** proxy (forwarded headers ignored). Upgraders: set `TRUSTED_PROXIES` to `*` or your proxy CIDRs, clear config cache, and verify login / rate limits / key IP allowlists see the real client.

Browser-facing Reverb (baked into the Vite bundle — rebuild image after changes):

```env
REVERB_HOST=externa.example.com
REVERB_PORT=443
REVERB_SCHEME=https
VITE_REVERB_HOST="${REVERB_HOST}"
VITE_REVERB_PORT="${REVERB_PORT}"
VITE_REVERB_SCHEME="${REVERB_SCHEME}"
```

**Passkeys** need a secure context: HTTPS `APP_URL` matching Fortify WebAuthn relying party — [Passkeys](https://docs.qiick.io/externa/passkeys#requirements).

Compose defaults: app published **`APP_PORT`→80** (prod sample often `8080`), Reverb **`REVERB_HOST_PORT`→8080** (often host `8081`). Proxy to those upstreams (or to service DNS `app:80` / `reverb:8080` on the Compose network).

## Caddy

```caddyfile
externa.example.com {
	encode gzip

	@websockets {
		header Connection *Upgrade*
		header Upgrade websocket
	}

	# Reverb WebSockets (path depends on your Echo setup — default Laravel Reverb)
	handle /app/* {
		reverse_proxy reverb:8080
	}

	handle {
		reverse_proxy app:80
	}
}
```

If Echo connects on a dedicated host/port, point a second site block at `reverb:8080` with TLS.

## Traefik (Docker labels sketch)

```yaml
services:
  app:
    labels:
      - traefik.enable=true
      - traefik.http.routers.externa.rule=Host(`externa.example.com`)
      - traefik.http.routers.externa.entrypoints=websecure
      - traefik.http.routers.externa.tls.certresolver=le
      - traefik.http.services.externa.loadbalancer.server.port=80

  reverb:
    labels:
      - traefik.enable=true
      - traefik.http.routers.externa-ws.rule=Host(`externa.example.com`) && PathPrefix(`/app`)
      - traefik.http.routers.externa-ws.entrypoints=websecure
      - traefik.http.routers.externa-ws.tls.certresolver=le
      - traefik.http.services.externa-ws.loadbalancer.server.port=8080
```

Ensure Traefik forwards `X-Forwarded-*` (default) so Laravel trusted proxies work with `TRUSTED_PROXIES`.

## nginx

```nginx
map $http_upgrade $connection_upgrade {
    default upgrade;
    '' close;
}

server {
    listen 443 ssl http2;
    server_name externa.example.com;

    # ssl_certificate …;
    # ssl_certificate_key …;

    location /app/ {
        proxy_pass http://127.0.0.1:8081;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_read_timeout 60s;
    }

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_read_timeout 600s;  # long AI / SSE if needed
    }
}
```

Adjust upstream ports to your `APP_PORT` / `REVERB_HOST_PORT`.

## Checklist

- [ ] `APP_URL` is HTTPS and matches the browser host
- [ ] `TRUSTED_PROXIES` set (`*` or proxy CIDRs); login/CSRF works behind the proxy
- [ ] After upgrade to 1.3.0+: client IP / HTTPS / per-IP rate limits / API key IP allowlists verified with real client (not the proxy IP)
- [ ] WebSocket upgrade reaches Reverb; Echo connects (avatar / notifications)
- [ ] Production image rebuilt after `VITE_REVERB_*` changes
- [ ] Passkeys tested on the public HTTPS origin
