last-verified: 2026-04-24 (draft ships with v0.4; founder VM test pending before v0.4.0 tag)
You like Caddy because TLS is handled automatically and the config file is five lines. You want to front Compendiq with Caddy on a public (or LAN + internal CA) hostname and keep the LLM / presence streams and collaborative-editing WebSockets working — Caddy's gotchas with Compendiq are default response buffering on SSE and a too-short read timeout on long-lived sockets.
flowchart LR
u[User browser]
u -->|HTTPS :443| cy[Caddy<br/>auto-TLS]
cy -->|HTTP :8081| cf[Compendiq frontend]
cf -->|HTTP :3051| cb[Compendiq backend]
cb --> pg[(PostgreSQL)]
cb --> rd[(Redis)]
- Caddy v2.7+ installed on the host (systemd unit, Docker, or binary — all equivalent).
- Compendiq running locally on
127.0.0.1:8081(bundled nginx inside the frontend image; bind to loopback so only Caddy reaches it). - DNS record for your hostname pointing at the Caddy host. Public internet + port 80/443 open → Caddy auto-fetches a public cert. Private DNS → use Caddy's internal CA (see §2.b below).
In docker-compose.yml:
frontend:
ports:
- "127.0.0.1:8081:8081"compendiq.corp.example.com {
# SSE streams: presence viewer list (v0.4 #301) and every LLM
# endpoint. `flush_interval -1` disables response buffering on
# THIS matcher only — everything else keeps normal buffering for
# best throughput on static assets.
@sse {
path_regexp sse ^/api/(pages/[^/]+/presence|llm/.*)$
}
reverse_proxy @sse 127.0.0.1:8081 {
# Belt-and-suspenders: Caddy auto-streams text/event-stream anyway
flush_interval -1
transport http {
read_timeout 1h
}
}
# Collaborative editing (Yjs WebSocket). Caddy upgrades WebSockets
# automatically; the matcher exists so we can pin a 1h read timeout
# without stretching it across the SPA. Browsers use HTTP/1.1 Upgrade
# (RFC 6455), not RFC 8441 Extended CONNECT — Caddy still accepts
# HTTP/1.1 on an HTTP/2 site for that handshake.
@collab {
path /api/collab*
}
reverse_proxy @collab 127.0.0.1:8081 {
transport http {
read_timeout 1h
}
}
# Everything else.
reverse_proxy 127.0.0.1:8081
# Raise the request body cap for diagram / image uploads.
request_body {
max_size 30MB
}
}Reload Caddy:
sudo caddy reload --config /etc/caddy/CaddyfileCaddy auto-issues a Let's Encrypt cert on first request. No additional config needed when the hostname resolves publicly.
Replace the site block's first line with:
compendiq.corp.internal {
tls internal
# ...same @sse and @collab matchers + reverse_proxy blocks as above
}tls internal tells Caddy to issue certs from its embedded CA (~/.local/share/caddy/pki/authorities/local/root.crt). Distribute that root to client machines so browsers trust it.
| Variable | Example | Why |
|---|---|---|
FRONTEND_URL |
https://compendiq.corp.example.com |
CORS origin + setup-wizard redirect target. Must match what Caddy serves. |
FRONTEND_PORT |
8081 |
Leave as default; reverse_proxy targets this on loopback. |
Compendiq's trustProxy: true honours Caddy's X-Forwarded-* headers (which Caddy sets automatically), so no extra env vars are needed for client-IP preservation.
1. LLM chat hangs on a spinning cursor; presence avatars never appear.
The @sse matcher isn't firing — Caddy is routing the request through the generic reverse_proxy which buffers. Two usual causes:
path_regexpis case-sensitive and anchored. Check your actual path:/api/pages/abc/presencematches,/api/pages/abc/presence/heartbeat(if a future backend adds it) does not.- The matcher block order matters in a Caddyfile. Put the
@ssematcher before the catch-allreverse_proxyline — Caddy evaluates top-to-bottom.
1b. Collaborative editing never connects, or the socket dies around a minute in.
The @collab matcher (path /api/collab*) must sit before the catch-all reverse_proxy, with read_timeout 1h on that proxy. Caddy upgrades WebSockets on its own — do not add a header_up Connection that overrides the Upgrade. If the matcher is missing, the generic proxy's default read timeout cuts the idle socket.
2. "502 Bad Gateway" on first request.
Compendiq isn't bound on 127.0.0.1:8081. Run ss -tlnp | grep 8081 — if the bind is 0.0.0.0:8081 or missing entirely, fix the Compose ports: line and restart the frontend container.
3. Uploads over ~10 MB return 413.
request_body max_size defaults to 10 MB in Caddy. Raise it (see the snippet above — 30 MB is a safe starting point for a 25 MB diagram + JSON overhead).
# Reachability + cert.
curl -I https://compendiq.corp.example.com/api/health
# Expect: 200 OK, valid TLS (from Let's Encrypt or Caddy's internal CA).
# SSE streaming (presence is the fastest sanity check — no LLM required).
# Grab a JWT from the browser (DevTools → Application → Local Storage →
# `compendiq-auth` → state.accessToken), substitute <TOKEN>.
curl -N -H "Authorization: Bearer <TOKEN>" \
https://compendiq.corp.example.com/api/pages/<pageId>/presence
# Expect: one `event: presence` frame every few seconds, streamed live.
# If you get a single buffered blob at the end, `flush_interval -1` is
# not bound to the route — revisit the @sse matcher.Before tagging v0.4.0: the founder should run the full verification on a fresh VM and update the
_last-verified_stamp at the top of this file.