The opencode container¶
opencode packages OpenCode, the provider-agnostic
terminal coding agent, in the repository's standard hardened workload
environment: non-root execution, a read-only root filesystem, minimal Linux
capabilities, and deny-by-default outbound networking.
It uses python:3.14-slim-trixie plus Node LTS. OpenCode is installed from
the official opencode-ai npm package. Python, uv, and uvx are included
so OpenCode can run Python-based MCP servers and project tooling without a
custom derivative image.
Build¶
Matching your host user's UID/GID¶
The image defaults to UID/GID 1000:1000. Everything the agent writes to
the bind-mounted project directory or the persistent home volume is owned
by whatever UID/GID the container ran as — if that doesn't match your host
account, those files aren't owned by you on the host (permission errors,
needing sudo to clean up). Rebuild with matching values instead:
docker build --build-context shared=agent-images/shared \
--build-arg UID=$(id -u) --build-arg GID=$(id -g) \
-t opencode:$(whoami) agent-images/opencode
Dockerfile splits into a base stage that installs everything —
apt/Node.js/uv baseline, packages-apt.txt/packages-uv.txt, OpenCode
itself, and the account-scoped packages-npm.txt/tools-uv.txt — under a
fixed placeholder account (UID/GID 1000:1000), and a final stage that
only remaps that account to the UID/GID build args (usermod/groupmod
plus a chown -R of what base already installed) when they differ from
the placeholder. Nothing in final reinstalls packages, so rebuilding for a
different UID after the first build is fast.
On a host shared by multiple accounts, tag the image per user
(opencode:alice, opencode:bob) rather than overwriting a shared
opencode tag — and use that same tag, plus a per-user home volume
(opencode-home-alice rather than the shared opencode-home), in the run
command below. The shell-shortcut
functions do both
substitutions automatically from a single CONTAINED_OPENCODE_TAG
variable; if invoking docker run directly instead, substitute your tag
into both the opencode-home volume name and the trailing opencode image
reference in the command below — copying it unchanged still runs the shared
opencode:latest.
Run¶
models.dev is included because OpenCode fetches its provider/model catalog
on startup; opencode.ai covers OpenCode's first-party hosted services.
docker run -it --rm \
--security-opt=no-new-privileges \
--read-only --tmpfs /tmp:exec --tmpfs /run \
--cap-drop=ALL --cap-add=NET_ADMIN --cap-add=NET_RAW --cap-add=SETUID --cap-add=SETGID \
-e 'AGENT_ALLOWED_EGRESS=opencode.ai,models.dev' \
-v opencode-home:/home/opencode \
-v "$PWD":"/workspace/$(basename "$PWD")" \
-w "/workspace/$(basename "$PWD")" \
opencode
Hardening flags¶
| Flag | Purpose |
|---|---|
--security-opt=no-new-privileges |
Prevents escalation through setuid binaries. |
--read-only |
Keeps the root filesystem immutable. |
--tmpfs /tmp:exec and --tmpfs /run |
Provide ephemeral writable locations for the egress-control tooling and runtime files. OpenCode's OpenTUI renderer dynamically loads a native library from /tmp, so that mount must be executable. |
--cap-drop=ALL |
Removes Docker's default capabilities. |
NET_ADMIN and NET_RAW |
Required for the default-deny iptables/ip6tables rules and domain allowlists. |
SETUID and SETGID |
Let gosu switch from the setup user to the unprivileged opencode user. |
The opencode-home volume persists OpenCode's configuration and credentials,
including its XDG locations under ~/.config/opencode and
~/.local/share/opencode, along with user-installed npm/Python tools.
Custom configuration¶
OpenCode loads global and project configuration layers, but the global file's single-file bind-mount and mutation behavior has not yet been verified for this image. See custom configuration files for the state-preservation constraints and local models for a version-recorded candidate project layer.
Providers and egress control¶
The container defaults to deny-all when neither AGENT_ALLOWED_EGRESS nor a
mounted /etc/agent/egress-allowlist.txt file is provided. The mounted file
takes precedence. See agent-images/opencode/examples/egress-allowlist.txt
for the default OpenCode hosts.
Mount a host-maintained policy file read-only with
-v "/path/to/egress-allowlist.txt":/etc/agent/egress-allowlist.txt:ro.
Add each provider endpoint that you configure, for example
api.anthropic.com, api.openai.com, or openrouter.ai; package registries,
MCP servers, and source-control hosts also need explicit entries when used.
Use -e 'AGENT_ALLOWED_EGRESS=*' only when unrestricted egress is
intentional; quoting prevents shells such as zsh from treating * as a
filename pattern.
Gateway-client mode¶
Setting AGENT_GATEWAY_HOST switches the image from its in-container
allowlist to an SSH tunnel through agent-gateway. The
gateway then owns all egress enforcement, so
AGENT_ALLOWED_EGRESS/egress-allowlist.txt are ignored by the workload.
Use the same gateway key, host-key pinning, bootstrap-rule, and Cloudflare
Access configuration described in
the Claude Code gateway guide.
Optional build-time tools¶
Edit packages-apt.txt, packages-npm.txt, tools-uv.txt, or
packages-uv.txt and rebuild to add general-purpose tools. tools-uv.txt
(uv tool install) is for standalone Python CLI tools, each in its own
isolated venv; packages-uv.txt (uv pip install --system) is for plain
importable libraries with no console-script of their own, installed into the
image's system Python instead. No plugins or skills are seeded in v1; all
OpenCode state lives under the persisted home volume.
The image also sets UV_LINK_MODE=copy — see claude-code's uv cache
notes for why.