From c1e10c3663d50f1cbdf133c26c74c1a6d954008b Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Wed, 16 Sep 2026 15:06:01 +0700 Subject: [PATCH] feat(paseo): add paseo service with Claude Code preinstalled The upstream image ships no agent CLIs, so build from a local Dockerfile that layers Claude Code on top. npm delivers the same native binary as the standalone installer, which cannot be used here: it writes to $HOME/.local, and $HOME is /home/paseo, a volume mount that masks anything baked in at build time. Runs as root by design -- the entrypoint chowns the mounted volumes and then drops to the unprivileged paseo user with gosu. --- README.md | 1 + paseo/.env.example | 11 ++++++++++ paseo/Dockerfile | 13 ++++++++++++ paseo/README.md | 53 ++++++++++++++++++++++++++++++++++++++++++++++ paseo/compose.yml | 12 +++++++++++ 5 files changed, 90 insertions(+) create mode 100644 paseo/.env.example create mode 100644 paseo/Dockerfile create mode 100644 paseo/README.md create mode 100644 paseo/compose.yml diff --git a/README.md b/README.md index aa6155c..8e45cb8 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ Each links to its own README for variables, ports, and storage. | [netdata](netdata/README.md) | Netdata monitoring agent | | [ollama](ollama/README.md) | Ollama, optionally with a web UI | | [openvpn-as](openvpn-as/README.md) | OpenVPN Access Server | +| [paseo](paseo/README.md) | Paseo coding-agent daemon and web UI | | [tastyigniter](tastyigniter/README.md) | TastyIgniter restaurant ordering platform | | [traffmonetizer](traffmonetizer/README.md) | TraffMonetizer bandwidth-sharing client | diff --git a/paseo/.env.example b/paseo/.env.example new file mode 100644 index 0000000..cdf6c71 --- /dev/null +++ b/paseo/.env.example @@ -0,0 +1,11 @@ +# Copy to .env and fill in. Never commit .env. +# +# cp .env.example .env + +# Password for the daemon API and web UI. +# Generate one with: openssl rand -base64 24 +PASEO_PASSWORD= + +# Comma-separated DNS names allowed to reach the daemon. The domain mapped in +# Coolify/Dokploy must be listed here. IPs and localhost are always allowed. +PASEO_HOSTNAMES= diff --git a/paseo/Dockerfile b/paseo/Dockerfile new file mode 100644 index 0000000..884cc78 --- /dev/null +++ b/paseo/Dockerfile @@ -0,0 +1,13 @@ +# The official image ships no agent CLIs. Add Claude Code globally under +# /usr/local, where the unprivileged paseo user can run it. +# +# npm here delivers the same native binary as the standalone installer -- it is +# not a Node wrapper. Do not swap this for the `curl | bash` installer: that +# writes to $HOME/.local, and $HOME is /home/paseo, a volume mount that masks +# anything baked in at build time. +# +# Stays root: the entrypoint needs root to chown the mounted volumes, then +# drops to paseo with gosu itself. +FROM ghcr.io/getpaseo/paseo:latest + +RUN npm install -g @anthropic-ai/claude-code diff --git a/paseo/README.md b/paseo/README.md new file mode 100644 index 0000000..9df25b6 --- /dev/null +++ b/paseo/README.md @@ -0,0 +1,53 @@ +# 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). + +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. + +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. + +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. + +The image intentionally keeps running as root — its entrypoint chowns the +mounted volumes and then drops to the unprivileged `paseo` user with `gosu`. + +Coolify and Dokploy build the image themselves from the compose `build:` +stanza; there is nothing to push. + +## 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. | + +Generate a password with `openssl rand -base64 24`. The proxied domain must +appear in `PASEO_HOSTNAMES` or requests are rejected. + +## 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. + +## Storage + +Two named volumes: + +| Volume | Mount | Holds | +| --- | --- | --- | +| `paseo-home` | `/home/paseo` | Daemon state, agent configs, credentials (`.codex`, `.claude`) | +| `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. + +The daemon runs as uid/gid `1000:1000`; anything bind-mounted in its place must +be writable by that user. diff --git a/paseo/compose.yml b/paseo/compose.yml new file mode 100644 index 0000000..b5ee9cb --- /dev/null +++ b/paseo/compose.yml @@ -0,0 +1,12 @@ +services: + paseo: + build: . + environment: + - PASEO_PASSWORD=${PASEO_PASSWORD} + - PASEO_HOSTNAMES=${PASEO_HOSTNAMES} + volumes: + - 'paseo-home:/home/paseo' + - 'paseo-workspace:/workspace' +volumes: + paseo-home: + paseo-workspace: