3.2 KiB
2026-03-22 – Hosted Dev IDE via Traefik Wildcard
Summary
Long session. Explored multiple approaches to masking the raw API IP in the IDE browser URL. Ended with the correct architecture working and curl-verified.
Architecture Decision
Rejected: Traefik direct routing to container (removes API auth boundary, insecure)
Rejected: Caddy on API host (HSTS/HTTPS issues with zerolaghub.dev domain)
Chosen: Traefik on zlh-zpack-proxy → API → container
This is correct because:
- Traefik already handles TLS for the platform
- API remains the auth and proxy boundary
- No per-container DNS or routing side effects needed
- Clean hostname with HTTPS without purchasing a cert
What Was Built
Traefik wildcard TLS
zpackv2certResolver already configured with Cloudflare DNS-01 challenge- Stale
_acme-challengeTXT records in Cloudflare blocked initial cert issuance — deleted manually - Wildcard cert
*.zerolaghub.devissued successfully via Let's Encrypt
Traefik dynamic config
http:
routers:
dev-ide:
rule: "HostRegexp(`dev-{vmid:[0-9]+}.zerolaghub.dev`)"
entryPoints:
- websecure
service: dev-ide-api
tls:
certResolver: zpackv2
domains:
- main: "zerolaghub.dev"
sans:
- "*.zerolaghub.dev"
services:
dev-ide-api:
loadBalancer:
passHostHeader: true
servers:
- url: "http://10.60.0.245:4000"
passHostHeader: true is critical — preserves dev-6070.zerolaghub.dev
through to the API so handleHostedProxy can extract the vmid.
API devProxy.js
handleHostedProxy added — extracts vmid from Host header, validates
token, sets cookie, proxies to container. This was the missing piece that
caused 404s until the code was deployed.
.env aligned:
DEV_IDE_RETURN_HOSTED_URL=trueDEV_IDE_HOST_SUFFIX=zerolaghub.devDEV_IDE_PUBLIC_SCHEME=https
Curl-Verified Response Chain
GET https://dev-6070.zerolaghub.dev/?token=<valid>
→ 302 + Set-Cookie: zlh_dev_ide_token
GET https://dev-6070.zerolaghub.dev/ (with cookie)
→ 302 → /?folder=/home/dev/workspace
GET https://dev-6070.zerolaghub.dev/?folder=/home/dev/workspace
→ 200 (code-server HTML)
Full chain confirmed: Browser → Traefik → API → container:6000
Key Lessons
- ERR_CONNECTION_CLOSED from browser (but curl works) = H2 mismatch or wrong target. In this case it was the API not running the new code (404), not H2.
passHostHeader: truein Traefik is the equivalent of Caddy'sheader_up Host {host}— without it Express resolves relative redirects against the internal IP, leaking it to the browser.- Wildcard certs require DNS-01 challenge — stale TXT records in Cloudflare will block issuance silently. Check and clear them first.
- Traefik and the API are on different subnets (10.70.x vs 10.60.x) — always verify cross-subnet reachability with curl before debugging proxy config.
Remaining
- Browser validation (curl is confirmed, browser WebSocket not yet tested)
- Portal "Open IDE" button confirmation under hosted flow
- Legacy
/__ide/:idcompatibility cleanup once browser is confirmed