Context / why
For teams running Coder as their dev-environment layer, a natural next step is a self-hosted agent orchestration console — one place to spawn, monitor and hand off Claude Code / Codex / OpenCode agents across machines. Paseo (self-hosted daemon + web UI for exactly that) is a good fit, but the obvious "add it as an external app" approach opens a second, Coder-independent network exposure. This issue documents a zero-code, tunnel-managed embedding (Paseo daemon lives inside the workspace; the Coder tunnel is the only ingress) that we have validated end-to-end, and asks whether upstream would consider first-class support (see "Ask").
What we did (validated on coder v2.36.3 + Paseo 0.7.2)
- Paseo runs inside the Coder workspace container, listening on
127.0.0.1:6767 only. No host port mapping (docker port = empty).
- Exposed via a non-external
coder_app with subdomain = true — Coder's WireGuard tailnet is the single ingress; requests pass Coder auth (session + share = "owner") on every hop.
- Key blocker found: Paseo's web UI is an Expo SPA with absolute asset paths (
/_expo/static/...) and does not support a base path — so path-based apps render a blank page. Subdomain mode is mandatory.
- Second blocker found: Paseo's daemon validates the
Host header against daemon.hostnames. When Coder forwards a subdomain request the Host is <slug>--<ws>--<owner>.<wildcard>:<port>, which is rejected by default ({"error":"Invalid Host header"}). Fix: pre-write the daemon config in the agent startup script (see template below).
- Claude Code (with a custom
settings.json, e.g. a DeepSeek/Anthropic-compatible endpoint) is pre-installed in the workspace image; the daemon spawns it against the workspace's own project directory (/home/coder).
Benefits
- Zero extra network exposure: the only public endpoint stays Coder (auth + WireGuard tunnel).
- Per-workspace isolation out of the box: each user's daemon, agents and API keys live in their own workspace and die with it (
coder stop/delete = everything released) — matches Coder's lifecycle semantics.
- No upstream modification of Paseo: everything is config (
hostnames/cors) + the standard coder_app resource.
How to configure (minimal template)
resource "coder_agent" "main" {
startup_script = <<-EOF
export PASEO_HOME=/home/coder/.paseo
mkdir -p /home/coder/.paseo
cat > /home/coder/.paseo/config.json <<'PASEO_CFG'
{
"version": 1,
"daemon": {
"listen": "127.0.0.1:6767",
"hostnames": ["paseo--<workspace>--<owner>.<wildcard>", "localhost", "127.0.0.1"],
"cors": { "allowedOrigins": ["*"] },
"relay": { "enabled": false }
},
"app": { "baseUrl": "http://localhost:6767" }
}
PASEO_CFG
paseo daemon start --web-ui --listen 127.0.0.1:6767 > /home/coder/.paseo/daemon.log 2>&1 &
sleep 3
paseo project ls 2>/dev/null | grep -q "/home/coder" || paseo project create /home/coder > /dev/null 2>&1
EOF
}
resource "coder_app" "paseo" {
agent_id = coder_agent.main.id
slug = "paseo"
display_name = "Paseo"
url = "http://localhost:6767"
subdomain = true # mandatory: SPA assets are absolute-path
share = "owner"
healthcheck { url = "http://localhost:6767/api/health"; interval = 5; threshold = 10 }
}
Server side: CODER_WILDCARD_ACCESS_URL="*.example.com:7080" (no scheme; include the port or generated links miss it).
Caveats
hostnames is hard-coded per workspace slug today; a wildcard/parametrized allow-list would remove the coupling (see Ask).
- Logout invalidates access immediately — every request is re-checked by Coder auth (verified).
- One daemon per workspace (resource cost is small, but it is per-user).
- Paseo's permission model is daemon-wide (no user-level scoping yet) — the per-workspace-instance pattern is what makes it multi-tenant safe.
Ask (upstream)
- Would Coder consider a documented "embedded service app" pattern (non-external apps already proxy arbitrary TCP/HTTP to the agent; a cookbook entry for long-running daemons like Paseo would help)?
- For Paseo: a wildcard/parameterized
hostnames option would remove the per-workspace config coupling when embedded behind a proxy.
Environment: coder v2.36.3 (Docker Compose + PostgreSQL), Paseo 0.7.2, workspace image based on codercom/enterprise-base with @getpaseo/cli + @anthropic-ai/claude-code.
Context / why
For teams running Coder as their dev-environment layer, a natural next step is a self-hosted agent orchestration console — one place to spawn, monitor and hand off Claude Code / Codex / OpenCode agents across machines. Paseo (self-hosted daemon + web UI for exactly that) is a good fit, but the obvious "add it as an
externalapp" approach opens a second, Coder-independent network exposure. This issue documents a zero-code, tunnel-managed embedding (Paseo daemon lives inside the workspace; the Coder tunnel is the only ingress) that we have validated end-to-end, and asks whether upstream would consider first-class support (see "Ask").What we did (validated on coder v2.36.3 + Paseo 0.7.2)
127.0.0.1:6767only. No host port mapping (docker port= empty).coder_appwithsubdomain = true— Coder's WireGuard tailnet is the single ingress; requests pass Coder auth (session +share = "owner") on every hop./_expo/static/...) and does not support a base path — so path-based apps render a blank page. Subdomain mode is mandatory.Hostheader againstdaemon.hostnames. When Coder forwards a subdomain request the Host is<slug>--<ws>--<owner>.<wildcard>:<port>, which is rejected by default ({"error":"Invalid Host header"}). Fix: pre-write the daemon config in the agent startup script (see template below).settings.json, e.g. a DeepSeek/Anthropic-compatible endpoint) is pre-installed in the workspace image; the daemon spawns it against the workspace's own project directory (/home/coder).Benefits
coder stop/delete = everything released) — matches Coder's lifecycle semantics.hostnames/cors) + the standardcoder_appresource.How to configure (minimal template)
Server side:
CODER_WILDCARD_ACCESS_URL="*.example.com:7080"(no scheme; include the port or generated links miss it).Caveats
hostnamesis hard-coded per workspace slug today; a wildcard/parametrized allow-list would remove the coupling (see Ask).Ask (upstream)
hostnamesoption would remove the per-workspace config coupling when embedded behind a proxy.Environment: coder v2.36.3 (Docker Compose + PostgreSQL), Paseo 0.7.2, workspace image based on
codercom/enterprise-basewith@getpaseo/cli+@anthropic-ai/claude-code.