Skip to content

fix(terminal): support same-origin WebSocket for Cloudflare tunnel#498

Draft
simonjcarr wants to merge 1 commit intomainfrom
claude/fix-cloudflare-tunnel-terminal-ZVhJB
Draft

fix(terminal): support same-origin WebSocket for Cloudflare tunnel#498
simonjcarr wants to merge 1 commit intomainfrom
claude/fix-cloudflare-tunnel-terminal-ZVhJB

Conversation

@simonjcarr
Copy link
Copy Markdown
Collaborator

Summary

When CT-Ops is accessed through a Cloudflare tunnel (or any reverse proxy that only exposes the web app's hostname), the in-app terminal never connects. The browser reads INGEST_WS_URL from the server action and opens the WebSocket directly to that URL — but a tunnel typically only fronts the web app on port 3000, so the absolute ws://host:8080 URL is unreachable from a remote browser.

This PR adds a same-origin mode so the WebSocket can traverse the same tunnel as the rest of the web traffic.

Changes

  • apps/web/lib/actions/terminal.ts — if INGEST_WS_URL is blank, the server action returns a path-only URL (/ws/terminal/<id>). Absolute URLs still work unchanged. http(s):// values are rewritten to ws(s):// for convenience.
  • apps/web/components/terminal/terminal-session.tsx — when the returned URL is path-only, the browser resolves it against window.location (picking wss: for HTTPS pages, ws: for HTTP).
  • docker-compose.single.yml — switched the default operator from :- to - so an explicit empty INGEST_WS_URL in .env is no longer silently replaced with the localhost default. Existing deployments are unchanged.
  • .env.example — documents the two modes (direct vs. same-origin reverse proxy).
  • apps/docs/docs/features/terminal.md — new "Deployment: Reverse Proxies and Cloudflare Tunnels" section with cloudflared, nginx, and Caddy examples, plus a troubleshooting checklist.

How to use behind a Cloudflare tunnel

  1. Add an ingress rule routing /ws/terminal/* to localhost:8080 (full example in the docs).
  2. Set INGEST_WS_URL= (empty) in .env and restart the web container.
  3. Terminal opens over wss://<tunnel-host>/ws/terminal/<id>, which goes through the same tunnel.

Absolute INGEST_WS_URL values remain the default, so existing LAN deployments are unaffected.

Test plan

  • Local LAN deployment (INGEST_WS_URL=ws://<lan-ip>:8080) — terminal still connects directly
  • Cloudflare tunnel deployment with INGEST_WS_URL= and path-based ingress rule — terminal connects via wss://<tunnel-host>/ws/terminal/...
  • nginx reverse proxy with /ws/terminal/ upstream to ingest:8080 — terminal connects
  • INGEST_WS_URL=https://... and http://... normalised to wss:// / ws://
  • pnpm run build (TypeScript) — clean
  • go build ./... in apps/ingest — clean

The browser-side terminal used to connect directly to the absolute
INGEST_WS_URL (ws://host:8080). Behind a Cloudflare tunnel only the web
app's hostname is publicly reachable, so the direct ingest port is
unreachable from a remote browser and the terminal never attaches.

INGEST_WS_URL now accepts an empty value: the server action returns a
path-only /ws/terminal/<id> URL and the browser resolves it against
window.location, so the WebSocket traverses the same tunnel/reverse
proxy as the rest of the web traffic. Absolute URLs still work
unchanged. http(s):// is rewritten to ws(s):// for convenience.

Docs updated with Cloudflare Tunnel, nginx, and Caddy routing examples
for /ws/terminal/* and a troubleshooting checklist.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants