Skip to content

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

docker build --build-context shared=agent-images/shared \
  -t opencode agent-images/opencode

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.