108 lines
3.2 KiB
Markdown
108 lines
3.2 KiB
Markdown
# 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
|
||
|
||
- `zpackv2` certResolver already configured with Cloudflare DNS-01 challenge
|
||
- Stale `_acme-challenge` TXT records in Cloudflare blocked initial cert issuance — deleted manually
|
||
- Wildcard cert `*.zerolaghub.dev` issued successfully via Let's Encrypt
|
||
|
||
### Traefik dynamic config
|
||
|
||
```yaml
|
||
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=true`
|
||
- `DEV_IDE_HOST_SUFFIX=zerolaghub.dev`
|
||
- `DEV_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: true` in Traefik is the equivalent of Caddy's
|
||
`header_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/:id` compatibility cleanup once browser is confirmed
|