feat(paseo): install glab, and stop preinstalling the agent CLIs

The paseo user cannot write /usr/local, so a baked-in agent CLI can never
apply its own update — every one of them ships an updater that expects to
rewrite its own binary, and Claude Code nags about the failure at startup.
Installed under $HOME they update themselves and still persist, since that is
the paseo-home volume. The README now lists each vendor's installer.

Bun goes with them: it was only there because omp is compiled against it.

glab arrives as the .deb from GitLab's releases page, pinned by GLAB_VERSION
because the URL carries the version. GitLab runs no apt repository and calls
Homebrew its only officially supported Linux package manager.

The Dockerfile drops its explanations along the way; they are in the README.
This commit is contained in:
tiennm99 committed 2026-09-17 09:44:12 +07:00
1 parent 044d9b5594
commit 55e8f7321d
2 files changed
+76 -77

No files matched your search

+12 -44
View File
@@ -1,18 +1,9 @@
# The official image ships no agent CLIs or language toolchains. Add them here,
# one concern per layer, cheapest and least-changing first.
#
# Stays root: the entrypoint needs root to chown the mounted volumes, then
# drops to paseo with gosu itself. Nothing installs into $HOME -- that is
# /home/paseo, a volume mount that masks anything baked in at build time.
# Adds language toolchains and CLI tooling to the official image, which ships
# none of it. Agent CLIs are not among them -- see README.md, which carries the
# reasoning for everything here.
FROM ghcr.io/getpaseo/paseo:latest
# --- system packages -------------------------------------------------------
# Everyday shell tooling. git and curl are already in the base image; sudo is
# not, despite Debian's base-passwd shipping an (empty) sudo group.
#
# build-essential is the C toolchain the language layers below assume but do
# not ship: cgo, npm's node-gyp addons and Python C extensions all shell out to
# gcc and make. Kept in this layer so there is only one apt-get update.
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
less nano jq unzip zip lsof psmisc ugrep bfs zsh sudo \
@@ -21,8 +12,6 @@ RUN apt-get update \
&& usermod -aG sudo paseo
# --- python ----------------------------------------------------------------
# Debian 12 only carries 3.11, so fetch a standalone CPython build with uv
# (astral.sh/uv). Both directories sit outside $HOME.
ARG PYTHON_VERSION=3.12
ENV UV_INSTALL_DIR=/usr/local/bin \
UV_PYTHON_INSTALL_DIR=/opt/python \
@@ -31,7 +20,6 @@ RUN curl -fsSL https://astral.sh/uv/install.sh | sh \
&& uv python install "${PYTHON_VERSION}" --default
# --- go --------------------------------------------------------------------
# Debian 12 carries 1.19, far too old, so use the official tarball.
ARG GO_VERSION=1.26.8
ENV PATH=/usr/local/go/bin:$PATH
RUN curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-$(dpkg --print-architecture).tar.gz" \
@@ -39,45 +27,25 @@ RUN curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-$(dpkg --print-architect
&& tar -C /usr/local -xzf /tmp/go.tar.gz \
&& rm /tmp/go.tar.gz
# --- gh --------------------------------------------------------------------
# Debian does not package gh, so use GitHub's own signed repository.
# --- gh and glab -----------------------------------------------------------
# gh from GitHub's signed apt repository, glab from the .deb on its releases
# page.
ARG GLAB_VERSION=1.118.0
RUN install -d -m 0755 /etc/apt/keyrings \
&& curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
-o /etc/apt/keyrings/githubcli-archive-keyring.gpg \
&& chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
> /etc/apt/sources.list.d/github-cli.list \
&& curl -fsSL "https://gitlab.com/gitlab-org/cli/-/releases/v${GLAB_VERSION}/downloads/glab_${GLAB_VERSION}_linux_$(dpkg --print-architecture).deb" \
-o /tmp/glab.deb \
&& apt-get update \
&& apt-get install -y --no-install-recommends gh \
&& apt-get install -y --no-install-recommends gh /tmp/glab.deb \
&& rm -f /tmp/glab.deb \
&& rm -rf /var/lib/apt/lists/*
# --- bun -------------------------------------------------------------------
# Only omp needs it: @oh-my-pi/pi-coding-agent is Bun-compiled and its bin
# opens with `#!/usr/bin/env bun`, so without Bun on PATH npm installs an omp
# that cannot start. BUN_INSTALL lands it in /usr/local, not the $HOME/.bun
# default that the paseo-home volume would mask.
ENV BUN_INSTALL=/usr/local
RUN curl -fsSL https://bun.sh/install | bash
# --- agent CLIs ------------------------------------------------------------
# These all publish the same way: a thin launcher plus the real binary as a
# per-platform optional dependency. npm therefore installs exactly what each
# vendor's own installer would, and is not a Node wrapper.
#
# Do not swap any of them for their `curl | bash` installer -- those write to
# $HOME/.local, and $HOME is /home/paseo, a volume that masks whatever the
# build put there. Last because these change most often.
RUN npm install -g \
@anthropic-ai/claude-code \
@openai/codex \
opencode-ai \
@github/copilot \
@earendil-works/pi-coding-agent \
@oh-my-pi/pi-coding-agent
# --- entrypoint ------------------------------------------------------------
# Wraps the image's entrypoint to set the paseo user's password while still
# root -- see entrypoint.sh. tini stays in front of it, as in the base image.
# Last so that editing the script rebuilds only this layer.
# root -- see entrypoint.sh.
COPY --chmod=0755 entrypoint.sh /usr/local/bin/paseo-sudo-entrypoint
ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/paseo-sudo-entrypoint"]
+64 -33
View File
@@ -1,9 +1,11 @@
# 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, a C toolchain, and shell tooling to the
[official image](https://paseo.sh/docs/docker), which ships none of it.
agents. Built from a local `Dockerfile` that adds `gh`, `glab`, Go, Python,
a C toolchain, and shell tooling to the
[official image](https://paseo.sh/docs/docker), which ships none of it. The
agent CLIs are not baked in — install them into `$HOME` yourself, see
[Agents](#agents).
## Setup
@@ -16,8 +18,8 @@ Python, a C toolchain, and shell tooling to the
paseo.example.com:443
```
4. In a terminal inside Paseo, log in once per tool you use — see
[Agents](#agents) — plus `gh auth login`.
4. In a terminal inside Paseo, install and log in to the agents you use — see
[Agents](#agents) — plus `gh auth login` and `glab 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,
@@ -56,18 +58,41 @@ Listens on `6767`, published nowhere — the platform maps the domain to it, so
## 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` |
The image installs none of them. Run each vendor's own installer once, in a
terminal inside Paseo, as the `paseo` user — never with `sudo`:
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.
| Agent | Command | Install with | Log in with |
| --- | --- | --- | --- |
| Claude Code | `claude` | `curl -fsSL https://claude.ai/install.sh \| bash` | `claude` |
| Codex | `codex` | `curl -fsSL https://chatgpt.com/codex/install.sh \| sh` | `codex login` |
| opencode | `opencode` | `curl -fsSL https://opencode.ai/install \| bash` | `opencode auth login` |
| Copilot CLI | `copilot` | `curl -fsSL https://gh.io/copilot-install \| bash` | `/login` inside `copilot` |
| Oh My Pi | `omp` | `curl -fsSL https://omp.sh/install \| sh` | `omp` |
| Pi | `pi` | `curl -fsSL https://pi.dev/install.sh \| sh` | `pi` |
Each lands in `$HOME` — `~/.local/bin` for most, `~/.opencode/bin` for
opencode — and appends that directory to your shell rc, so open a new terminal
afterwards. `$HOME` is the `paseo-home` volume, so both the binaries and the
logins survive a redeploy.
The installers pull in any runtime they need, into `$HOME` as well. `omp` is
the one to know about: it is Bun-compiled, and with no Bun on `PATH` its
installer takes the prebuilt binary. Ask for the source build (`--source`) and
it installs Bun to `~/.bun` first.
Why not bake them into the image: `paseo` cannot write `/usr/local`, so a
preinstalled CLI can never apply its own update. Every one of these ships an
updater (`claude update`, `codex update`, `omp update`, …) that expects to
rewrite its own binary, and under `/usr/local` it fails on permissions — Claude
Code nags about it at startup. Installed in `$HOME` they update themselves, and
you are never waiting on an image rebuild for a release that shipped that
morning.
Run the installers unprivileged. Under `sudo` they target root's home and land
outside the volume; Claude Code's refuses outright.
Pi and Oh My Pi are separate projects sharing an ancestor; their commands do
not collide.
## Storage
@@ -76,9 +101,10 @@ commands do not collide.
| `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
The agent CLIs, `gh` and `glab` all keep their config under `/home/paseo`, so
every login survives a redeploy — the base image points `CLAUDE_CONFIG_DIR`,
`CODEX_HOME` and the `XDG_*` variables into it. The agent binaries live there
too, since you install them yourself. Dotfiles as well, so a `.zshrc` or
oh-my-zsh install persists. Anything written outside `$HOME` (`chsh`,
`apt install`) is lost on rebuild.
@@ -86,29 +112,34 @@ oh-my-zsh install persists. Anything written outside `$HOME` (`chsh`,
| 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 |
| `glab` (`GLAB_VERSION`) | The `.deb` on GitLab's releases page | Debian does not package it, and GitLab runs no apt repo |
| 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 | — |
| `build-essential` | apt | — |
Bump a language with a build arg, e.g. `--build-arg GO_VERSION=1.27.1`. `uv`
itself is installed too.
Bump a pinned version with a build arg, e.g. `--build-arg GO_VERSION=1.27.1`.
`uv` itself is installed too. `GLAB_VERSION` is pinned rather than tracking the
latest because GitLab's download URL carries the version in the path.
Constraints worth knowing before changing the `Dockerfile`:
The `Dockerfile` itself only says what each layer installs. The reasoning is
all here:
- `$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.
there. Hence `/opt/python` and `/usr/local/go` rather than the defaults.
- Do not add agent CLIs here, or a runtime only they need. Under `/usr/local`
they cannot self-update; in `$HOME` they can, and they persist anyway. Bun
used to be here for `omp` and went the same way. See [Agents](#agents).
- One concern per layer, cheapest and least-changing first, so bumping a
version rebuilds as little as possible.
- `build-essential` is the C toolchain the language layers assume but do not
ship: cgo, npm's node-gyp addons and Python C extensions all shell out to
`gcc` and `make`. It rides along in the apt layer so there is one
`apt-get update`.
- `git` and `curl` are already in the base image. `sudo` is not, despite
Debian's `base-passwd` shipping an empty `sudo` group, so the apt layer adds
it and puts `paseo` in the group.
- 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