docs(paseo): add setup steps and tighten the README

The pairing screen needs an explicit port, which is the least obvious
part of getting connected. Lead with the setup steps and cut the
explanation around them down to what a reader has to act on.
This commit is contained in:
tiennm99 committed 2026-09-16 15:49:30 +07:00
1 parent 5edb865597
commit 341469ff43
1 file changed
+52 -42
+52 -42
View File
@@ -1,66 +1,76 @@
# paseo # paseo
[Paseo](https://paseo.sh) — a self-hosted daemon and web UI for running coding [Paseo](https://paseo.sh) — self-hosted daemon and web UI for running coding
agents, from the [official image](https://paseo.sh/docs/docker). 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 ## Setup
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.
npm installs the same native binary as Anthropic's standalone installer, so 1. Set the three variables from `.env.example` in Coolify or Dokploy.
there is nothing to gain by switching. The `curl | bash` installer is in fact 2. Point the domain at port `6767` and deploy.
the wrong choice here — it writes to `$HOME/.local`, and `$HOME` is 3. Open the domain. At the pairing screen enter the host **with the port**:
`/home/paseo`, a volume mount that hides anything baked in at build time.
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 Then the `PASEO_PASSWORD` value.
mounted volumes and then drops to the unprivileged `paseo` user with `gosu`.
Coolify and Dokploy build the image themselves from the compose `build:` The port is required — the UI rejects a bare hostname. You must type the
stanza; there is nothing to push. 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 ## Environment
| Variable | Purpose | | Variable | Purpose |
| --- | --- | | --- | --- |
| `PASEO_PASSWORD` | Auth for the daemon API and WebSocket | | `PASEO_PASSWORD` | Web UI and API login. Generate with `openssl rand -base64 24`. |
| `PASEO_HOSTNAMES` | Comma-separated DNS names allowed to reach the daemon, e.g. `paseo.example.com,.lan`. IPs and localhost always pass. | | `PASEO_HOSTNAMES` | Domains allowed to reach the daemon, comma-separated. Your domain must be listed. |
| `PASEO_TRUSTED_PROXIES` | Proxy addresses whose `X-Forwarded-*` headers the daemon believes. Required behind a TLS-terminating proxy — see below. | | `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 `PASEO_TRUSTED_PROXIES` matches the *source IP* of the proxy, so hostnames are
appear in `PASEO_HOSTNAMES` or requests are rejected. 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 ## Networking
Listens on `6767`. No ports are published — point the domain at that port in Listens on `6767`, published nowhere — the platform maps the domain to it, so
Coolify or Dokploy. See the [root README](../README.md) for why. `localhost:6767` `localhost:6767` on the host refuses connections. See the
on the host will refuse connections; reach the daemon through its domain. [root README](../README.md) for why.
`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.
## Storage ## Storage
Two named volumes:
| Volume | Mount | Holds | | 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 | | `paseo-workspace` | `/workspace` | Code the agents work on |
Claude Code's own config lives under `/home/paseo/.claude`, so it persists in Claude Code's config lives in `/home/paseo/.claude`, so logins survive a
`paseo-home` — credentials survive a redeploy. redeploy.
The daemon runs as uid/gid `1000:1000`; anything bind-mounted in its place must ## Image
be writable by that user.
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`.