This change proposed using Cloudflare API for persistent tunnels. Superseded by temporary tunnel approach using 'cloudflared tunnel --url' which requires no API tokens, account IDs, or DNS configuration.
6.5 KiB
Context
Currently, tool instances are exposed via an API proxy endpoint that forwards requests from /instances/{id}/proxy/ to the internal Docker container. This creates latency, adds load to the API service, and doesn't support WebSocket features well. Cloudflare Tunnel offers a better architecture where each instance gets its own HTTPS subdomain.
Goals / Non-Goals
Goals:
- Each running tool instance gets a unique public HTTPS subdomain
- No manual DNS or reverse proxy configuration per instance
- Automatic cleanup when instances are stopped or deleted
- Support for WebSocket and real-time features (code-server terminal, jupyter kernels)
- Minimal latency compared to API proxy approach
Non-Goals:
- Custom domains per instance (use Cloudflare zone's wildcard)
- Advanced tunnel features (load balancing, failover, ingress rules)
- Replacing Traefik for the main app (API + frontend)
- Supporting non-HTTP protocols (TCP/UDP raw tunneling)
Decisions
Cloudflare API vs cloudflared CLI
Decision: Use the Cloudflare REST API to create/manage tunnels, not the cloudflared CLI.
Rationale:
- The API gives us programmatic control without parsing CLI output
- We can use
httpx(already a dependency) instead of subprocess calls - Easier to test and mock
Alternative considered: Running cloudflared tunnel create via subprocess
- Rejected: Fragile, harder to test, requires cloudflared binary in API container
Architecture: cloudflared as a separate container
Decision: Run cloudflared as a standalone Docker service that connects to Cloudflare and routes traffic.
Rationale:
- Separation of concerns: API manages tunnels, cloudflared handles connectivity
- The cloudflared container can access the Docker internal network where instances run
- Easier to scale/restart independently
┌─────────────────────────────────────────────────────────────┐
│ Cloudflare Edge │
└──────────────────────┬──────────────────────────────────────┘
│ HTTPS
┌──────────────────────▼──────────────────────────────────────┐
│ cloudflared container │
│ (connects to Cloudflare, receives traffic for *.zone) │
└──────────┬──────────────────────────────────────────────────┘
│ Docker network
┌──────────▼──────────────────────────────────────────────────┐
│ code-server container:8443 jupyter container:8888 │
│ (tool instances on Docker network with DNS names) │
└─────────────────────────────────────────────────────────────┘
Subdomain naming
Decision: Use instance-{short-uuid}.{zone} format (e.g., instance-a1b2c3d4.headquarter.commumedia.org)
Rationale:
- Predictable and URL-safe
- Short enough to be readable
- UUID ensures uniqueness without exposing internal IDs
Tunnel lifecycle
Decision: Create tunnel on instance start, delete on instance stop/delete.
Flow:
- User clicks "Start"
- Backend creates Cloudflare tunnel via API
- Backend creates DNS CNAME record:
instance-abc123→{tunnel-id}.cfargotunnel.com - Backend stores
tunnel_idandpublic_urlin ToolInstance - cloudflared container routes traffic to container:port
- On stop: delete DNS record, delete tunnel
cloudflared configuration
Decision: Use a single cloudflared container with dynamic config file updates.
Approach:
- The cloudflared container reads an
config.ymlfile mounted as a volume - The API writes ingress rules to this file when instances start/stop
- cloudflared automatically reloads the config (or we restart the container)
# /etc/cloudflared/config.yml
tunnel: {tunnel-token}
credentials-file: /etc/cloudflared/credentials.json
ingress:
- hostname: instance-abc123.headquarter.commumedia.org
service: http://code-server-repo-abc123:8443
- hostname: instance-xyz789.headquarter.commumedia.org
service: http://jupyter-repo-def:8888
- service: http_status:404
Authentication
Decision: Cloudflare tunnels provide HTTPS but do NOT handle app-level auth. Tool instances without built-in auth (like code-server) will be publicly accessible.
Rationale:
- Cloudflare Access could add auth, but adds complexity
- Many tools (code-server) have their own password/auth mechanisms
- Users should configure tool-level auth via ToolConfig
Mitigation: Document that users must configure tool passwords via ToolConfig (e.g., PASSWORD env for code-server).
Risks / Trade-offs
[Risk] Cloudflare API rate limits (1200 requests/5 min) → Mitigation: Tunnel creation is infrequent (user-initiated), unlikely to hit limits
[Risk] cloudflared container becomes a single point of failure → Mitigation: It's stateless; can be restarted quickly. All instances share one cloudflared.
[Risk] Subdomain enumeration exposes running instances → Mitigation: UUID-based names are hard to guess. Consider adding Cloudflare Access in future.
[Risk] cloudflared config file updates require container restart
→ Mitigation: Investigate cloudflared --no-autoupdate with config watch, or accept brief restart
[Risk] Tool instances publicly accessible without auth → Mitigation: Document security best practices, recommend setting tool passwords
Migration Plan
- Deploy cloudflared container with base config
- Add Cloudflare env vars to API container
- Deploy backend changes (tunnel service, updated lifecycle)
- Deploy frontend changes (use public_url instead of proxy)
- Test with code-server instance
- Remove old proxy endpoint code
Open Questions
- Should we add Cloudflare Access (Zero Trust) to protect instances?
- Do we need to support custom subdomains (e.g.,
myproject.headquarter.commumedia.org)? - Should we keep the proxy endpoint as a fallback?