The pi-only variant was published as opencode-devbox:latest-pi-only —
an 'opencode-devbox' tag containing no opencode, which confused users.
- build-variant-pi-only now pushes joakimp/pi-devbox:base-pi-only[-vX.Y.Z]
instead of opencode-devbox:*-pi-only. New PI_IMAGE workflow env.
- Still built from the same Dockerfile.variant (single source of truth),
still smoke-tested by smoke-pi-only / validate-pi-only before publish.
- De-advertised pi-only from README, DOCKER_HUB (HUB_TEMPLATE), AGENTS,
.gitea/README. opencode-devbox now publishes 8 tags + base-latest.
- Documented in CHANGELOG (Unreleased) and the plan doc.
Note: old opencode-devbox:{latest,vX.Y.Z}-pi-only tags from v1.15.13b are
superseded and should be deleted from Docker Hub.
All opencode-devbox variants set INSTALL_OPENCODE=true, so pointing pi-devbox
at with-pi dragged opencode along and made it ~a re-tag of latest-with-pi.
Add a 5th variant pi-only (INSTALL_OPENCODE=false, INSTALL_PI=true): pi +
companions (toolkit, extensions, fork, recall) + base tooling, no opencode
(~145 MB lighter than with-pi).
- Dockerfile.variant: document pi-only in the variant table.
- CI docker-publish-split.yml: new smoke-pi-only + build-variant-pi-only jobs
(tags :VERSION-pi-only / :latest-pi-only, multi-arch); wired into
promote-base-latest and update-description needs.
- validate.yml: new validate-pi-only main-branch gate job.
- smoke-test.sh: accept --variant pi-only; threshold 2750 MB; opencode-absent
path already handled.
- Docs: HUB_TEMPLATE (regenerated DOCKER_HUB.md), README, AGENTS (variant/tag
counts 4->5, 8->10 tags), .gitea/README, manual-host-publish.sh (5 variants),
plan doc implementation note.
This is the single source of truth for joakimp/pi-devbox, which now FROMs
latest-pi-only. Versions unchanged (opencode 1.15.13, pi 0.78.0).
The previous Quick Start in both surfaces led with 'git clone',
which is overkill for users who just want to run the published image.
Match pi-devbox's pattern: lead with 'mkdir; curl docker-compose.yml;
curl .env.example; edit .env; docker compose run --rm devbox'. Keep
the git-clone path as 'for hackers/forkers'.
Required pre-step: make the gitea repo public so unauthenticated
curl to the raw URL works (done out of band — repo was private until
this commit landed).
- Bump OPENCODE_VERSION 1.14.50 -> 1.15.0 in Dockerfile.variant.
- Wrap pi-toolkit/pi-extensions git clone in Dockerfile.variant in a
5-attempt retry loop with linear backoff (matches pi-devbox pattern).
gitea.jordbo.se occasionally returns transient HTTP 500s that
previously broke with-pi/omos-with-pi variant builds.
- Add 'Sibling images' section to DOCKER_HUB.md mentioning
joakimp/pi-devbox as the pi-only counterpart.
- CHANGELOG entry for v1.15.0 with full notes.
Pi moved to its new home at earendil-works on 2026-05-07
(https://pi.dev/news/2026/5/7/pi-has-a-new-home).
The old @mariozechner/pi-coding-agent npm package is deprecated with
the explicit message 'please use @earendil-works/pi-coding-agent
instead going forward', and the version stream has moved on (old
top-out 0.73.1; new currently 0.74.0). Anyone npm-installing the old
name today gets a deprecation warning + a stale binary, so this is
a non-optional migration before the next tagged release.
Sweep:
- Dockerfile (production single-Dockerfile path) and Dockerfile.variant
(split-base path on main): npm install -g target updated.
- README, AGENTS, HUB_TEMPLATE: github.com/mariozechner/pi-coding-agent
URL refs (which now 404) -> github.com/earendil-works/pi.
- DOCKER_HUB.md regenerated (5529 bytes, ~78% headroom).
- CHANGELOG Unreleased: rename entry added with migration context.
Brew install references (`brew install pi-coding-agent`) left as-is:
formula still works at 0.73.1 and a homebrew tap update is tracked
upstream at earendil-works/pi#2755.
Historical CHANGELOG entries: only github URL refs updated (the
package name was never spelled out in those entries; we're correcting
dead hyperlinks, not rewriting feature descriptions).
The previous derive-from-README mechanism (split_sections, SECTION_RULES,
TRIM_SUBSECTIONS, REPLACEMENTS) generated a 24 997 byte Hub doc with
3 byte headroom against the 25 kB Hub limit. Every README addition
forced a 'trim something else first' exercise, and the resulting copy
was awkward (terse, repetitive linkbacks injected mid-section).
Replace with a single hand-maintained HUB_TEMPLATE constant. The Hub
doc is now intentionally slim (~5.5 kB, ~78 percent headroom) and
focuses on what Hub readers actually need: elevator pitch, image
variants, quick start, what's inside, auth, persistence, and link-outs
to the gitea README for depth.
Trade-off: when image-variants or quick-start change, update
HUB_TEMPLATE here too. That coupling is now explicit and local rather
than spread across SECTION_RULES + REPLACEMENTS + TRIM machinery,
and most README edits no longer require regenerating DOCKER_HUB.md
at all.
Generator simplified from 323 lines to 199 lines (270-line net
reduction across the script + DOCKER_HUB.md). README and Hub doc are
now independent surfaces.
CHANGELOG and AGENTS updated to reflect the new coupling. Release-day
checklist tightened: README -> regenerate DOCKER_HUB ONLY if
HUB_TEMPLATE changed -> promote CHANGELOG -> grep AGENTS -> commit
-> tag.
NPM_CONFIG_PREFIX is now /home/developer/.pi/npm-global, with that
prefix's bin/ prepended to PATH. Without this, 'pi install npm:<pkg>'
(and any 'npm install -g') by the developer user would EACCES against
the system prefix (/usr).
The new prefix lives on the devbox-pi-config named volume, so:
- User-installed pi packages (themes, skills, extensions) survive
container recreate AND image rebuild, complementing pi's auto-
restore from settings.json with one less cold-start step.
- A user-driven 'npm install -g @mariozechner/pi-coding-agent' lands
on the volume and wins over the baked pi via PATH order.
Build-time 'npm install -g' calls (opencode, pi, oh-my-opencode-slim)
are unaffected: the new ENVs are declared after those steps in the
Dockerfile, so the baked binaries still install to /usr at build time
and are not shadowed by the volume mount at runtime.
Verified end-to-end with a Bun-driven smoke test: as developer,
'npm install -g cowsay' inside the container succeeds, the binary
lands on PATH, and survives a fresh container against the same volume.
DOCKER_HUB.md regenerated (24997/25000 bytes, 3-byte headroom — was
138 before; future README additions to the persistence section need
to trim something else first).
Docs updated: Dockerfile inline comments, README persistence section,
AGENTS install contract, DOCKER_HUB persistence table, .env.example
notes, CHANGELOG Unreleased entry.
Hub readers had no signal at all about pi beyond the variant tag names
in the Image Variants table. The full README pi section is too large
to include verbatim (would push past 25 kB), so this adds a custom
`replace` rule in generate-dockerhub-md.py with a slimmed Hub-tailored
version covering the three things a Hub user actually needs:
- Run: how to start pi via compose run / compose exec
- MemPalace integration: shared palace with opencode, mempalace.ts
bridge symlinked at first start
- Persistence: which paths are on volumes, what survives --rm,
upgrade path for the pi binary itself
Build args, extension list, and toolkit detail link out to the gitea
README anchor for users who want full reference.
DOCKER_HUB.md now 24862 bytes (138 byte headroom under the 25k limit).
The v1.14.41b release expanded the CI matrix to four image variants
and pushed eight tags total, but the docs lagged behind:
- DOCKER_HUB.md's Image Variants table still listed only base + omos.
- README.md's pi section only described building from source; no
mention that prebuilt latest-with-pi / latest-omos-with-pi tags
exist (asymmetric vs the OMOS section which does mention latest-omos).
Fix:
- generate-dockerhub-md.py HEADER: extend Image Variants table to all
four variants (base, omos, with-pi, omos-with-pi).
- README.md pi section: add a Setup subsection mentioning the prebuilt
pi-enabled tags, mirroring the OMOS section's pattern.
- Regenerate DOCKER_HUB.md (22975 bytes, well under the 25k Hub limit).
The pi section itself remains intentionally dropped from DOCKER_HUB.md
to fit the 25k limit (SECTION_RULES); the Image Variants table at the
top is sufficient signal for Hub readers.
The scheduler templates in mempalace-toolkit's contrib/ assume
mempalace-session is available inside the container, but the image
never actually installed it. Users following the *-devbox scheduler
docs would silently lose the wrappers on every container recreate,
because the only way to get them was a post-hoc install.sh inside
the container — which lives in the ephemeral layer. The host-side
systemd timer would then fire, docker exec in, and hit
"mempalace-session: command not found".
Caught during runtime validation on 2026-04-30: host-side systemd
unit ran cleanly at 16:15 today, then the container was rebuilt
and recreated, and the wrappers were gone. The rebuild produced
an image that the scheduler template's own documented precondition
did not hold for.
Fix: new Dockerfile block clones mempalace-toolkit at build time
(depth-1) to /opt/mempalace-toolkit/, symlinks bin/mempalace-session
and bin/mempalace-docs into /usr/local/bin/, asserts both respond
to --help before the layer succeeds. Gated by
INSTALL_MEMPALACE_TOOLKIT=true (defaults on, depends on
INSTALL_MEMPALACE=true). Floated ref via MEMPALACE_TOOLKIT_REF=main
for auto-picking-up toolkit updates; override for reproducible
builds once the toolkit starts tagging releases.
Smoke test gains three assertions (mempalace-session --help,
mempalace-docs --help, symlink target check). Resolved-versions
preamble logs the toolkit git short-SHA alongside the other
floated components, so CI logs always record what got baked in.
README gains a Scheduled mining (mempalace-toolkit) subsection
and a build-args row. DOCKER_HUB.md regenerated; sync-check passes.
The mempalace Python package ships a 'mempalace-mcp' console entry
point; 'uv tool install' places it on PATH as a shim whose shebang
points at the isolated venv's Python. Our hand-rolled wrapper at
/usr/local/bin/mempalace-mcp-server was duplicating what uv installs
for free — one less file to maintain.
Fixes the MCP error users saw after the v1.14.28b → v1.14.29 upgrade
path: custom opencode.json files typically had the pre-v1.14.29
command ['python3', '-m', 'mempalace.mcp_server'] which worked with
the old pip install but fails silently after the uv-tool migration
because system python3 cannot import from the venv. Opencode surfaced
this as 'MCP error -32000: connection closed'.
- generate-config.py now emits ['mempalace-mcp'] and keys its detect
on shutil.which('mempalace-mcp').
- Dockerfile drops 'COPY rootfs/usr/local/bin/' and the chmod of the
wrapper. Build shrinks from 30 to 29 stages.
- rootfs/usr/local/bin/ removed entirely.
- Smoke test asserts /usr/local/bin/mempalace-mcp is executable and
prints its symlink target.
- README's MemPalace section shows ['mempalace-mcp'] and explicitly
warns against the old pattern with the observed failure mode.
- CHANGELOG adds a v1.14.29c entry.
Main changes:
- Extract opencode.json generation from entrypoint-user.sh into a
standalone Python script (rootfs/usr/local/lib/opencode-devbox/
generate-config.py). Preserves the never-overwrite-existing-config
guarantee. Cuts entrypoint-user.sh from 176 to 97 lines.
- Install MemPalace via 'uv tool install' into an isolated venv at
/opt/uv-tools/mempalace/ with a /usr/local/bin/mempalace-mcp-server
wrapper, replacing the 'pip install --break-system-packages' escape
hatch. The wrapper is what generate-config.py references in the
auto-generated opencode.json. Also fix 'mempalace init' in
entrypoint-user.sh to use --yes so first-start initialization isn't
interactive (this used to hang or print prompts into the user's
terminal). Gated by INSTALL_MEMPALACE build arg (default true) so
users who don't need AI memory can shave ~300 MB.
- Sentinel-file pattern in entrypoint.sh volume-ownership loop: write
.devbox-owner after a successful chown -R, skip the recursive walk
on subsequent starts when the sentinel matches FINAL_UID:FINAL_GID.
Cuts multi-second startup costs to milliseconds on large volumes
(nvim plugins, palace data). UID changes still trigger a full chown.
- Float all GitHub/Gitea-hosted binary versions: gosu, fzf, git-lfs,
neovim, bat, eza, zoxide, uv, gitea-mcp now default to 'latest' and
resolve the newest upstream release at build time via the /releases/
latest redirect. Go (go.dev JSON feed) and oh-my-opencode-slim (npm
@latest) likewise. Intentional pins still in place: OPENCODE_VERSION,
NODE_VERSION=22, DEBIAN_VERSION=trixie-slim. Each *_VERSION ARG
accepts an explicit value to lock a specific version when needed.
- New scripts/smoke-test.sh verifies binary presence, opencode startup,
entrypoint user drop, generate-config idempotency, bun's presence-
per-variant, and image size against thresholds (2500 MB base, 3000
MB OMOS). Prints resolved component versions as its first step so
CI logs always record what got baked into a given image.
- New .gitea/workflows/validate.yml runs on push to main and PRs:
single-arch amd64 build, smoke test, DOCKER_HUB.md sync check. Tag-
triggered docker-publish.yml now smoke-tests each variant on amd64
before the full multi-arch push.
- scripts/generate-dockerhub-md.py auto-generates DOCKER_HUB.md from
README.md using explicit SECTION_RULES. --check mode fails CI when
the committed file is out of sync. Enforces the 25 kB Docker Hub
limit. Adding a new README section forces an explicit keep/drop/
replace decision.
- Remove dead INSTALL_PYTHON build arg (was a no-op since mempalace
added python3 unconditionally).
Python 3 has been unconditionally present since the Debian trixie
upgrade (e58962a, Apr 13) — python3 3.13 ships as a transitive
dependency of the trixie base image. python3-pip (e1029bb) and
python3-venv (3a7ec45) were later added to the base layer on Apr 23
so Mason could install Python-based LSPs (ruff, ansible-lint) into
venvs on nvim startup. MemPalace's pip install (b9c08c3) just
piggybacks on what was already there.
In other words, INSTALL_PYTHON=true has been a no-op reinstall of
already-installed packages for two weeks before MemPalace existed.
The flag is dead weight and the docs that advertise it as meaningful
are misleading. Remove it everywhere.
Users who want Python tooling should use the pre-installed uv/uvx.
The sample was missing volumes and env vars added since the original:
shell-history, zoxide, nvim-data, palace, chroma-cache, and the
GitHub/Gitea token forwarding env vars. Now matches the actual
docker-compose.yml shipped in the repo.
Install gitea-mcp v1.1.0 (Go binary from gitea.com/gitea/gitea-mcp)
using the same multi-arch pattern as gosu/fzf/bat. Provides 50+ MCP
tools for Gitea API — repos, issues, PRs, releases, branches, wiki,
and Actions.
Disabled by default in auto-generated opencode.json (requires
GITEA_ACCESS_TOKEN and GITEA_HOST to be useful). Users enable it by
setting those env vars in .env and flipping enabled to true in their
opencode.json.
Env vars forwarded into the container via docker-compose.yml and
docker-compose.shared.yml environment blocks. Same {env:VAR} pattern
as the GitHub MCP server.
Docs updated: README.md (new Gitea MCP section with setup steps),
DOCKER_HUB.md (tools list), CHANGELOG.md (v1.14.28b entry).
The ONNX embedding model (~79 MB) downloads to ~/.cache/chroma/ on
first mempalace search. Without persistence it re-downloads on every
container recreation. Add a separate devbox-chroma-cache volume
rather than mixing it into the palace data volume — model cache is
disposable (delete and re-download), palace data is precious (back
up and migrate). Both volumes are commented out by default (opt-in).
Updated README.md storage section to explain the two-volume split
and the air-gapped pre-population path. Added chroma cache row to
DOCKER_HUB.md data storage table.
Install mempalace via pip in the Dockerfile. Provides 29 MCP tools
for semantic search over conversation history, knowledge graph
queries, agent diaries, and wing/room/drawer management. Everything
runs locally — no API keys, no data egress.
Integration:
- Dockerfile: pip install mempalace (with --break-system-packages
for Debian trixie PEP 668 compliance)
- entrypoint-user.sh: auto-initializes palace for /workspace on
first run (idempotent, skips if palace exists)
- entrypoint.sh: adds ~/.mempalace to the volume ownership-fix loop
- docker-compose.yml + shared: optional devbox-palace named volume
at ~/.mempalace (commented out by default — user opts in)
Users configure MCP integration by adding a mempalace server entry
to their opencode.json. No wrapper plugin needed — the upstream
Python MCP server is used directly.
Docs updated: README.md (new MemPalace section with setup, MCP
config, usage examples, storage details), DOCKER_HUB.md (data
storage table + tools list), CHANGELOG.md (unreleased entry).
Generated from annotated git tag messages. Covers every release from
v1.4.2 (initial) through v1.14.22b. One-line summaries for simple
bumps, bullet-point detail for feature/fix releases.
DOCKER_HUB.md gains a Changelog link in the Source section so Docker
Hub users can find release history without navigating the git forge.
Mason LSP installs and Lazy plugin cache live at ~/.local/share/nvim,
which was in the container's writable layer. Every --force-recreate
triggered a full re-download of all plugins and LSP servers on next
nvim launch — slow and wasteful.
Add devbox-nvim-data named volume in docker-compose.yml and
docker-compose.shared.yml, add to entrypoint ownership-fix loop,
update persistence tables in README.md and DOCKER_HUB.md.
Zoxide stores its database at ~/.local/share/zoxide/db.zo. Without a
named volume, the 'z <fragment>' jump targets are lost on every
'docker compose up --force-recreate'.
Add devbox-zoxide named volume in docker-compose.yml and
docker-compose.shared.yml, add ~/.local/share/zoxide to the
entrypoint ownership-fix loop per AGENTS.md convention, and update
the data-persistence tables in README.md and DOCKER_HUB.md.
DOCKER_HUB.md focuses on single-user setup. Rather than duplicating
the multi-user docs, add a short section linking to the source repo's
Multi-user setup section which covers volume isolation, the shared
compose layout, and the SIGNUM / $USER auto-detection.
Previous behaviour (e4063b5) COPY'd .bash_aliases and .inputrc
directly into /home/developer/ during image build. That silently
shadowed any host bind-mount or in-container customization for users
upgrading from v1.14.19b — if you'd written your own .bash_aliases
and rebuilt the container, our baked version would overwrite it
without warning.
Ship the files to /etc/skel-devbox/ instead. The entrypoint copies
them to $HOME only if the target file does not already exist, so:
- Fresh containers get the defaults automatically (unchanged)
- Host bind-mounts win (they materialize before the entrypoint runs)
- Existing in-container customizations survive upgrades
- Defaults remain discoverable at /etc/skel-devbox/ for anyone who
wants to copy, diff, or reset back to upstream
Docs (README.md, DOCKER_HUB.md, deploy/README.md) describe the new
skel layout and the restore/diff commands.
Two changes that address a longstanding frustration: bash history is
lost on every container recreate, and the container's ~/.bashrc and
~/.inputrc are stock Debian (no history tuning, no prefix search on
arrow keys, no integrations).
Added a named volume 'devbox-shell-history' mounted at ~/.cache/bash
with HISTFILE pointing there; history now survives 'docker compose up
--force-recreate'. The volume is added to both docker-compose.yml and
docker-compose.shared.yml, and ~/.cache/bash is registered in the
entrypoint ownership-fix loop per the AGENTS.md convention.
Baked rootfs/home/developer/.bash_aliases (sourced automatically by
Debian's default ~/.bashrc) and rootfs/home/developer/.inputrc into
the image. They give new containers: 100k-entry timestamped dedup
history with per-prompt flush, Up/Down arrow prefix history search,
case-insensitive coloured completion, aliases that prefer eza and
bat when present, git shortcuts, interactive rm/mv/cp, zoxide and
fzf (via 'fzf --bash') integration, and a [devbox] prompt marker.
The fzf integration uses 'fzf --bash' because we install fzf from
GitHub releases, not apt — the apt-path key-bindings aren't present.
Users who prefer their host's own shell config can uncomment two
commented bind-mount lines in docker-compose.yml to shadow the
baked defaults.
Two related documentation fixes for users mounting ~/.config/opencode
from the host:
1. Gate oh-my-opencode-slim references (file and agents) to the OMOS
variant in the Custom opencode config sections and data persistence
tables. Base-variant users no longer see oh-my-opencode-slim.json
listed as if it were always present.
2. Add a portability note warning that host-absolute paths in
opencode.json (e.g. file:///usr/local/lib/node_modules/... or
file:///opt/homebrew/...) will not resolve inside the Linux
container, and to prefer bare package specifiers that work on
both macOS and Linux hosts.
Reduces locale generation from 200+ to 16 targeted locales (major world
languages + Nordic + key European). Saves build time and image size.
Users can add more at runtime via locale-gen.
Install rustup-init binary from Rust CDN. Users bootstrap Rust with
'rustup-init -y' — persists via devbox-rustup and devbox-cargo volumes.
Add JavaScript/TypeScript development docs (Node.js + npm in base, Bun in OMOS).
Install uv from GitHub releases (~23MB). Users can install Python with
'uv python install 3.12' — persists across restarts via devbox-uv volume.
Eliminates need for a separate Python image variant.
Mount ~/.config/opencode as a directory instead of individual files.
This persists all config changes (opencode.json, oh-my-opencode-slim.json,
skills) across container restarts. Add make to README architecture diagram.
Replace vim-tiny with neovim from GitHub releases (pinned, multi-arch).
Add bat, eza, zoxide from GitHub releases and tmux, htop, patch from apt.
Move tmux from OMOS-only to base image. Set EDITOR=nvim.
Add neovim config mount option to docker-compose and docs.