Run the OpenCode CLI inside a locked-down container (Docker or Podman): read-only root filesystem, all Linux capabilities dropped, privilege escalation blocked, and API keys loaded from files instead of the command line. Your current directory is mounted as the only writable workspace.
Run it with the canonical launcher:
bin/opencode-container(recommended) — persists everything to~/.opencode-container/and works from any directory.
# Build the image
make build
# Add a provider API key (one-time)
mkdir -p ~/.opencode-container/secrets
chmod 700 ~/.opencode-container/secrets
echo "your-api-key" > ~/.opencode-container/secrets/anthropic_api_key
chmod 600 ~/.opencode-container/secrets/*
# Run with the wrapper script (recommended)
bin/opencode-containerThe OpenCode TUI starts inside the container. Your current directory is mounted at /workspace; sessions, cache, and settings persist in ~/.opencode-container/. See Secrets Management for other providers.
The bin/opencode-container script is the recommended way to run OpenCode Container. It:
- Picks an engine:
$CONTAINER_ENGINEif set, elsepodman, elsedocker - Persists all data to
~/.opencode-container/(sessions, cache, settings) - Reads secrets from
~/.opencode-container/secrets/ - Uses the current directory as the workspace
- Works from any directory once added to your PATH
| Flag | Default | Description |
|---|---|---|
-w, --websearch |
off | Enable Exa web search |
-e, --experimental |
off | Enable experimental features and models |
-u, --update-config |
off | Overwrite ~/.opencode-container/config/ with the repo's config/ |
--memory |
4g |
Container memory limit |
--cpus |
4 |
Container CPU count |
--host-access |
off | Add the engine's host alias (host.docker.internal for Docker, host.containers.internal for Podman) so the container can reach host services (see Accessing Host Services) |
Everything else is passed through to the OpenCode CLI — including short flags like -m/--model, -c/--continue, and -s/--session, as well as subcommands. Run opencode-container --help to see them all.
Adjust the path below to where you cloned this repository.
Bash — add to ~/.bashrc, then run source ~/.bashrc:
export PATH="$HOME/git/opencode-container/bin:$PATH"Fish:
fish_add_path $HOME/git/opencode-container/binOnce added to your PATH, you can run from any directory:
# Run in the current directory
opencode-container
# Continue a session (passed through to OpenCode)
opencode-container -s ses_2d068fdfaffefxNTts5doK0upT
# Pass subcommands through (cwd is the mounted workspace)
opencode-container -e -w auth logout
opencode-container run "fix the login bug"
# Override the workspace directory
OPENCODE_WORKSPACE=/path/to/project opencode-container
# Raise resource limits for a heavy session
opencode-container --memory 8g --cpus 8The container applies several independent layers of restriction:
- Distroless runtime: no package manager and no compiler; a minimal POSIX shell (
/bin/sh) is present because OpenCode's shell tool and npm require one - Read-only root filesystem: only
/tmp(tmpfs) and mounted volumes are writable - Dropped capabilities:
--cap-drop=ALLremoves all Linux capabilities (principle of least privilege) - No privilege escalation:
--security-opt=no-new-privilegesblocks setuid/setgid exploits - Non-root user: runs as UID 1000 (configurable at build time)
- Resource limits: the wrapper defaults to 4 GB memory / 4 CPUs
- File-based secrets: keys mounted read-only at
/run/secrets— never baked into the image or passed on the command line
API keys live as plain files on the host and are loaded at container start — they are never baked into the image or passed on the command line.
- Set up one file per key as shown in Quick Start (
~/.opencode-container/secrets/, directory700, files600). - The runtime bootstrap (
bootstrap.py) reads only supported secret filenames from/run/secretsand exports them as environment variables. Unsupported files are ignored. Filenames are uppercased; dashes and dots become underscores. - The table below lists the supported provider secrets. Other filenames do not create environment variables.
| Filename | Environment Variable | Provider |
|---|---|---|
anthropic_api_key |
ANTHROPIC_API_KEY |
Anthropic |
openai_api_key |
OPENAI_API_KEY |
OpenAI |
context7_api_key |
CONTEXT7_API_KEY |
Context7 MCP |
google_application_credentials |
GOOGLE_APPLICATION_CREDENTIALS |
Vertex AI |
aws_access_key_id |
AWS_ACCESS_KEY_ID |
AWS Bedrock |
aws_secret_access_key |
AWS_SECRET_ACCESS_KEY |
AWS Bedrock |
Note: the launcher mounts these files read-only at /run/secrets; the
bootstrap loads supported secrets directly into the OpenCode process
environment. OpenCode commands and plugins can therefore access configured
provider secrets. Do not place unrelated files in the secrets directory.
When using the wrapper script (bin/opencode-container):
| Data | Location | Description |
|---|---|---|
| Home Directory | ~/.opencode-container/ |
OpenCode cache, plugins, settings, sessions |
| Secrets | ~/.opencode-container/secrets/ |
API keys and credentials |
| Config | ./config/ (this repo) |
OpenCode configuration, MCP servers, custom skills |
| Workspace | Current directory | Your project files |
By default, 127.0.0.1 inside the container refers to the container itself.
The default bridge network can still have engine- and host-firewall-dependent
reachability to host or local-network services. Launch with --host-access to
add an engine-appropriate host alias:
opencode-container --host-accessInside the container, reach host ports via:
- Docker:
http://host.docker.internal:<port> - Podman:
http://host.containers.internal:<port>
The launcher prints a warning when host access is enabled. Host services must
authenticate requests and should not expose administrative endpoints to the
agent. On Linux, host services must listen beyond 127.0.0.1 — bind them to
0.0.0.0 (or, with Docker, the bridge IP usually 172.17.0.1) if access is
required.
Example — OmniRoute gateway: with Omniroute listening on port 20128, paste this into ~/.opencode-container/config/opencode.json, then launch opencode-container --host-access. It serves an OpenAI-compatible /v1 and is keyless by default; if a key is ever needed, add "apiKey": "{env:OMNIROUTE_API_KEY}" under options. Podman users: use host.containers.internal in baseURL instead.
{
"provider": {
"omniroute": {
"npm": "@ai-sdk/openai-compatible",
"name": "OmniRoute",
"options": {
"baseURL": "http://host.docker.internal:20128/v1"
},
"models": {
"auto": { "name": "OmniRoute auto" }
}
}
}
}make build # Build with auto-detected UID/GID
make build VERSION=1.18.18 # Build a specific OpenCode version
make build-latest # Build the latest OpenCode release
make tag-latest VERSION=1.18.18 # Tag a built version as latest
make shell # Debug shell (builder-tools stage with bash)
make clean # Remove imageThe version examples above match ARG OPENCODE_VERSION in the Dockerfile; check there for the current default.
For advanced users who need custom container configuration. The wrapper script is the maintained reference and defaults to 4 GB / 4 CPUs. Identical flags work with both docker run and podman run. Adjust the config mount to your clone path.
docker run --rm -it \
--workdir /workspace \
--read-only \
--tmpfs /tmp:exec,size=512m,mode=1777 \
--cap-drop ALL \
--security-opt=no-new-privileges \
--memory=2g \
--cpus=2 \
-v ~/.opencode-container:/app:rw \
-v /path/to/opencode-container/config:/app/.config/opencode:rw \
-v $(pwd):/workspace:rw \
-v ~/.opencode-container/secrets:/run/secrets:ro \
opencode-container# Default UID/GID (1000) — same flags for podman build
docker build -t opencode-container .
# With your UID/GID (recommended)
docker build --build-arg USER_UID=$(id -u) --build-arg USER_GID=$(id -g) -t opencode-container .
# With version tag
docker build --build-arg USER_UID=$(id -u) --build-arg USER_GID=$(id -g) -t opencode-container:1.18.18 .make build does this for you with whichever engine is installed (override with ENGINE=docker or ENGINE=podman).
Place an optional PEM bundle named custom-ca.crt at the build-context root and run make build. The image keeps Debian's default certificates, splits and validates each certificate from the bundle, then runs update-ca-certificates after ca-certificates is installed. The normalized certificates are retained so hash-based TLS lookups continue to work in the distroless runtime.
This uses a BuildKit-capable builder to read the optional file without requiring a placeholder in the repository.
The custom CA is installed before the remaining build-time HTTPS downloads, including NodeSource and OpenCode. npm is configured to use the resulting system CA bundle for both build-time and runtime registry access. The initial Debian package bootstrap still uses the base image's system trust.
Export credential-free proxy variables in your shell and make build / make build-builder-tools. BuildKit supplies them to network commands without storing them as image ENV values; runtime commands receive both upper- and lower-case forms (so apt/curl/npm all pick them up):
export HTTP_PROXY=http://proxy.example:3128
export HTTPS_PROXY=$HTTP_PROXY
export NO_PROXY=localhost,127.0.0.1
make buildCredential-free proxy URLs are forwarded into the running container, so
OpenCode inside it can reach the network. Proxy URLs containing embedded
credentials are rejected; use a credential-free proxy or configure proxy
authentication outside this launcher. If a previously failed (no-proxy) build
left stale layers, run make prune-cache first. Note: if your proxy terminates
TLS with a corporate CA certificate, build-time certificate validation will
still fail unless that CA is added to the image.
The image uses a multi-stage build with a distroless runtime:
- Base:
gcr.io/distroless/base-debian13(no package manager; Debiandashis added as/bin/shfor OpenCode's shell tool and npm) - Node.js: Node 24 from NodeSource, runtime dependencies extracted via
collect-runtime-deps.sh - Python: Python 3 with venv support from Debian 13
- OpenCode: installed via the official, checksum-verified installer in the build stage
- Runtime collector: resolves and verifies every executable in the Dockerfile manifest before the final image is assembled
- Bootstrap:
bootstrap.pyloads supported secrets from/run/secrets, then execs OpenCode