# Reverse proxy cookbook

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

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

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

## App env (always)

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

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](/docs/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; login/CSRF works behind the proxy
- [ ] WebSocket upgrade reaches Reverb; Echo connects (avatar / notifications)
- [ ] Production image rebuilt after `VITE_REVERB_*` changes
- [ ] Passkeys tested on the public HTTPS origin
