diff --git a/SELF_HOSTING_ADVANCED.md b/SELF_HOSTING_ADVANCED.md index 4f7209ccc..36f4aee1e 100644 --- a/SELF_HOSTING_ADVANCED.md +++ b/SELF_HOSTING_ADVANCED.md @@ -186,16 +186,47 @@ In production, put a reverse proxy in front of both the backend and frontend to ### Caddy (Recommended) +**Single-domain layout** — frontend and backend served on the same hostname (this is what `docker-compose.selfhost.yml` defaults to): + +``` +multica.example.com { + # WebSocket route — must come before the catch-all + @multica_ws path /ws /ws/* + handle @multica_ws { + reverse_proxy localhost:8080 { + flush_interval -1 + } + } + + # Everything else → frontend + reverse_proxy localhost:3000 +} +``` + +**Separate-domain layout** — frontend and backend on different hostnames: + ``` app.example.com { reverse_proxy localhost:3000 } api.example.com { + @multica_ws path /ws /ws/* + handle @multica_ws { + reverse_proxy localhost:8080 { + flush_interval -1 + } + } + reverse_proxy localhost:8080 } ``` +Two non-obvious bits inside the `/ws` block are worth calling out — both are common reasons real-time updates "stop working" on a Caddy-fronted self-host: + +- **`path /ws /ws/*` (not `/ws*`)** — bare `handle /ws` is an exact match, so future path variants under `/ws/` fall through to the frontend block. The obvious shortcut `handle /ws*` overcorrects in the other direction: Caddy's `*` is a glob without a path-segment boundary, so it would also catch unrelated paths like `/ws-foo`, which is a legitimate workspace URL (only the exact slug `ws` is reserved). Listing `/ws` and `/ws/*` explicitly covers both real cases without overreach. +- **`flush_interval -1`** — disables response buffering so WebSocket frames are forwarded as soon as they arrive. Without it, frames can sit behind Caddy's default flush window, which looks like delayed comments, missing typing indicators, or "comments only appear after a page refresh." + ### Nginx ```nginx diff --git a/apps/docs/content/docs/getting-started/self-hosting.zh.mdx b/apps/docs/content/docs/getting-started/self-hosting.zh.mdx index 07d8d08ef..cdb8b4d32 100644 --- a/apps/docs/content/docs/getting-started/self-hosting.zh.mdx +++ b/apps/docs/content/docs/getting-started/self-hosting.zh.mdx @@ -337,16 +337,47 @@ In production, put a reverse proxy in front of both the backend and frontend to ### Caddy (Recommended) +**Single-domain layout** — frontend and backend served on the same hostname (this is what `docker-compose.selfhost.yml` defaults to): + +``` +multica.example.com { + # WebSocket route — must come before the catch-all + @multica_ws path /ws /ws/* + handle @multica_ws { + reverse_proxy localhost:8080 { + flush_interval -1 + } + } + + # Everything else → frontend + reverse_proxy localhost:3000 +} +``` + +**Separate-domain layout** — frontend and backend on different hostnames: + ``` app.example.com { reverse_proxy localhost:3000 } api.example.com { + @multica_ws path /ws /ws/* + handle @multica_ws { + reverse_proxy localhost:8080 { + flush_interval -1 + } + } + reverse_proxy localhost:8080 } ``` +Two non-obvious bits inside the `/ws` block are worth calling out — both are common reasons real-time updates "stop working" on a Caddy-fronted self-host: + +- **`path /ws /ws/*` (not `/ws*`)** — bare `handle /ws` is an exact match, so future path variants under `/ws/` fall through to the frontend block. The obvious shortcut `handle /ws*` overcorrects in the other direction: Caddy's `*` is a glob without a path-segment boundary, so it would also catch unrelated paths like `/ws-foo`, which is a legitimate workspace URL (only the exact slug `ws` is reserved). Listing `/ws` and `/ws/*` explicitly covers both real cases without overreach. +- **`flush_interval -1`** — disables response buffering so WebSocket frames are forwarded as soon as they arrive. Without it, frames can sit behind Caddy's default flush window, which looks like delayed comments, missing typing indicators, or "comments only appear after a page refresh." + ### Nginx ```nginx