Files
composes/paseo/README.md
T
tiennm99 23b9bf8bf6 docs: describe current state only, and fill in the stub READMEs
Drop the history and roadmap asides: which services predate the collection's
conventions, the unwired Open Web UI plan, the generic clone-and-troubleshoot
boilerplate. Services that publish ports or set `restart:` now simply say so.

couchbase, openvpn-as and traffmonetizer had two-line READMEs; give them the
ports, variables and storage the root README promises. traffmonetizer reads
${TOKEN} and had no .env.example, so add one.
2026-09-16 20:45:18 +07:00

119 lines
5.4 KiB
Markdown

# paseo
[Paseo](https://paseo.sh) — self-hosted daemon and web UI for running coding
agents. Built from a local `Dockerfile` that adds six agent CLIs, `gh`, Go,
Python, and shell tooling to the
[official image](https://paseo.sh/docs/docker), which ships none of it.
## Setup
1. Set the 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**,
then the `PASEO_PASSWORD` value:
```
paseo.example.com:443
```
4. In a terminal inside Paseo, log in once per tool you use — see
[Agents](#agents) — plus `gh auth login`.
The port must be typed by hand. The UI rejects a bare hostname, and the
auto-connect hint does not help: the daemon builds it from the `Host` header,
browsers drop the default `:443`, and the UI discards a hint with no port. What
you see instead is its `localhost:6767` placeholder, which in a browser means
your own machine.
If it stays on `localhost:6767` after you enter the address, clear site data —
the old entry is cached in `localStorage`.
## Environment
| Variable | Purpose |
| --- | --- |
| `PASEO_PASSWORD` | Web UI and API login, and the `paseo` user's `sudo` password. 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. |
| `PASEO_LABEL` | Container hostname, shown as the host label in the UI. Without it the label is a random container ID. |
| `GIT_NAME` / `GIT_EMAIL` | Git author and committer identity for agents and terminals. |
| `TZ` | Timezone for logs and agent shells. |
| `SHELL` | Shell for Paseo's terminals. Paseo reads `$SHELL` and falls back to `/bin/sh`, ignoring the login shell, so `chsh` has no effect. |
`PASEO_TRUSTED_PROXIES` matches the proxy's *source IP*, so hostnames are
rejected. The daemon trusts `X-Forwarded-Proto` from loopback only, but
Coolify's Traefik reaches it from the Docker bridge network — so it 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`, 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.
## Agents
| Agent | Command | Package | Log in with |
| --- | --- | --- | --- |
| Claude Code | `claude` | `@anthropic-ai/claude-code` | `claude` |
| Codex | `codex` | `@openai/codex` | `codex login` |
| opencode | `opencode` | `opencode-ai` | `opencode auth login` |
| Copilot CLI | `copilot` | `@github/copilot` | `/login` inside `copilot` |
| Pi | `pi` | `@earendil-works/pi-coding-agent` | `pi` |
| Oh My Pi | `omp` | `@oh-my-pi/pi-coding-agent` | `omp` |
All six come from npm, so another provider is one more entry on the `npm
install` line. Pi and Oh My Pi are separate projects sharing an ancestor; their
commands do not collide.
## Storage
| Volume | Mount | Holds |
| --- | --- | --- |
| `paseo-home` | `/home/paseo` | Daemon state, agent configs, credentials (`.claude`, `.codex`, `.config/*`) |
| `paseo-workspace` | `/workspace` | Code the agents work on |
Every agent CLI and `gh` keep their config under `/home/paseo`, so all logins
survive a redeploy — the base image points `CLAUDE_CONFIG_DIR`, `CODEX_HOME`
and the `XDG_*` variables into it. Dotfiles live there too, so a `.zshrc` or
oh-my-zsh install persists. Anything written outside `$HOME` (`chsh`,
`apt install`) is lost on rebuild.
## Image
| Tool | Source | Why not apt |
| --- | --- | --- |
| Agent CLIs (see [above](#agents)) | npm | — |
| Bun | Official `bun.sh` install script | Debian does not package it |
| `gh` | GitHub's signed apt repo | Debian does not package it |
| Go (`GO_VERSION`) | Official go.dev tarball | Debian 12 ships 1.19 |
| Python (`PYTHON_VERSION`) | `uv python install` | Debian 12 ships 3.11 |
| `less nano jq unzip zip lsof psmisc ugrep bfs zsh sudo` | apt | — |
Bump a language with a build arg, e.g. `--build-arg GO_VERSION=1.27.1`. `uv`
itself is installed too.
Constraints worth knowing before changing the `Dockerfile`:
- `$HOME` is `/home/paseo`, a volume that masks anything the build writes
there. Hence `/opt/python` and `/usr/local/go` rather than the defaults, and
hence npm rather than each vendor's `curl | bash` installer, which writes to
`$HOME/.local`. npm delivers the same native binaries regardless — every one
of these packages is a thin launcher with the real binary as a per-platform
optional dependency.
- Bun exists for `omp` alone, whose bin opens with `#!/usr/bin/env bun`.
Node runs the other five.
- The agent CLIs cannot auto-update, since `paseo` cannot write `/usr/local`.
Claude Code shows a notice at startup. Rebuild to update.
- The image stays root: the entrypoint chowns the volumes, then drops to the
`paseo` user (uid 1000) with `gosu`.
- `entrypoint.sh` sets the `paseo` password from `PASEO_PASSWORD` on every
start, while still root. `/etc/shadow` is not on a volume, so it reverts on
each recreate. An empty `PASEO_PASSWORD` leaves the account locked and `sudo`
unusable.
- `sudo` resets `PATH` to its `secure_path`, which excludes
`/usr/local/go/bin`. Use `sudo env PATH="$PATH" go ...` or the full path.