f51e9f52a1
Optional integration of pi-coding-agent alongside opencode in the same
container. Both harnesses share the mempalace install and palace path —
wing/diary entries are mutually visible.
Build:
--build-arg INSTALL_PI=true # opt-in
--build-arg PI_VERSION=0.73.1 # pin a version (default: latest)
--build-arg INSTALL_OPENCODE=false # build pi-only image
Dockerfile:
• New INSTALL_PI block: npm install -g @mariozechner/pi-coding-agent
+ git-clones pi-toolkit and pi-extensions to /opt/.
• Existing opencode install gated behind new INSTALL_OPENCODE arg
(default true; existing builds unaffected).
• mkdir adds ~/.pi/agent/extensions for the named volume mount root.
• CMD changed from ['opencode'] to ['bash', '-l']. compose run --rm
devbox now drops to a login shell so users pick the harness; pass
'opencode' or 'pi' explicitly to launch directly. compose exec
workflows are unaffected (bypass entrypoint+CMD).
entrypoint.sh:
• Adds ~/.pi to volume ownership loop.
entrypoint-user.sh:
• New 'pi: deploy toolkit + extensions + mempalace bridge' block runs
pi-toolkit/install.sh, pi-extensions/install.sh, settings.json
template bootstrap, then symlinks the mempalace.ts bridge directly.
Order: toolkit before extensions before bridge. mempalace-toolkit's
full install.sh is intentionally NOT called (its install_skill
would race with skillset auto-deploy --prune-stale).
docker-compose.yml:
• New devbox-pi-config named volume mounted at /home/developer/.pi.
Persists user toggles (/ext-disabled extensions) and settings.json
edits across container recreate. Mirrors devbox-opencode-config
pattern from v1.14.33.
scripts/smoke-test.sh:
• New --variant with-pi (threshold 2700 MB) and --variant omos-with-pi
(3400 MB).
• Pi assertions gated on `command -v pi`: version, /opt/pi-toolkit
clone HEAD, /opt/pi-extensions clone HEAD, deployed keybindings
symlink, ≥4 extension symlinks, mempalace.ts bridge symlink,
settings.json bootstrap.
• Pi state assertions use docker exec from the host (not 'run'),
since the container has no docker CLI.
• opencode core test now gated on INSTALL_OPENCODE presence.
scripts/generate-dockerhub-md.py:
• SECTION_RULES adds 'pi (alternative/complementary harness)': drop.
Section stays in README; dropped from DOCKER_HUB.md to keep under
the 25 kB Docker Hub limit.
Docs:
• README adds full 'pi (alternative/complementary harness)' section.
• AGENTS.md codifies pi install contract, deploy ordering, named
volume rationale, and CMD change.
• CHANGELOG.md gets an Unreleased entry.
• .env.example documents new build args.
• docker-compose.yml example args block updated.
Verification (local builds on arm64):
• Default (INSTALL_PI=false): 1871 MB, all assertions pass — no
regression.
• INSTALL_PI=true: 2110 MB (within 2700 threshold), 37 assertions
pass including pi version, all 7 extensions deployed (6 from
pi-extensions + mempalace.ts bridge), settings.json bootstrap.
Not yet:
• CI workflow updates to add -with-pi tag variants. Deferred until
local path stabilizes through user testing.
• pi-devbox separate repo for fully stripped pi-only image. Phase 2.
755 lines
33 KiB
Markdown
755 lines
33 KiB
Markdown
# opencode-devbox
|
||
|
||
Portable AI developer environment in a Docker container. Run [opencode](https://opencode.ai) on any Docker-capable machine with configurable LLM providers, dev tools, and host filesystem access.
|
||
|
||
## Why?
|
||
|
||
The official `ghcr.io/anomalyco/opencode` image (now archived) was Alpine-based and minimal — no git, no dev tools, broken PTY support due to musl/glibc incompatibility. This project provides a **Debian-based, production-ready** alternative using the current v1.x release.
|
||
|
||
## Quick Start
|
||
|
||
```bash
|
||
# Clone
|
||
git clone ssh://gitea.jordbo.se:2222/joakimp/opencode-devbox.git
|
||
cd opencode-devbox
|
||
|
||
# Configure
|
||
cp .env.example .env
|
||
# Edit .env with your provider, API key, workspace path, git config
|
||
|
||
# Install git hooks (secret scanning)
|
||
brew install gitleaks # macOS / Linuxbrew
|
||
./setup-hooks.sh
|
||
|
||
# Build and run
|
||
docker compose run --rm devbox
|
||
```
|
||
|
||
## Features
|
||
|
||
- **Debian trixie** base — glibc, full PTY/terminal support
|
||
- **Configurable providers** — Anthropic, OpenAI, AWS Bedrock via env vars
|
||
- **Host filesystem access** — bind mount any directory as `/workspace`
|
||
- **SSH key forwarding** — git push/pull to private repos
|
||
- **MCP server support** — Node.js included for `npx`-based MCP servers
|
||
- **Non-root user** — runs as `developer` with UID auto-matched to workspace owner (sudo available)
|
||
- **Python via uv** — `uv` package manager included; install Python on demand with `uv python install`
|
||
- **Rust via rustup** — `rustup-init` included; bootstrap Rust on demand with `rustup-init -y`
|
||
- **Optional runtimes** — Python (apt), Go via build args (Node.js always included — required for opencode v1.x)
|
||
- **Multi-agent orchestration** — optional [oh-my-opencode-slim](https://github.com/alvinunreal/oh-my-opencode-slim) integration via build arg
|
||
- **AWS CLI v2** — built-in SSO/Bedrock authentication with headless device-code flow
|
||
- **Multi-arch** — amd64 and arm64
|
||
|
||
## Usage
|
||
|
||
### Prerequisites
|
||
|
||
Bind-mounted directories must exist on the host before starting the container. Docker creates missing directories as root-owned, which causes permission issues.
|
||
|
||
```bash
|
||
# Required: workspace for your projects
|
||
mkdir -p ~/projects
|
||
```
|
||
|
||
### Connecting to the container
|
||
|
||
From your laptop, SSH into the remote server where Docker is running, then start the container:
|
||
|
||
```bash
|
||
# 1. SSH into the remote server
|
||
ssh user@remote-server
|
||
|
||
# 2. Navigate to the project
|
||
cd opencode-devbox
|
||
|
||
# 3. Start the container with an interactive shell
|
||
docker compose run --rm devbox bash
|
||
|
||
# You're now inside the container — run commands here
|
||
aws sso login --sso-session <your-sso-session> --use-device-code
|
||
opencode
|
||
```
|
||
|
||
### Running modes
|
||
|
||
**Interactive shell** — enter the container, run multiple commands:
|
||
```bash
|
||
docker compose run --rm devbox bash
|
||
```
|
||
|
||
**Direct to opencode** — skips the shell, launches opencode immediately:
|
||
```bash
|
||
docker compose run --rm devbox
|
||
```
|
||
|
||
**Background container** — keep it running, attach when needed:
|
||
```bash
|
||
# Start in background
|
||
docker compose up -d
|
||
|
||
# Attach a shell to the running container
|
||
docker compose exec -u developer devbox bash
|
||
|
||
# Or run a single command inside it
|
||
docker compose exec -u developer devbox aws --version
|
||
```
|
||
|
||
> `run` creates a new container (cleaned up with `--rm`). `exec` attaches to an already running one.
|
||
|
||
## Configuration
|
||
|
||
### Environment Variables
|
||
|
||
| Variable | Description | Default |
|
||
|---|---|---|
|
||
| `OPENCODE_PROVIDER` | LLM provider (`anthropic`, `openai`, `amazon-bedrock`) | `anthropic` |
|
||
| `OPENCODE_MODEL` | Model override | Provider default |
|
||
| `ANTHROPIC_API_KEY` | Anthropic API key | — |
|
||
| `OPENAI_API_KEY` | OpenAI API key | — |
|
||
| `AWS_REGION` | AWS region for Bedrock | `us-east-1` |
|
||
| `AWS_PROFILE` | AWS SSO profile name | `default` |
|
||
| `GIT_USER_NAME` | Git commit author name | — |
|
||
| `GIT_USER_EMAIL` | Git commit author email | — |
|
||
| `WORKSPACE_PATH` | Host path to mount | `.` |
|
||
| `SSH_KEY_PATH` | Host SSH key directory | `~/.ssh` |
|
||
| `USER_UID` | Override container user UID | Auto-detect from `/workspace` |
|
||
| `USER_GID` | Override container user GID | Auto-detect from `/workspace` |
|
||
| `LANG` | System locale | `en_US.UTF-8` |
|
||
| `LANGUAGE` | Language priority list | `en_US:en` |
|
||
| `LC_ALL` | Override all locale settings | `en_US.UTF-8` |
|
||
| `EDITOR` | Default text editor | `nvim` |
|
||
| `ENABLE_OMOS` | Enable oh-my-opencode-slim multi-agent orchestration | `false` |
|
||
| `OMOS_TMUX` | Enable tmux pane integration for OMOS | `false` |
|
||
| `OMOS_SKILLS` | Install OMOS recommended skills on first run | `true` |
|
||
| `OMOS_RESET` | Force regenerate OMOS config on next start | `false` |
|
||
| `SKILLSET_CONTAINER_PATH` | Path to skillset repo inside container (for auto-deploy when not at /workspace/skillset) | Auto-detect |
|
||
|
||
### Custom opencode config
|
||
|
||
Opencode configuration is persisted automatically via the named volume `devbox-opencode-config`. This volume is mounted at `/home/developer/.config/opencode` by default — no host directory setup required. All changes to `opencode.jsonc`, skills, and (on the OMOS variant) `oh-my-opencode-slim.json` survive container recreation.
|
||
|
||
When an existing `opencode.jsonc` is found in the volume, the `OPENCODE_PROVIDER` auto-config is skipped.
|
||
|
||
**Alternative: host bind-mount** — if you specifically want to share config from the host (e.g. to version-control it or sync across machines), replace the named volume with a bind mount:
|
||
|
||
```yaml
|
||
volumes:
|
||
- ~/.config/opencode:/home/developer/.config/opencode
|
||
```
|
||
|
||
> **Portability note:** The mounted config runs inside a Linux container. Any absolute paths inside `opencode.jsonc` (for example, host-specific `plugin` entries like `file:///usr/local/lib/node_modules/...` or `file:///opt/homebrew/...`) will not resolve inside the container. Prefer bare package specifiers (e.g. `"oh-my-opencode-slim"`) that resolve via `node_modules` lookup, which works on both macOS and Linux hosts.
|
||
|
||
### Custom skills
|
||
|
||
Skills are deployed automatically from a skillset repo on container start. The entrypoint detects the skillset location in this order:
|
||
|
||
1. `SKILLSET_CONTAINER_PATH` env var (explicit path to skillset repo inside container)
|
||
2. `~/skillset` mount (if present)
|
||
3. `/workspace/skillset` fallback (if your workspace contains a `skillset/` directory)
|
||
|
||
When a skillset repo is detected, its skills are symlinked into `~/.agents/skills/` automatically. No manual configuration needed.
|
||
|
||
> **Warning:** Do not bind-mount a host `~/.agents/skills` directory directly into the container. This conflicts with the symlink-based auto-deploy mechanism and causes broken skill references.
|
||
|
||
### Neovim configuration
|
||
|
||
The image includes neovim 0.12 with `EDITOR=nvim` set by default. To use your own neovim config (and have plugins auto-install via lazy.nvim on first start), mount it from the host:
|
||
|
||
```yaml
|
||
volumes:
|
||
- ~/.config/nvim:/home/developer/.config/nvim:ro
|
||
```
|
||
|
||
### Python development with uv
|
||
|
||
The image includes Python 3.13 (from Debian Trixie) and [uv](https://docs.astral.sh/uv/), a fast Python package manager that replaces pip, venv, and pyenv:
|
||
|
||
```bash
|
||
# Python 3.13 is available out of the box
|
||
python3 --version
|
||
|
||
# Use uv for package management
|
||
uv venv
|
||
uv pip install -r requirements.txt
|
||
|
||
# Or use uv's project workflow (reads pyproject.toml)
|
||
uv sync
|
||
|
||
# Run a Python script
|
||
uv run python script.py
|
||
|
||
# Install standalone Python tools
|
||
uvx ruff check .
|
||
|
||
# Install a newer Python version (persists with devbox-uv volume)
|
||
uv python install 3.14
|
||
```
|
||
|
||
Python installations are stored in `~/.local/share/uv/`. To persist them across container restarts, add the `devbox-uv` named volume to your `docker-compose.yml`:
|
||
|
||
```yaml
|
||
volumes:
|
||
- devbox-uv:/home/developer/.local/share/uv
|
||
|
||
volumes:
|
||
devbox-uv:
|
||
```
|
||
|
||
Project virtual environments (`.venv`) are stored in your workspace directory and persist automatically via the `/workspace` bind mount.
|
||
|
||
### Rust development with rustup
|
||
|
||
The image includes `rustup-init`, the Rust toolchain installer. Rust is not pre-installed but can be bootstrapped on demand:
|
||
|
||
```bash
|
||
# One-time setup: install Rust toolchain (~300MB, persists with volumes)
|
||
rustup-init -y
|
||
source ~/.cargo/env
|
||
|
||
# Now use Rust normally
|
||
cargo new my-project
|
||
cargo build
|
||
cargo run
|
||
```
|
||
|
||
To persist Rust toolchains and cargo data across container restarts, add named volumes to your `docker-compose.yml`:
|
||
|
||
```yaml
|
||
volumes:
|
||
- devbox-rustup:/home/developer/.rustup
|
||
- devbox-cargo:/home/developer/.cargo
|
||
|
||
volumes:
|
||
devbox-rustup:
|
||
devbox-cargo:
|
||
```
|
||
|
||
### JavaScript and TypeScript
|
||
|
||
The base image includes **Node.js 22** and **npm** — sufficient for most JavaScript and TypeScript development:
|
||
|
||
```bash
|
||
# Initialize a new project
|
||
npm init -y
|
||
|
||
# Install dependencies
|
||
npm install
|
||
|
||
# Run TypeScript (via tsx, ts-node, etc.)
|
||
npx tsx src/index.ts
|
||
|
||
# Use npx for one-off tools
|
||
npx tsc --init
|
||
```
|
||
|
||
The OMOS image variant also includes **Bun**, a faster JavaScript runtime and package manager:
|
||
|
||
```bash
|
||
bun init
|
||
bun install
|
||
bun run src/index.ts
|
||
```
|
||
|
||
Node modules are stored in your project directory under `/workspace` and persist automatically.
|
||
|
||
### VS Code integration
|
||
|
||
VS Code can connect directly to a running opencode-devbox container for a full IDE experience with IntelliSense, debugging, and extensions running inside the container.
|
||
|
||
**Local Docker (Docker running on your workstation):**
|
||
|
||
1. Install the [Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) extension
|
||
2. Start the container: `docker compose up -d`
|
||
3. In VS Code: `Ctrl+Shift+P` → "Dev Containers: Attach to Running Container" → select `opencode-devbox`
|
||
|
||
**Remote Docker (Docker running on a remote server, e.g. via SSH):**
|
||
|
||
1. Install the [Remote - SSH](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-ssh) and [Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) extensions
|
||
2. Connect to the remote host: `Ctrl+Shift+P` → "Remote-SSH: Connect to Host"
|
||
3. On the remote host, start the container: `docker compose up -d`
|
||
4. In VS Code (now connected to the remote): `Ctrl+Shift+P` → "Dev Containers: Attach to Running Container"
|
||
|
||
VS Code extensions installed inside the container persist as long as the container exists (not removed with `docker compose down`). For persistent extension storage across container recreations, add a named volume:
|
||
|
||
```yaml
|
||
volumes:
|
||
- devbox-vscode:/home/developer/.vscode-server
|
||
```
|
||
|
||
### Multi-user setup
|
||
|
||
The shared-machine compose file (`docker-compose.shared.yml`) supports two modes:
|
||
|
||
**Own-account mode** (each user has their own OS login — the common case):
|
||
Leave `SIGNUM` unset in `.env`. The project name defaults to `devbox-$USER`, so each OS user automatically gets isolated container names and named volumes with zero configuration.
|
||
|
||
**Shared-account mode** (everyone logs in as the same OS user, e.g. `garage`):
|
||
Each user sets `SIGNUM=<unique-id>` in `.env` to get isolation.
|
||
|
||
Setup per user:
|
||
|
||
```bash
|
||
# Replace <signum> with your username/identifier
|
||
mkdir -p ~/<signum>/opencode-devbox
|
||
cd ~/<signum>/opencode-devbox
|
||
|
||
# Copy the shared-machine compose and env files
|
||
cp /path/to/opencode-devbox/docker-compose.shared.yml docker-compose.yml
|
||
cp /path/to/opencode-devbox/.env.shared.example .env
|
||
|
||
# Edit .env — set SIGNUM only if you're in shared-account mode
|
||
vim .env
|
||
|
||
# Start
|
||
docker compose up -d
|
||
docker compose exec -u developer devbox opencode
|
||
```
|
||
|
||
Each user's container, config, and named volumes are fully isolated:
|
||
- Container name: `devbox-<signum>` (or `devbox-$USER` in own-account mode)
|
||
- Named volumes: prefixed with the project name (`devbox-<signum>_devbox-data`, etc.) — the Docker daemon is system-wide, so directory-name prefixing alone is NOT sufficient for isolation
|
||
- Opencode config: persisted via per-user named volume (`devbox-<signum>_devbox-opencode-config`)
|
||
|
||
See `docker-compose.shared.yml` and `.env.shared.example` for the full configuration.
|
||
|
||
### Rebuilding the Image
|
||
|
||
`docker compose run` and `docker compose up` use the existing image — they **do not rebuild** when you change the Dockerfile or build args (e.g. updating `OPENCODE_VERSION`). Rebuild explicitly:
|
||
|
||
```bash
|
||
# Rebuild then run
|
||
docker compose build
|
||
docker compose run --rm devbox
|
||
|
||
# Or rebuild and run in one step
|
||
docker compose run --rm --build devbox
|
||
```
|
||
|
||
### Build Args
|
||
|
||
Enable optional language runtimes, pin a specific opencode version, or lock any of the tooling components:
|
||
|
||
```bash
|
||
docker compose build --build-arg INSTALL_GO=true
|
||
docker compose build --build-arg OPENCODE_VERSION=1.5.0
|
||
docker compose build --build-arg NVIM_VERSION=0.12.1 # pin to a specific version
|
||
```
|
||
|
||
| Arg | Default | Description |
|
||
|---|---|---|
|
||
| `INSTALL_GO` | `false` | Go toolchain (resolves latest stable from go.dev when `GO_VERSION=latest`) |
|
||
| `INSTALL_MEMPALACE` | `true` | [MemPalace](https://github.com/MemPalace/mempalace) local AI memory system (~300 MB — disable to shrink image if you don't need MCP memory) |
|
||
| `INSTALL_MEMPALACE_TOOLKIT` | `true` | [mempalace-toolkit](https://gitea.jordbo.se/joakimp/mempalace-toolkit) bash wrappers (`mempalace-session`, `mempalace-docs`). Cloned at build time from `MEMPALACE_TOOLKIT_REF` (default `main`). Requires `INSTALL_MEMPALACE=true`. |
|
||
| `INSTALL_OMOS` | `false` | [oh-my-opencode-slim](https://github.com/alvinunreal/oh-my-opencode-slim) multi-agent orchestration (installs Bun and plugin) |
|
||
| `INSTALL_OPENCODE` | `true` | Install opencode. Set `false` to build a pi-only image (still includes Bun if `INSTALL_OMOS=true`; for a fully stripped pi-only image see the `pi-devbox` repo). |
|
||
| `INSTALL_PI` | `false` | Install [pi](https://github.com/mariozechner/pi-coding-agent) as alternative/complementary harness. Both clones [pi-toolkit](https://gitea.jordbo.se/joakimp/pi-toolkit) (~5 MB) and [pi-extensions](https://gitea.jordbo.se/joakimp/pi-extensions) (~1 MB) into `/opt/`; entrypoint deploys them on container start. ~150 MB total image growth. |
|
||
| `PI_VERSION` | `latest` | npm version of `@mariozechner/pi-coding-agent`. Floats by default (image rebuild = pi update). |
|
||
| `PI_TOOLKIT_REF`, `PI_EXTENSIONS_REF` | `main` | Git refs for the toolkit/extensions clones. Pin to a tag/commit for reproducibility. |
|
||
| `OPENCODE_VERSION` | *(pinned per release)* | opencode npm version. Drives the image tag and is intentionally not floated. |
|
||
| `NODE_VERSION` | `22` | Node.js major version. Pinned to protect against upstream breaking changes across majors. |
|
||
| `GOSU_VERSION`, `FZF_VERSION`, `GIT_LFS_VERSION`, `NVIM_VERSION`, `BAT_VERSION`, `EZA_VERSION`, `ZOXIDE_VERSION`, `UV_VERSION`, `GITEA_MCP_VERSION`, `GO_VERSION`, `OMOS_VERSION` | `latest` | All GitHub/Gitea/go.dev-hosted binaries resolve to the newest upstream release at build time. Override with a specific version to pin. Resolved versions are logged in CI output. |
|
||
|
||
> **Reproducibility note:** With `latest` defaults, two builds of the same `v{opencode}` tag may embed different tool versions if upstream releases have happened in between. This is intentional — it means every rebuild picks up upstream CVE fixes automatically. If you need a bit-for-bit reproducible build, pass explicit `*_VERSION` args. The CI smoke test logs the resolved versions for every release build.
|
||
|
||
## oh-my-opencode-slim (Multi-Agent Orchestration)
|
||
|
||
[oh-my-opencode-slim](https://github.com/alvinunreal/oh-my-opencode-slim) adds a multi-agent layer on top of opencode — an Orchestrator delegates tasks to specialized agents (Explorer, Oracle, Librarian, Designer, Fixer), each configurable with different models and providers.
|
||
|
||
### Setup
|
||
|
||
A pre-built OMOS image is available on Docker Hub as `joakimp/opencode-devbox:latest-omos`. Alternatively, build from source:
|
||
|
||
**1. Build the image with OMOS support:**
|
||
|
||
```bash
|
||
docker compose build --build-arg INSTALL_OMOS=true
|
||
```
|
||
|
||
This installs Bun and the oh-my-opencode-slim package into the image.
|
||
|
||
**2. Enable in `.env`:**
|
||
|
||
```bash
|
||
ENABLE_OMOS=true
|
||
```
|
||
|
||
**3. Run as normal:**
|
||
|
||
```bash
|
||
docker compose run --rm devbox
|
||
```
|
||
|
||
On first start, the entrypoint runs the oh-my-opencode-slim installer in non-interactive mode. It generates agent configuration at `~/.config/opencode/oh-my-opencode-slim.json` inside the container. The default preset uses OpenAI models — edit the generated config or mount your own to customize.
|
||
|
||
### OMOS Environment Variables
|
||
|
||
| Variable | Default | Description |
|
||
|---|---|---|
|
||
| `ENABLE_OMOS` | `false` | Activate oh-my-opencode-slim on container start |
|
||
| `OMOS_TMUX` | `false` | Enable tmux pane integration (tmux is included in the base image) |
|
||
| `OMOS_SKILLS` | `true` | Install recommended skills (simplify, agent-browser, cartography) |
|
||
| `OMOS_RESET` | `false` | Force regenerate config on next start (backs up existing config) |
|
||
|
||
### Custom Configuration
|
||
|
||
If you mount the opencode config directory (see Custom opencode config above), the `oh-my-opencode-slim.json` file is included and persists across restarts. Edit it directly to control which models power each agent, fallback chains, council setup, and more.
|
||
|
||
See the [oh-my-opencode-slim configuration docs](https://github.com/alvinunreal/oh-my-opencode-slim/blob/master/docs/configuration.md) for the full reference.
|
||
|
||
### Verifying Agents
|
||
|
||
After starting opencode with OMOS enabled, run inside the opencode session:
|
||
|
||
```
|
||
ping all agents
|
||
```
|
||
|
||
All six agents should respond if your provider authentication is working.
|
||
|
||
## pi (alternative/complementary harness)
|
||
|
||
[pi](https://github.com/mariozechner/pi-coding-agent) is a lightweight TUI coding-agent that can run alongside opencode in the same container. Both harnesses share the mempalace install and palace data — wing/diary entries created by one are visible to the other.
|
||
|
||
### Build
|
||
|
||
```bash
|
||
docker compose build --build-arg INSTALL_PI=true
|
||
# Or: pin a pi version
|
||
docker compose build --build-arg INSTALL_PI=true --build-arg PI_VERSION=0.73.0
|
||
# Or: pi-only image (no opencode, smaller)
|
||
docker compose build --build-arg INSTALL_PI=true --build-arg INSTALL_OPENCODE=false
|
||
```
|
||
|
||
### Run
|
||
|
||
The default `compose run --rm devbox` invocation drops to a login bash so you can choose:
|
||
|
||
```bash
|
||
docker compose run --rm devbox # bash, then `pi` or `opencode` or `aws sso login`
|
||
docker compose run --rm devbox pi # launch pi directly
|
||
docker compose run --rm devbox opencode
|
||
```
|
||
|
||
For an attached `compose up -d` container, both harnesses are reachable via `compose exec`:
|
||
|
||
```bash
|
||
docker compose exec -u developer devbox pi
|
||
docker compose exec -u developer devbox opencode
|
||
docker compose exec -u developer devbox bash
|
||
```
|
||
|
||
### What gets installed
|
||
|
||
- **`pi` CLI** — npm-installed globally at build time. Version pinned by `PI_VERSION`.
|
||
- **pi-toolkit** — keybindings.json (mosh/tmux newline fixes), pi-env.zsh (AWS env loader), settings.json template. Cloned to `/opt/pi-toolkit`; deployed to `~/.pi/agent/` on first container start.
|
||
- **pi-extensions** — 6 extensions: `confirm-destructive`, `ext-toggle` (`/ext` slash command), `git-checkpoint`, `notify`, `ssh-controlmaster`, `todo`. Cloned to `/opt/pi-extensions`; symlinked into `~/.pi/agent/extensions/`.
|
||
- **mempalace bridge** — `mempalace.ts` extension symlinked from the cloned mempalace-toolkit. Provides pi's MCP tools for palace search/diary/kg.
|
||
|
||
### Persistence
|
||
|
||
`~/.pi/` is mounted on the `devbox-pi-config` named volume. User toggles via `/ext`, edits to `~/.pi/agent/settings.json`, and any pi state survive container recreate. `pi update` writes to the npm global prefix which is *not* on a volume — image rebuild is the upgrade path.
|
||
|
||
### Configuration
|
||
|
||
The entrypoint copies `pi-toolkit/settings.example.json` to `~/.pi/agent/settings.json` on first start. Edit it to set provider/model:
|
||
|
||
```bash
|
||
docker compose exec -u developer devbox $EDITOR ~/.pi/agent/settings.json
|
||
```
|
||
|
||
The AWS env loader (`pi-env.zsh`) reads `~/.config/pi/.env` if you bind-mount one; otherwise pi uses container env vars passed via `.env`.
|
||
|
||
## AWS Bedrock Authentication
|
||
|
||
When using AWS Bedrock as your LLM provider, you need:
|
||
|
||
### 1. AWS config on the host
|
||
|
||
The container needs access to your `~/.aws/config` with SSO session configuration. If you already have this on another machine, copy it:
|
||
|
||
```bash
|
||
scp -r user@other-machine:~/.aws ~/.aws
|
||
```
|
||
|
||
Or configure from scratch on the host:
|
||
|
||
```bash
|
||
aws configure sso
|
||
```
|
||
|
||
### 2. Mount `~/.aws` into the container
|
||
|
||
Uncomment the AWS volume mount in `docker-compose.yml`:
|
||
|
||
```yaml
|
||
- ~/.aws:/home/developer/.aws
|
||
```
|
||
|
||
Note: do **not** use `:ro` — SSO writes token cache files to this directory.
|
||
|
||
### 3. Authenticate inside the container
|
||
|
||
Since the container runs headless (no browser), use the device-code flow:
|
||
|
||
```bash
|
||
# Start the container
|
||
docker compose up -d
|
||
docker compose exec -u developer devbox bash
|
||
|
||
# Authenticate — prints a URL and code you open in your local browser
|
||
aws sso login --sso-session <your-sso-session> --use-device-code
|
||
|
||
# Once approved in the browser, start opencode
|
||
opencode
|
||
```
|
||
|
||
The `--use-device-code` flag outputs a URL and short code instead of trying to open a browser. Copy the URL into any browser (on your laptop, phone, etc.), enter the code, and complete the 2FA flow. The CLI in the container picks up the session automatically.
|
||
|
||
SSO sessions typically last 8–12 hours before requiring re-authentication. Since `~/.aws` is mounted from the host, tokens persist across container restarts.
|
||
|
||
## MemPalace — persistent AI memory
|
||
|
||
The image includes [MemPalace](https://github.com/MemPalace/mempalace), a local-first AI memory system that stores conversation history verbatim and retrieves it via semantic search. Nothing leaves your machine.
|
||
|
||
> MemPalace adds ~300 MB to the image (chromadb, embedding model deps). If you don't use it, rebuild with `--build-arg INSTALL_MEMPALACE=false` to shrink the image.
|
||
|
||
### Enabling persistence
|
||
|
||
Uncomment the palace volume in `docker-compose.yml`:
|
||
|
||
```yaml
|
||
- devbox-palace:/home/developer/.mempalace
|
||
```
|
||
|
||
Without the volume, palace data lives in the container's writable layer and is lost on `--force-recreate`.
|
||
|
||
### MCP integration with opencode
|
||
|
||
Add mempalace as an MCP server in your `opencode.jsonc` (inside `~/.config/opencode/`):
|
||
|
||
```json
|
||
{
|
||
"mcp": {
|
||
"mempalace": {
|
||
"type": "local",
|
||
"command": ["mempalace-mcp"]
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
> The image installs mempalace into an isolated `uv tool` venv at `/opt/uv-tools/mempalace/`. `uv tool install` places `mempalace-mcp` on `PATH` as a shim whose shebang points at the venv's Python, so MCP clients can invoke it as a normal binary without worrying about the venv. Do **not** use `["python3", "-m", "mempalace.mcp_server"]` — the system Python cannot import from the uv-managed venv and you'll get `ModuleNotFoundError` / `MCP error -32000: connection closed`.
|
||
|
||
This gives opencode access to 29 MCP tools for searching memory, querying the knowledge graph, managing wings/rooms/drawers, and agent diaries.
|
||
|
||
### Basic usage
|
||
|
||
```bash
|
||
# Mine project files into the palace
|
||
mempalace mine /workspace
|
||
|
||
# Mine conversation transcripts
|
||
mempalace mine ~/.local/share/opencode/ --mode convos
|
||
|
||
# Search memory
|
||
mempalace search "why did we switch to eno1"
|
||
|
||
# Load context for a new session
|
||
mempalace wake-up
|
||
```
|
||
|
||
Each workspace gets its own isolated "wing" — memories never leak between projects.
|
||
|
||
### Scheduled mining (mempalace-toolkit)
|
||
|
||
The image bakes in [mempalace-toolkit](https://gitea.jordbo.se/joakimp/mempalace-toolkit), a small set of bash wrappers that pair with mempalace for two common routines:
|
||
|
||
```bash
|
||
# Mine opencode session history (reads ~/.local/share/opencode/opencode.db, stages JSONL, mines into wing_conversations)
|
||
mempalace-session
|
||
|
||
# Mine a project's docs into a dedicated wing
|
||
mempalace-docs /workspace/my-project
|
||
```
|
||
|
||
Both wrappers are idempotent and dedup-aware — re-running them on unchanged input is a cheap no-op.
|
||
|
||
For weekly automated runs, the toolkit ships ready-to-use scheduler templates (systemd user timer, launchd user agent, cron) in its [`contrib/`](https://gitea.jordbo.se/joakimp/mempalace-toolkit/src/branch/main/contrib) directory. The `*-devbox` variants are designed for this container: host-side schedulers that `docker exec` into the running opencode-devbox.
|
||
|
||
Disable the toolkit (keeps mempalace itself) with `--build-arg INSTALL_MEMPALACE_TOOLKIT=false`. Pin to a specific ref with `--build-arg MEMPALACE_TOOLKIT_REF=v0.3.0` once tagged releases exist.
|
||
|
||
### Storage
|
||
|
||
Two separate named volumes keep different data classes apart:
|
||
|
||
- **Palace data** (`~/.mempalace/`): ChromaDB vectors, SQLite knowledge graph, drawers. This is your memory — back it up, treat it as precious. Persists via the `devbox-palace` named volume.
|
||
- **Embedding model cache** (`~/.cache/chroma/`): ONNX model (~79 MB), downloaded automatically on first search. Disposable — blow it away and it re-downloads in ~4 seconds. Persists via the `devbox-chroma-cache` named volume so you don't re-download on every container recreation.
|
||
- **No API keys required** for core functionality (local embeddings via ONNX).
|
||
|
||
Both volumes are commented out by default in `docker-compose.yml` — uncomment to enable:
|
||
|
||
```yaml
|
||
- devbox-palace:/home/developer/.mempalace
|
||
- devbox-chroma-cache:/home/developer/.cache/chroma
|
||
```
|
||
|
||
**Air-gapped environments:** pre-populate the `devbox-chroma-cache` volume with the `all-MiniLM-L6-v2/` model contents. The palace volume needs no pre-population.
|
||
|
||
## Gitea MCP server
|
||
|
||
The image includes the [official Gitea MCP server](https://gitea.com/gitea/gitea-mcp) (`gitea-mcp`), providing 50+ MCP tools for interacting with self-hosted Gitea instances — repositories, issues, pull requests, releases, branches, wiki, and Actions.
|
||
|
||
### Setup
|
||
|
||
1. Create a Personal Access Token on your Gitea instance (Settings → Applications → Generate Token, scopes: `repo`, `read:user`).
|
||
|
||
2. Add to your `.env`:
|
||
```env
|
||
GITEA_HOST=https://your-gitea-instance.example.com
|
||
GITEA_ACCESS_TOKEN=your_token_here
|
||
```
|
||
|
||
3. Enable the gitea MCP server in your `opencode.jsonc`:
|
||
```json
|
||
{
|
||
"mcp": {
|
||
"gitea": {
|
||
"type": "local",
|
||
"command": ["gitea-mcp", "-t", "stdio", "--host", "{env:GITEA_HOST}"],
|
||
"environment": {
|
||
"GITEA_ACCESS_TOKEN": "{env:GITEA_ACCESS_TOKEN}"
|
||
},
|
||
"enabled": true
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
The server is installed but disabled by default — it requires authentication to be useful.
|
||
|
||
## Context7 MCP server
|
||
|
||
The image auto-registers a [Context7](https://context7.com) MCP server, which provides up-to-date library documentation and code examples to LLMs at query time. This is a remote MCP server at `mcp.context7.com/mcp` — no local binary is needed.
|
||
|
||
- Auto-registered in the generated `opencode.jsonc` (no manual setup required)
|
||
- Provides documentation for any programming library/framework on demand
|
||
- Requires internet access — useless in air-gapped/offline environments
|
||
|
||
## Shell defaults
|
||
|
||
The image ships a baked `.bash_aliases` and `.inputrc` with quality-of-life defaults. On first container start they are copied from `/etc/skel-devbox/` into `/home/developer/` **only if the target file does not already exist** — so host bind-mounts and any version you've customized inside the container are never overwritten on upgrade.
|
||
|
||
Defaults you get out of the box:
|
||
|
||
- **Prefix history search** on Up/Down arrows (type `git `, press Up, walk back through prior `git ...` commands only). Ctrl-Up / Ctrl-Down still step through full history.
|
||
- **Persistent history** — `$HISTFILE` points at `~/.cache/bash/history`, backed by the `devbox-shell-history` named volume so history survives container recreation. Timestamps, 100 000 entries, dedup.
|
||
- **Case-insensitive tab completion**, coloured completion lists, `show-all-if-ambiguous`.
|
||
- **Aliases** — `ls`/`ll`/`la` use `eza`, `cat` uses `bat`, `gs`/`gd`/`gl` for git, safe `rm`/`mv`/`cp`.
|
||
- **Integrations** — `zoxide` (`z <fragment>` to jump), `fzf` Ctrl-R / Ctrl-T key bindings.
|
||
- **Prompt marker** — `[devbox]` prefix so it's always obvious you're inside the container.
|
||
|
||
### Overriding the defaults
|
||
|
||
**Option A — bind-mount host files.** Uncomment the bind-mount lines in `docker-compose.yml`:
|
||
|
||
```yaml
|
||
- ~/.bash_aliases:/home/developer/.bash_aliases:ro
|
||
- ~/.inputrc:/home/developer/.inputrc:ro
|
||
```
|
||
|
||
> **Single-file bind-mount caveat (all platforms):** Docker bind-mounts the file's **inode**, not its path. When editors like vim, nvim, VS Code, or `sed -i` save a file, they write to a temp file and `rename()` it over the original — creating a new inode. The container stays pinned to the old (now unlinked) inode and never sees the update. This is a kernel limitation ([Docker #15793](https://github.com/moby/moby/issues/15793)), not fixable by Docker. Append-only writes (`echo "alias foo=bar" >> file`) are safe because they modify the same inode. **Workaround:** mount the parent directory instead of the single file (e.g. `~/.config/devbox-shell:/home/developer/.config/devbox-shell:ro`) and source files from there.
|
||
|
||
**Option B — customize inside the container.** Just edit `~/.bash_aliases` or `~/.inputrc` as normal. Pair this with a bind-mount or named volume on the home dir if you want the edits to survive container recreation.
|
||
|
||
### Restoring or diffing defaults
|
||
|
||
The skel files remain available inside every container at `/etc/skel-devbox/`. Useful commands:
|
||
|
||
```bash
|
||
# See what the image currently ships
|
||
cat /etc/skel-devbox/.bash_aliases
|
||
|
||
# Diff your current config against the upstream defaults
|
||
diff ~/.bash_aliases /etc/skel-devbox/.bash_aliases
|
||
|
||
# Reset to the baked defaults
|
||
cp /etc/skel-devbox/.bash_aliases ~/.bash_aliases
|
||
|
||
# …or delete the file and recreate the container — the entrypoint
|
||
# copies from /etc/skel-devbox/ on next start if the target is absent
|
||
rm ~/.bash_aliases
|
||
```
|
||
|
||
## Secret Scanning
|
||
|
||
A [gitleaks](https://github.com/gitleaks/gitleaks) pre-commit hook prevents accidentally committing API keys, passwords, or other secrets.
|
||
|
||
### Setup
|
||
|
||
```bash
|
||
# macOS / Linuxbrew
|
||
brew install gitleaks
|
||
|
||
# Debian/Ubuntu (download binary)
|
||
curl -sSL https://github.com/gitleaks/gitleaks/releases/latest/download/gitleaks_$(uname -s)_$(uname -m).tar.gz | sudo tar -xz -C /usr/local/bin gitleaks
|
||
```
|
||
|
||
The hook runs automatically on every `git commit`. If gitleaks isn't installed, the hook prints a warning and allows the commit (no hard dependency on collaborators).
|
||
|
||
### Bypass
|
||
|
||
For legitimate cases (test data, documentation with example keys):
|
||
|
||
```bash
|
||
git commit --no-verify -m "Add test fixtures"
|
||
```
|
||
|
||
### Configuration
|
||
|
||
Allowlisted paths and rules are in `.gitleaks.toml`. The defaults extend gitleaks' built-in rules and allow `.env.example` and documentation files.
|
||
|
||
## Architecture
|
||
|
||
```
|
||
Host Machine
|
||
├── ~/projects/my-app ──bind mount──▶ /workspace (container)
|
||
├── ~/.ssh ──bind mount──▶ /home/developer/.ssh (ro)
|
||
├── ~/.aws ──bind mount──▶ /home/developer/.aws (Bedrock SSO)
|
||
└── .env ──env vars───▶ provider config + API keys
|
||
|
||
Container (Debian trixie)
|
||
├── opencode binary
|
||
├── oh-my-opencode-slim (optional — multi-agent orchestration plugin, includes Bun)
|
||
├── AWS CLI v2 (SSO + Bedrock auth)
|
||
├── neovim 0.12, tmux, htop, bat, eza, zoxide, uv, rustup, make, gcc, g++, rsync
|
||
├── git, git-crypt, age, ssh, ripgrep, fd, fzf, jq, curl, tree
|
||
├── Node.js (for MCP servers)
|
||
├── Bun (optional — included with oh-my-opencode-slim)
|
||
├── entrypoint.sh (UID adjustment, git config, provider setup)
|
||
└── /workspace ← your code lives here
|
||
```
|
||
|
||
### Data persistence
|
||
|
||
| Path in container | Source | Survives `--rm`? | Contains |
|
||
|---|---|---|---|
|
||
| `/workspace` | Host bind mount | ✅ Yes | Your project files |
|
||
| `/home/developer/.ssh` | Host bind mount (ro) | ✅ Yes | SSH keys |
|
||
| `/home/developer/.aws` | Host bind mount (if configured) | ✅ Yes | AWS credentials/SSO cache |
|
||
| `/home/developer/.local/share/opencode` | Named volume `devbox-data` | ✅ Yes | Session history, memory |
|
||
| `/home/developer/.local/state/opencode` | Named volume `devbox-state` | ✅ Yes | TUI settings (theme, toggles) |
|
||
| `/home/developer/.cache/bash` | Named volume `devbox-shell-history` | ✅ Yes | Bash history (`$HISTFILE`), survives container recreate |
|
||
| `/home/developer/.local/share/zoxide` | Named volume `devbox-zoxide` | ✅ Yes | Zoxide directory history (`z <fragment>` jump targets) |
|
||
| `/home/developer/.local/share/nvim` | Named volume `devbox-nvim-data` | ✅ Yes | Neovim plugins, Mason LSP installs, Lazy plugin cache |
|
||
| `/home/developer/.local/share/uv` | Named volume `devbox-uv` (if configured) | ✅ Yes | Python installs, uv tool installs |
|
||
| `/home/developer/.rustup` | Named volume `devbox-rustup` (if configured) | ✅ Yes | Rust toolchains |
|
||
| `/home/developer/.cargo` | Named volume `devbox-cargo` (if configured) | ✅ Yes | Cargo binaries, registry cache |
|
||
| `/home/developer/.vscode-server` | Named volume `devbox-vscode` (if configured) | ✅ Yes | VS Code server and extensions |
|
||
| `/home/developer/.config/opencode` | Named volume `devbox-opencode-config` | ✅ Yes | `opencode.jsonc`, skills, plus `oh-my-opencode-slim.json` on the OMOS variant |
|
||
|
||
**opencode config** (`opencode.jsonc`) is auto-generated from `OPENCODE_PROVIDER` on each start. It sets provider and model only — no MCP servers. To persist config changes and use custom settings, use the named volume (default) or bind-mount from host (see Custom opencode config above).
|
||
|
||
## License
|
||
|
||
MIT
|