Sign inSign up

devsnowdon/opencode-docker

By devsnowdon

•Updated 1 day ago

Image
0

6.3K

devsnowdon/opencode-docker repository overview

⁠opencode dev container

CI Docker Pulls Docker Image Version License

VersionEmptyDuckFull
v2sizesizesize
v1sizesizesize

OpenCode doesn’t impose an opinion on the sandbox its agents run in. A sandboxed launcher gives you a secure, controlled environment that still lets agents work with the permissions they need.

Snowdon Dev's opencode-docker is a security- and human-control-oriented OpenCode⁠ workflow that runs inside Docker containers. It lets you manage multiple agents, including sub-agents, across distinct worktrees of your repository library, all through a simple CLI. Your project is available in the container at /workspace/project, and toolchain caches (Go, Rust, Node.js, Python) persist between runs.

Every feature is security-first, and convenience features are opt-in. Built for arm devices like the Raspberry Pi⁠ but compatible with x86 and others — please report any issues you find. The opencode version is updated weekly, best-effort; file an issue if yours is out of date. See Build⁠.

⁠Commands

View command list Run commands through the launcher; the workspace defaults to the current directory. Every command has its own help: `opencode:help ` or `opencode: -h`.
CommandAliasPurpose
Start & one-off runs
opencode (start)oc(Re)create the container and attach a TUI
opencode:scaffold [path] <task>ocsfCreate a new project with opencode (task via stdin)
opencode:bg [path] <task>ocbgRun a one-off opencode task (an opencode run in the service) on an existing project
opencode:changes [--output <path>]... [task]occhAnalyse the branch changes and propose a plan, optionally into a writable output (--output, workspace read-only), then run it with opencode:bg
opencode:io --output <path>... [task]ocioRun a task against a read-only workspace with N writable outputs (--output file or directory, repeatable; --allow-empty)
opencode:replocreplInteractive bash shell bound to the workspace



Ensure running
opencode:newocnStop existing containers, then start a fresh session
opencode:upocuStart the container in the background, running nothing
opencode:setup <cmd>ocsetStart the container if needed, then run a command in it
opencode:uptreeocutStart a container for an existing agent worktree (--branch <branch> picks the worktree of a branch)



In the running container
opencode:run <cmd>ocrRun a task (docker task, docker run) in the service, one-off container if not running
opencode:exec <cmd>oceRun a command interactively in the running container
opencode:shellocshexec sh
opencode:compose <args>occPass arguments straight through to docker compose



Stop & inspect
opencode:stopocsStop managed containers
opencode:downocdRemove this project's containers, networks and volumes
opencode:deleteocdelForce-remove managed containers (--all also drops images and networks)
opencode:ls (list)oclList managed containers in a ps-style table



Host-side & meta
opencode:git <args>ocgRun git on the host, in the workspace
opencode:env [ws|workspace]ocenvPrint launcher variables, or the effective workspace path
opencode:create <action>occreCreate a workspace asset on the host (--dockerfile, --worktree)
opencode:updateocudRefresh the launcher repo and its images
opencode:help [command]ochShow help, or help for one command
View command directory cli options ### Common directory options

Accepted by stop, delete, ls and down (each honours the subset that applies to it):

OptionEffect
--all, -aAlso act on one-off (compose run) containers and resources (images, networks)
--other, -oAct on everything except the current workspace and its worktrees
--this, -tAct on the current workspace (the default when no path is given)
--quiet, -qls only: print just the container ids, one per line
--dry-run, -nPrint what would be done without touching anything (down, stop, delete)
--Stop parsing options; everything after is a positional argument

An optional positional argument is a workspace path, or a container-id prefix (ambiguous prefixes are rejected). --other and --this are mutually exclusive, and down refuses both: it only ever removes the current project.

Images and networks are labelled with the workspace that created them, which for a worktree is the worktree's own path, plus the worktree's parent repository. delete --all scoped to a repository therefore covers the repository's worktrees as well, and --other preserves them with the repository.

View create options
⁠Create options

opencode:create (occre) creates a workspace asset on the host. Every action is requested by its own option — there is no default action, so a bare opencode:create lists them — and several can be combined in one invocation, running in the order given:

OptionEffect
--dockerfile, --Dockerfile, -dfCopy Dockerfile.example to <workspace>/ocdocker/ and print the exports that build the workspace image from it
--worktree, --wt, -wCreate an agent worktree opencode:uptree starts, at $SD_AGENT_TREE_ROOT/<project>-dev (or $SD_AGENT_TREE_ROOT/<project>-<branch> when a branch is given)
[branch] (positional)The branch to create for --worktree (default: <project>-dev, created from the current HEAD); it also names the worktree, so several agent worktrees of one project coexist (dev is reserved for the canonical one)
⁠Examples
# a workspace argument overrides the current directory
opencode /home/other/somerepo
opencode:exec /home/other/somerepo sh
opencode:down /home/other/somerepo
opencode:delete "$(pwd)" --all
opencode:ls --quiet ~/somerepo
opencode:compose exec -it opencode sh

# long tasks, then work in the same container
opencode:run npm install
opencode:setup sh -c 'cd front-end && npm install' && opencode

# tasks, replacing the session or analysing the branch first
opencode -c --auto
opencode:changes "identify issues in these changes"
opencode:bg ./ "refactor this module"

# plan with the workspace read-only (only --output is writable), then run the
# plan with bg; changes --output plans the branch changes the same way
opencode:changes --output /tmp/plan.md
opencode:io --output /tmp/out.md < /tmp/create-task-plan.md
opencode:bg ./ < /tmp/out.md

# new projects, either with a real path or under $SD_REPO_HOME
opencode:scaffold /tmp/project < /tmp/sometask.md
opencode:scaffold --path gists/project-1 "Create basic hello world html project"

# workspace assets, and an agent worktree to run in
opencode:create --dockerfile
opencode:create --worktree feature/my-branch
opencode:create --dockerfile --worktree && opencode:uptree

# run on another base image, or pin and cap the CPUs
OPENCODE_IMAGE_URL="devsnowdon/opencode-docker:full-v1" opencode
OPENCODE_CPUSET="2-3" OPENCODE_CPUS="2" opencode

# using opencode v2
OPENCODE_IMAGE_URL="devsnowdon/opencode-docker:empty-v2" \
OPENCODE_IMAGE_URL_TUI="devsnowdon/opencode-docker:empty-v2" opencode

⁠Install

Clone the repository to ~/opencode (the launcher looks for its files there, and a .env in the repository root is read by plain docker compose runs):

git clone https://github.com/snowdon-dev/opencode-docker.git ~/opencode

Only SD_OPENCODE is required; the rest is optional: Compose variables⁠ move toolchain cache paths, Launcher variables⁠ configure the launcher itself.

⁠oh-my-zsh plugin
View install via oh-my-zsh details The launcher sets up the required environment variables, such as `WORKSPACE`, so use it to launch the container. The `omz` plugin is very lightweight and not required — you are encouraged to create your own profiles and environments using whichever shell you prefer. To install it, link it as an oh-my-zsh custom plugin:
mkdir -p "${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/plugins/opencode"
ln -s ~/opencode/omz/opencode.zsh \
    "${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/plugins/opencode/opencode.plugin.zsh"

Then enable it in ~/.zshrc and point $SD_OPENCODE at the repository (the aliases use it to find the repo):

export SD_OPENCODE="$HOME/opencode"
export SD_REPO_HOME="$HOME/repos"
plugins=(... opencode)

After restarting your shell (exec zsh), list the commands with alias | grep -E ^oc. The plugin wraps the launcher in one alias per command (oc, ocs, ocu, ocn, ocsh, ocset, ocr, ocd, ocdel, ocl, occ, occh, ocsf, ocbg, ocio, ocg, ocenv, ocut, och, ocud, ocrepl, occre) — see omz/opencode.zsh⁠ for the exact list.

⁠Environment setup steps
  • SD_OPENCODE must point at the repository (default $HOME/opencode).
  • Docker is required (docker.io⁠); rootless Docker works for extra security.
  • A modern version of bash is required.
  • The opencode binary on your shell path runs the TUI (npm i -g opencode-ai); without it an in-container TUI is used instead.
  • CPUs are unrestricted by default; limit them with OPENCODE_CPUSET (pinning) or OPENCODE_CPUS (count).
  • To apply a custom Dockerfile to every workspace without setting OPENCODE_DOCKERFILE, copy Dockerfile.example to $SD_OPENCODE/Dockerfile.
  • It is recommended to use the API key file method⁠ and configure a provider in the project configuration template to read the key from the file. See Config, data and cache directories⁠ below.
⁠config, data and cache directories

opencode's config, data and cache live on the host under three roots — OPENCODE_CONFIG_ROOT (~/.config/opencode-docker), OPENCODE_DATA_ROOT (~/.local/share/opencode-docker) and OPENCODE_CACHE_ROOT (~/.cache/opencode-docker) — with a per-project directory created under each one automatically at launch. Each root must exist, be a directory and be writable; the launcher offers to create what is missing (SD_YOLO=true does it without asking). If your user is not uid/gid 1000, chown -R 1000:1000 the roots instead.

The first launch of a project copies $OPENCODE_CONFIG_ROOT/template into the project's config; later launches leave it alone, so your changes survive. Delete the project's config directory to re-seed it from the template.

A simple root template for opencode v2 could could be (~/.config/opencode-docker/template/opencode.jsonc):

{
  "$schema": "https://opencode.ai/config.json",

  // Default model for all sessions.
  //"model": "opencode/big-pickle",
  //"model": "opencode-go/gpt-5.6-luna",
  //"model": "opencode-go/gpt-6-luna",
  "model": "opencode-go/mimo-v2.6-flash",
  "providers": {
    "opencode-go": {
      "settings": {
        "apiKey": "{file:/var/run/secrets/api_key}"
       }
     }
   },
  "variant": "low",
  "skills": [
    "/opt/opencode/skills"
  ]
}

Set OPENCODE_CONFIG_DIR, OPENCODE_DATA_DIR or OPENCODE_CACHE_DIR to use one exact directory instead of a root. They are launcher variables: export them in the shell, not in a .env file.

Toolchain caches (OPENCODE_PIP_CACHE_DIR, OPENCODE_NPM_CACHE_DIR, OPENCODE_GO_BUILD_CACHE_DIR, OPENCODE_GO_MOD_CACHE_DIR, OPENCODE_CARGO_REGISTRY_DIR, OPENCODE_CARGO_GIT_DIR, OPENCODE_SCCACHE_DIR) are only mounted when enabled with OPENCODE_CACHE; see Compose variables⁠. Create and chown them only if you enable them.

The container also writes to your workspace, so if something fails on permissions make sure it is owned by the container user:

chown -R 1000:1000 "$WORKSPACE"

⁠Environment variables

⁠Compose variables

Toolchain cache and workspace host paths are configured with environment variables; every one has a default under your home directory.

View the compose variables
VariableDefaultMounted at (container)
WORKSPACE$SD_OPENCODE (repo root)/workspace
OPENCODE_NETWORK–– (external network, see compose/net/docker-compose.network.yml)
OPENCODE_PIP_CACHE_DIR$HOME/.cache/pip/home/other/.cache/pip
OPENCODE_NPM_CACHE_DIR$HOME/.npm/home/other/.npm
OPENCODE_GO_BUILD_CACHE_DIR$HOME/.cache/go-build/home/other/.cache/go-build
OPENCODE_GO_MOD_CACHE_DIR$HOME/go/pkg/mod/home/other/go/pkg/mod
OPENCODE_CARGO_REGISTRY_DIR$HOME/.cargo/registry/home/other/.cargo/registry
OPENCODE_CARGO_GIT_DIR$HOME/.cargo/git/home/other/.cargo/git
OPENCODE_SCCACHE_DIR$HOME/.cache/sccache/home/other/.cache/sccache

The container-side paths are fixed: they must match the user baked into the image (other, uid/gid 1000, see the opencode/Dockerfile.* variants).

⁠Launcher variables

These variables are read by the launcher from the shell environment:

View the launcher variables
VariableDefaultEffect
SD_OPENCODE$HOME/opencodeThe repository holding the launcher and compose files
SD_REPO_HOME/home/$USER/reposRoot for scaffold/bg --path targets, and the parent of the default agent worktree root
SD_AGENT_TREE_ROOT$SD_REPO_HOME/agent-treesRoot holding the agent worktrees uptree starts and create --worktree creates; it must exist
SD_DRIVERautoContainer engine: auto, docker or podman. Only docker is implemented; podman exits with an error
OPENCODE_IMAGE_URLdevsnowdon/opencode-docker:duck-v1– (build arg: base image for the opencode service)
OPENCODE_IMAGE_URL_TUIdevsnowdon/opencode-docker:empty-v1– (image for the tui service)
OPENCODE_IMAGE_VERSIONv1Version layer (v1, v2) of the published images the launcher pulls; the tag defaults above are built from it
OPENCODE_CONTEXT.Docker build context directory passed to the compose build section. Derived from OPENCODE_DOCKERFILE's directory when that is a file path and this is unset
OPENCODE_DOCKERFILEDockerfileDockerfile path (relative to the context) passed to the compose build section
OPENCODE_COMPOSE–Space-separated extra docker compose files, merged into the project via -f
OPENCODE_WORKSPACE–The current opencode workspace context; launches targeting a directory outside it prompt before using its environment
OPENCODE_CACHE–Toolchain cache mounts to enable: false (default) none, all every cache, or space-separated ids (go, node, python, rust); any other id is rejected before the container is composed. Also selects the base image variant (:empty-v1, :duck-v1, :full-v1) unless OPENCODE_IMAGE_URL is set
OPENCODE_CONFIG_ROOT$HOME/.config/opencode-dockerRoot holding the template/ config seeded once per project into projects/<project>, see Config, data and cache directories⁠
OPENCODE_DATA_ROOT$HOME/.local/share/opencode-dockerRoot of the per-project data directories, created under it at launch
OPENCODE_CACHE_ROOT$HOME/.cache/opencode-dockerRoot of the per-project cache directories, created under it at launch
OPENCODE_CONFIG_DIR– (derived from the root)Exact directory mounted as the container's config; when set, OPENCODE_CONFIG_ROOT is not used
OPENCODE_DATA_DIR– (derived from the root)Exact directory mounted as the container's data; when set, OPENCODE_DATA_ROOT is not used
OPENCODE_CACHE_DIR– (derived from the root)Exact directory mounted as the container's cache; when set, OPENCODE_CACHE_ROOT is not used
OPENCODE_BACKEND_ORIGINhttp://opencode:4096, else resolved from compose portBackend URL used for the TUI connection
OPENCODE_SERVER_PASSWORD–Password for the backend's basic auth, passed to both the opencode and tui services. Unset leaves the backend open inside the compose network; the health endpoint the launcher probes stays reachable either way
OPENCODE_CPUSET–CPU pinning (e.g. 2-3); unset by default (Docker uses all CPUs)
OPENCODE_CPUS–CPU limit (e.g. 2); unset by default (no quota)
SD_YOLO–true (case-insensitive) skips the outside-HOME workspace check
SD_YOLO_HOME–true validates workspaces against SD_REPO_HOME instead of $HOME
SD_READ_ONLY–false (case-insensitive) skips the read-only .git mounts
OPENCODE_NET_RANGE172.20.0.0/16CIDR or bare prefix defining the managed IP range
OPENCODE_NET_SUBNET29Subnet mask for each workspace network
OPENCODE_KEY_FILE~/.config/opencode-docker/opencode.keyHost file the api key is read from before every launch: a non-empty one merges compose/secret/docker-compose.secret.yml, which mounts the file into the container at /run/secrets/api_key
DOCKER_ARGS–– (extra args passed to all docker commands)
COMPOSE_ARGS–– (extra args passed to every docker compose command)

⁠How a session starts

opencode (the start command) starts a backend inside the container, waits for it to become healthy, then attaches a TUI — your host opencode binary when one is on PATH, otherwise a one-off in-container tui service. The backend is stopped when the TUI exits.

run, changes and setup use the running container when there is one, a throwaway container otherwise; scaffold and bg always use a throwaway one. io (and changes --output) always run on their own, with the workspace mounted read-only (see opencode:help io).

⁠Security model

  • Workspace .git directories are mounted read-only, so history cannot be rewritten and commands cannot be injected; SD_READ_ONLY=false opts out.
  • opencode:io and opencode:changes --output mount the whole workspace read-only; only the declared --output paths are writable.
  • A worktree's parent repository .git is mounted read-only, keeping git usable while the worktree stays isolated.
  • Each workspace gets its own Docker network, isolated from other workspaces.
  • Toolchain caches and other host mounts are opt-in (OPENCODE_CACHE).
  • The api key travels as a file, never an environment variable.
  • A workspace outside $HOME prompts before it is used, unless SD_YOLO=true.
⁠API key secret

Put your api key in OPENCODE_KEY_FILE (default ~/.config/opencode-docker/opencode.key); a non-empty file is mounted into the container at /run/secrets/api_key and stays out of the container's environment, docker inspect and docker compose config. When the file is missing or empty the backend starts without a key. The container user must be able to read the file.

opencode reads the key from that path only when your config says so: add the reference to the provider in the opencode.json of your config template ($OPENCODE_CONFIG_ROOT/template, see config, data and cache directories⁠). On the v2 images it goes into providers.<id>.settings.apiKey:

{
    "providers": {
        "opencode-go": {
            "settings": {
                "apiKey": "{file:/run/secrets/api_key}"
            }
        }
    }
}

On the v1 images the same {file:/run/secrets/api_key} reference goes into provider.<id>.options.apiKey instead.

⁠Network isolation

OPENCODE_NETWORK picks one of three modes:

  1. Unset (default) — Docker's default bridge network.
  2. @default (recommended) — a bridge network created for this workspace on first use and reused across restarts; clean up with opencode:delete --all.
  3. Named network — any existing Docker network, shared between workspaces or external services.
OPENCODE_NETWORK=@default opencode
OPENCODE_NETWORK="my-shared-net" opencode

Each workspace network gets its own subnet, carved from OPENCODE_NET_RANGE (default 172.20.0.0/16) at OPENCODE_NET_SUBNET (default /29).

⁠Setting variables

Launcher variables are read from the shell environment, not from a .env file. Compose variables resolve in this order: shell environment, a .env file next to the compose files (~/opencode/.env), then the defaults in docker-compose.yml.

opencode:env (ocenv) prints the exported SD_* and OPENCODE_* variables the launcher sees — useful for checking a shell profile before launching anything. Only exported variables appear:

opencode:env | grep '^SD_'
opencode:env | grep '^OPENCODE_CACHE'

opencode:env ws prints the effective workspace path.

⁠Env file example

Add a file like the following to ~/opencode/.env (only the toolchain cache paths you want to move; every other path variable keeps its default):

OPENCODE_PIP_CACHE_DIR=/mnt/usb2/storage/opencode/cache/pip
OPENCODE_SCCACHE_DIR=/mnt/usb2/storage/opencode/cache/rust/sccache

The launcher-managed roots are launcher variables and belong in the shell environment instead — a .env file never reaches the launcher:

export OPENCODE_CONFIG_ROOT=/mnt/usb2/storage/opencode/config
export OPENCODE_DATA_ROOT=/mnt/usb2/storage/opencode/data
export OP

Tag summary

Content type

Image

Digest

sha256:580bc0ee1…

Size

904.2 MB

Last updated

1 day ago

docker pull devsnowdon/opencode-docker