Run OpenCode in a Docker container: the official ghcr.io/anomalyco/opencode image, pinned, plus everything it leaves out. A non-root user, git, one volume that keeps your logins and sessions across rebuilds, a compose file for the TUI, an egress-firewalled autonomous mode, the headless server and web UI behind basic auth, and examples for CI and for running it always-on in the cloud.
OpenCode's install docs list docker run -it --rm ghcr.io/anomalyco/opencode as one way to run it. That image is Alpine, the OpenCode binary, and ripgrep, running as root, with no git, no persistence, and the auto-updater on. Every file the agent writes into a bind mount comes out root-owned, every docker run starts logged out, and the binary can replace itself mid-session. This repo is the layer between that image and something you would run every day, in CI, or on a server.
Container behavior below (build, non-root file ownership, credential and session persistence, config tiers, firewall allow and block, server auth and health, the custom-provider config) was verified by building and running this exact image with OpenCode 1.18.25 in August 2026. CLI flags and config keys are per the official docs, linked throughout.
- OpenCode allows every tool by default. Per the config docs, "opencode allows all operations without requiring explicit approval" until you write
permissionrules. Verified here: a plainopencode runwrote a file with no prompt and no--autoflag. Productive on your laptop, alarming on a shared box. Inside the container a badrm -rfhits/workspace, not your home directory, SSH keys, or other projects. - One image, same everywhere. The version is pinned and the auto-updater is off (the docs: "OpenCode will automatically download any new updates when it starts up" unless
autoupdateis false). Every teammate and every CI run gets the same agent. - Root is the upstream default. The official image has no user at all. This one runs OpenCode as
opencode(UID 1000, overridable at build time), so files land in your project owned by you. - Servers need a boundary too.
opencode serveandopencode webturn the agent into an HTTP service with full tool access to its working directory. A container, basic auth, and a loopback port binding are the minimum to put around that.
git clone https://github.com/agent37-platform/opencode-docker.git
cd opencode-docker
docker compose run --rm opencodeThat opens the OpenCode TUI with the current directory mounted at /workspace. Pick a provider with /connect (or export a key, next section). Credentials, sessions, and config land in the opencode-home volume, so every later run and every rebuild stays logged in.
No key yet? As of 1.18.25 the free OpenCode Zen models (opencode/big-pickle and friends) answer with no credentials configured at all; that is what this repo's own verification used. The Zen docs call them "available for a limited time", so treat them as a way to try the container, not something to build on.
Work on a real project instead of this repo:
WORKSPACE=~/code/myapp docker compose run --rm opencodePrefer raw docker?
docker build -t opencode-docker .
docker run --rm -it \
-v opencode-home:/home/opencode \
-v "$PWD:/workspace" \
opencode-dockerPin a different release with --build-arg OPENCODE_VERSION=1.18.22 (any version tag on ghcr.io/anomalyco/opencode), and match your host user with --build-arg UID=$(id -u) --build-arg GID=$(id -g) if files created in /workspace come out with the wrong owner (Linux hosts; Docker Desktop on macOS/Windows maps ownership for you). On SELinux-enforcing hosts (Fedora/RHEL, podman), add :z to the workspace mount ("$PWD:/workspace:z") or the container cannot read your project.
All the documented options work here (providers docs):
| Method | How | Best for |
|---|---|---|
/connect in the TUI |
Pick a provider, paste the key; it lands in ~/.local/share/opencode/auth.json inside the volume |
Daily use |
| Provider env var | Export OPENROUTER_API_KEY, ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY (or put it in .env); OpenCode enables a provider when its key is in the environment |
Headless and CI |
| OpenCode Zen | OPENCODE_API_KEY, or /connect |
The curated paid models behind opencode.ai |
| Subscription logins | /connect inside the container for GitHub Copilot, ChatGPT Plus, GitLab Duo; the browser step happens on your host and the token persists in the volume |
People who already pay for one of those |
Verified: a placeholder OPENROUTER_API_KEY made opencode models openrouter list models, and an auth.json written in one container was picked up by opencode auth list in the next one. One thing not to try: the providers docs say Anthropic explicitly prohibits using a Claude Pro/Max subscription with OpenCode, and the plugins that did it were removed in 1.3.0. Use an Anthropic API key, or Claude models through OpenRouter.
Three container-specific things the docs do not spell out:
- State lives in four directories, not one. Credentials, the sessions database, and logs are in
~/.local/share/opencode; config, agents, and plugins in~/.config/opencode; locks in~/.local/state/opencode; the provider catalog and downloaded binaries in~/.cache/opencode. One named volume on/home/opencodecatches all of them, which is what the compose file does. If you prefer bind mounts, mount at least the first two. - Config tiers still apply inside. The precedence order is global (
~/.config/opencode/opencode.json) <OPENCODE_CONFIGfile < projectopencode.jsonin/workspace<OPENCODE_CONFIG_CONTENTinline JSON < managed/etc/opencode/opencode.json. This image uses the managed tier for exactly one key,autoupdate: false, so nothing below it can turn the updater back on. Both env forms were verified to apply in the image (each switched the default model). - Never mount host secrets (
~/.ssh,~/.aws, a~/.gitconfigwith credential helpers) into a container whose agent runs with every tool allowed. Anything inside the boundary is readable by whatever the agent executes, including its own credentials volume. Give the container copies of exactly what the task needs.
docker compose run --rm yoloSame TUI, behind a default-deny egress firewall (init-firewall.sh, nftables). Only what OpenCode needs is reachable: models.dev (the provider catalog it loads at startup), opencode.ai (Zen models and session sharing), the direct provider APIs (api.anthropic.com, api.openai.com, openrouter.ai, generativelanguage.googleapis.com), registry.npmjs.org (npx-launched MCP servers), and GitHub's published IP ranges. Everything else is dropped before it leaves the container. Verified behavior of this exact image: api.github.com and the npm registry answer, example.com and google.com time out, and a model turn completes behind the firewall.
Your project needs more endpoints? Allow them per run, no file edits:
FIREWALL_ALLOW_DOMAINS="pypi.org files.pythonhosted.org" docker compose run --rm yoloAbout --auto: OpenCode's auto mode approves permission requests that are not explicitly denied. Since the default config already allows everything, it only changes anything once you have written ask rules, so this image does not pass it for you. For headless runs with ask rules in place, opencode run --auto "..." is the documented form.
Honest limits, so you can decide what to trust it with: DNS stays open (the allowlist has to resolve, so DNS tunneling remains an exfil channel), allowlisted hosts like GitHub are themselves reachable, the allowlist IPs are a startup snapshot (a long session can lose an endpoint if a CDN rotates IPs; restart the container to re-resolve), and a malicious repo can still read anything inside the container. The firewall shrinks the blast radius; it does not make untrusted code safe. For hostile-code review, use a throwaway checkout, no credentials volume, and a short-lived key.
opencode run is the non-interactive mode, and it works exactly as documented inside the image:
docker run --rm -i \
-e OPENROUTER_API_KEY \
-v "$PWD:/workspace" \
opencode-docker \
opencode run --format json --model openrouter/anthropic/claude-sonnet-5 \
"Explain the failing test in tests/api.test.ts"--format json streams NDJSON events (step_start, text, step_finish with token counts and cost) that a pipeline can parse; --continue and --session <id> resume earlier work from the volume; --agent, --file, and --title do what their names say. A ready-to-copy GitHub Actions workflow is in examples/github-actions.yml.
If what you want is the "/oc fix this" bot on issues and PRs, that already exists: OpenCode's own GitHub integration (opencode github install, action anomalyco/opencode/github). This image is for running a pinned agent you control, on your own triggers.
echo 'OPENCODE_SERVER_PASSWORD=change-me' >> .env
docker compose up -d serve # HTTP API on http://127.0.0.1:4096
docker compose up -d web # same server plus the browser UIopencode serve is the headless HTTP server the TUI, the desktop app, and the SDK talk to; opencode web adds the browser interface on top of it. Both read OPENCODE_SERVER_PASSWORD for HTTP basic auth (username opencode, or set OPENCODE_SERVER_USERNAME). Verified: without credentials every route returns 401; with them GET /global/health returns {"healthy":true,"version":"1.18.25"}, which is also the compose healthcheck.
Two guardrails are built in. The port is published on 127.0.0.1 only; to reach it from another machine, put a reverse proxy with HTTPS or a tunnel in front rather than changing the bind. And the entrypoint refuses to start either server on all interfaces without a password, because a bare opencode serve --hostname 0.0.0.0 is an unauthenticated shell with tool access to /workspace. Connect a local TUI to the container's server with opencode attach http://127.0.0.1:4096.
As of September 2026 there are several ways to run OpenCode in a box. They solve different problems:
| Option | What it is | Reach for it when |
|---|---|---|
| This repo | The official image, pinned, plus non-root, persistence, compose, firewall, server auth | You want docker run, compose, CI, or server usage from a readable Dockerfile you can extend |
| Official image alone | docker run -it --rm ghcr.io/anomalyco/opencode: Alpine + binary + ripgrep, root, no volumes |
A one-off try with nothing to keep |
| Docker Sandboxes | sbx run opencode: Docker's managed microVM per session, provider keys injected through a host-side proxy |
You want managed per-session VMs with secrets kept on the host, and accept its own tooling |
| pilinux/opencode-docker, nezhar/opencode-container | Community images: one auto-builds ghcr.io/pilinux/opencode per release and ships a compose file for opencode serve; the other wraps docker run in a helper script with an optional API-logging proxy |
You want their specific extras and are fine with a third-party image |
| opencode-devcontainers | An OpenCode plugin for per-branch devcontainer isolation | You live in VS Code devcontainers and want one container per branch |
A container on your laptop dies with your laptop. We run Agent37, which hosts any public Docker image as a persistent instance: from $1.99/mo shared, or $4.94/mo dedicated for an always-on 2 vCPU / 4 GB box with 6 GB disk, metered per minute, delete anytime. New workspaces get a $1 starter credit, so the first instance is free to try, no card required.
The zero-setup path is the one-click template: agent37-opencode runs OpenCode on the managed model out of the box, no account or key needed. Its surface is Agent37's chat gateway on the instance URL (POST /v1/responses with sessions backed by OpenCode's native session ids, plus a browser terminal and file browser), which is what you want when OpenCode is the engine behind an app.
The child image in examples/agent37/ is the other surface, and the reason this section exists: OpenCode's own web UI and server API on the instance URL, the exact pinned image this repo builds, your Dockerfile on top. It runs on the same managed model endpoint every Agent37 instance boots with (AGENT37_LLM_PROXY_URL plus a per-instance AGENT37_MANAGED_TOKEN, metered to the instance budget, per the managed services docs). examples/agent37/opencode.json wires OpenCode to it as a custom provider using OpenCode's {env:...} substitution, so no key is baked into the image and the token being reissued on every restart does not matter. That config was verified in this image against a mock OpenAI-compatible endpoint: the bearer token arrived, the stream came back, opencode run printed the reply. You can of course bring your own provider key as instance env instead. And if what you want is the chat gateway on top of a custom image, build FROM ghcr.io/agent37-platform/opencode-base per the OpenCode template docs rather than from this repo.
# 1. Build the child image for amd64 (Agent37 runs linux/amd64) and push anywhere public
docker build -t opencode-docker .
docker build --platform linux/amd64 -t ghcr.io/you/opencode-agent37:v1 examples/agent37
docker push ghcr.io/you/opencode-agent37:v1
# 2. Register it as a template; port 4096 is the OpenCode server (API key: dashboard -> Cloud -> API keys)
curl -X POST https://api.agent37.com/v1/templates \
-H "Authorization: Bearer $AGENT37_API_KEY" -H "Content-Type: application/json" \
-d '{ "name": "opencode", "image_ref": "ghcr.io/you/opencode-agent37:v1", "default_port": 4096 }'
# 3. Provision an always-on instance; the password protects the server and web UI
curl -X POST https://api.agent37.com/v1/instances \
-H "Authorization: Bearer $AGENT37_API_KEY" -H "Content-Type: application/json" \
-d '{ "template": "opencode", "env": { "OPENCODE_SERVER_PASSWORD": "change-me" } }'
# 4. Open https://<id>.agent37.app in a browser for the OpenCode web UI (basic auth),
# or drive it headless over the exec API from your app or your own cron
curl -X POST https://api.agent37.com/v1/instances/<id>/exec \
-H "Authorization: Bearer $AGENT37_API_KEY" -H "Content-Type: application/json" \
-d '{ "command": "cd /workspace && opencode run --format json \"Triage the open issues in this repo\"" }'Exec calls are capped at 280 seconds: for longer agent runs, start the turn in the background (nohup opencode run ... > /tmp/out.json &) and collect the file with a second exec. Two billing facts worth knowing up front: managed-model calls are metered at provider cost with no markup, and an instance created with no budget has a managed-services cap of zero, so give it one on the create call (or in the dashboard) or the first managed-model turn returns 402. Full walkthrough, including registry-free cloud builds (npx agent37 templates build, which needs a publicly pullable FROM), budgets, and updates: Agent37 custom image docs.
| File | Purpose |
|---|---|
Dockerfile |
FROM ghcr.io/anomalyco/opencode:<pinned> + git, bash, curl, non-root user, managed config, firewall tools |
docker-compose.yml |
opencode (TUI), yolo (firewalled TUI), serve (API), web (browser UI) services, one persistent home volume |
opencode.managed.json |
The managed-tier config: autoupdate: false |
init-firewall.sh |
Default-deny egress allowlist (OpenCode + providers + GitHub + npm), extensible via env var |
entrypoint.sh |
Raises the firewall when FIREWALL=1, refuses a password-less network-bound server, then execs your command |
examples/github-actions.yml |
Headless OpenCode in CI |
examples/agent37/ |
Child image + config for an always-on instance on Agent37's managed model |
tests/smoke.sh |
Keyless checks the CI runs: version, non-root, managed config, env-var providers, server auth and health |
tests/mock-openai.py |
Mock OpenAI-compatible endpoint used to verify the custom-provider config without spending tokens |
.env.example |
Every knob, documented |
The image stays close to upstream on purpose: no Node.js, no second copy of OpenCode, no wrapper CLI. Add your project's toolchain in a child image (FROM opencode-docker + apk add ... as root, then USER opencode).
MIT for the files in this repo. Not affiliated with Anomaly or the OpenCode project; OpenCode is MIT-licensed and the image here builds on their official container.