Go 1.26.8 from the official tarball and Python 3.12 via uv, since Debian 12 carries 1.19 and 3.11. Both land outside $HOME, which the paseo-home volume would otherwise mask. Hostname and timezone come from the environment rather than being fixed in the compose file. Paseo uses the container hostname as the host label in its web UI, so without one the UI shows a random container ID.
3.8 KiB
paseo
Paseo — self-hosted daemon and web UI for running coding
agents. Built from a local Dockerfile that adds Claude Code, gh, Go,
Python, and shell tooling to the official image,
which ships none of it.
Setup
-
Set the variables from
.env.examplein Coolify or Dokploy. -
Point the domain at port
6767and deploy. -
Open the domain. At the pairing screen enter the host with the port:
paseo.example.com:443Then the
PASEO_PASSWORDvalue. -
In a terminal inside Paseo, log in once:
claudeandgh auth login.
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 |
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. |
PASEO_LABEL |
Container hostname. Paseo shows it as the host label in the UI; without it you get a random container ID. |
TZ |
Timezone for logs and agent shells. |
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, published nowhere — the platform maps the domain to it, so
localhost:6767 on the host refuses connections. See the
root README for why.
Storage
| Volume | Mount | Holds |
|---|---|---|
paseo-home |
/home/paseo |
Daemon state, agent configs, credentials (.claude, .codex) |
paseo-workspace |
/workspace |
Code the agents work on |
Claude Code and gh keep their config in /home/paseo, so both logins survive
a redeploy.
Image
| Tool | Source | Why not apt |
|---|---|---|
| Claude Code | npm | — |
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 jq unzip zip lsof psmisc ugrep bfs zsh |
apt | — |
Bump a language with a build arg, e.g. --build-arg GO_VERSION=1.27.1. Add
other agent providers to the npm install line:
RUN npm install -g @anthropic-ai/claude-code @openai/codex opencode-ai
uv itself is installed too. Notes for anyone tempted to change things:
- npm installs the same native binary as Anthropic's standalone installer.
Don't swap in
curl | bash— it writes to$HOME/.local, and$HOMEis/home/paseo, a volume mount that hides anything baked in at build time. - Claude Code can't auto-update (
paseocan'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
paseouser (uid 1000) withgosu. - Nothing installs into
$HOME. That is/home/paseo, a volume mount that hides anything baked in at build time — hence/opt/pythonand/usr/local/gorather than the defaults.