diff --git a/paseo/README.md b/paseo/README.md index bb84331..b231870 100644 --- a/paseo/README.md +++ b/paseo/README.md @@ -1,66 +1,76 @@ # paseo -[Paseo](https://paseo.sh) — a self-hosted daemon and web UI for running coding -agents, from the [official image](https://paseo.sh/docs/docker). +[Paseo](https://paseo.sh) — self-hosted daemon and web UI for running coding +agents. Built from a local `Dockerfile` that adds Claude Code to the +[official image](https://paseo.sh/docs/docker), which ships no agent CLIs. -Built from a local `Dockerfile` rather than the upstream image directly: the -official image ships no provider CLIs, so this one layers Claude Code on top -via `npm install -g @anthropic-ai/claude-code`. Add other providers -(`@openai/codex`, `opencode-ai`) to that same line if you need them. +## Setup -npm installs the same native binary as Anthropic's standalone installer, so -there is nothing to gain by switching. The `curl | bash` installer is in fact -the wrong choice here — it writes to `$HOME/.local`, and `$HOME` is -`/home/paseo`, a volume mount that hides anything baked in at build time. +1. Set the three variables from `.env.example` in Coolify or Dokploy. +2. Point the domain at port `6767` and deploy. +3. Open the domain. At the pairing screen enter the host **with the port**: -Claude Code cannot auto-update, since `paseo` can't write `/usr/local`; expect -a one-time notice at startup. Rebuild the image to pick up a new version. + ``` + paseo.example.com:443 + ``` -The image intentionally keeps running as root — its entrypoint chowns the -mounted volumes and then drops to the unprivileged `paseo` user with `gosu`. + Then the `PASEO_PASSWORD` value. -Coolify and Dokploy build the image themselves from the compose `build:` -stanza; there is nothing to push. +The port is required — the UI rejects a bare hostname. You must type the +address yourself: the daemon builds its auto-connect hint from the `Host` +header, browsers drop the default `:443`, and the UI discards a hint with no +port. It then shows its built-in `localhost:6767` placeholder, which in a +browser means your own machine. + +Still stuck on `localhost:6767` after entering the address? Clear site data — +the old entry is cached in `localStorage`. ## Environment | Variable | Purpose | | --- | --- | -| `PASEO_PASSWORD` | Auth for the daemon API and WebSocket | -| `PASEO_HOSTNAMES` | Comma-separated DNS names allowed to reach the daemon, e.g. `paseo.example.com,.lan`. IPs and localhost always pass. | -| `PASEO_TRUSTED_PROXIES` | Proxy addresses whose `X-Forwarded-*` headers the daemon believes. Required behind a TLS-terminating proxy — see below. | +| `PASEO_PASSWORD` | Web UI and API login. Generate with `openssl rand -base64 24`. | +| `PASEO_HOSTNAMES` | Domains allowed to reach the daemon, comma-separated. Your domain must be listed. | +| `PASEO_TRUSTED_PROXIES` | Set to `uniquelocal`, or the UI loads but never connects. | -Generate a password with `openssl rand -base64 24`. The proxied domain must -appear in `PASEO_HOSTNAMES` or requests are rejected. +`PASEO_TRUSTED_PROXIES` matches the *source IP* of the proxy, so hostnames are +rejected. By default the daemon believes `X-Forwarded-Proto` only from +loopback, but Coolify's Traefik reaches it from the Docker bridge network. It +therefore reads the request as plain HTTP, tells the UI to use `ws://` on an +`https://` page, and the browser blocks that as mixed content. `uniquelocal` +covers the private ranges Docker uses; an exact CIDR works too, but Coolify +assigns a fresh subnet per project. ## Networking -Listens on `6767`. No ports are published — point the domain at that port in -Coolify or Dokploy. See the [root README](../README.md) for why. `localhost:6767` -on the host will refuse connections; reach the daemon through its domain. - -`PASEO_TRUSTED_PROXIES` must be set, or the web UI loads but never connects. -The daemon trusts `X-Forwarded-Proto` from loopback only by default. Coolify's -Traefik reaches it from the Docker bridge network instead, so the daemon -concludes the request was plain HTTP and hands the UI `useTls: false`. The UI -then builds a `ws://` URL from an `https://` page, the browser blocks it as -mixed content, and the UI falls back to its built-in `localhost:6767` default — -which in a browser means the viewer's own machine, not the server. - -`uniquelocal` covers the private ranges Docker uses. A specific CIDR works too, -but Coolify assigns a fresh subnet per project, so it will not survive a move. +Listens on `6767`, published nowhere — the platform maps the domain to it, so +`localhost:6767` on the host refuses connections. See the +[root README](../README.md) for why. ## Storage -Two named volumes: - | Volume | Mount | Holds | | --- | --- | --- | -| `paseo-home` | `/home/paseo` | Daemon state, agent configs, credentials (`.codex`, `.claude`) | +| `paseo-home` | `/home/paseo` | Daemon state, agent configs, credentials (`.claude`, `.codex`) | | `paseo-workspace` | `/workspace` | Code the agents work on | -Claude Code's own config lives under `/home/paseo/.claude`, so it persists in -`paseo-home` — credentials survive a redeploy. +Claude Code's config lives in `/home/paseo/.claude`, so logins survive a +redeploy. -The daemon runs as uid/gid `1000:1000`; anything bind-mounted in its place must -be writable by that user. +## Image + +Add other providers to the `npm install` line in the `Dockerfile`: + +```dockerfile +RUN npm install -g @anthropic-ai/claude-code @openai/codex opencode-ai +``` + +Notes for anyone tempted to change it: + +- npm installs the same native binary as Anthropic's standalone installer. + Don't swap in `curl | bash` — it writes to `$HOME/.local`, and `$HOME` is + `/home/paseo`, a volume mount that hides anything baked in at build time. +- Claude Code can't auto-update (`paseo` can't write `/usr/local`), so it shows + a notice at startup. Rebuild to update. +- The image stays root on purpose: the entrypoint chowns the volumes, then + drops to the `paseo` user (uid 1000) with `gosu`.