Run opencode inside Docker instead of installing it locally.
README.md(this file): Docker setup, plugins, run flow, auth, config, and security notes..env.dist: template for local environment variables (PLUGINS,OPENCODE_VERSION, etc.).docs/opencode-commands-skills-tools.md: slash commands, skills, and custom tool reference..opencode/config/AGENTS.md: default agent behavior and skill loading policy.
# 1) Build image (run once, or after Dockerfile changes)
make opencode-build
# 2) Start container
make opencode-run
# -> http://localhost:4096
# 3) Stop and remove container
make opencode-downBefore first run, copy both templates and configure them for your environment.
1. Compose override — sets the path to your projects directory. Full absolute paths are required to avoid Docker-in-Docker volume mounting issues.
cp compose.override.yml.dist compose.override.ymlEdit compose.override.yml and replace the placeholder path:
services:
opencode:
volumes:
- /full/path/to/my/projects:/full/path/to/my/projects2. Environment file — sets local configuration variables.
cp .env.dist .envThe container runs as a non-root user matching your host UID/GID (detected automatically by the Makefile).
To pin a specific OpenCode version, set OPENCODE_VERSION in .env:
OPENCODE_VERSION=1.15.4Plugins extend the base image with additional tools and libraries. They are installed as separate optional layers — the base image stays lean by default.
Available plugins:
| Plugin | Adds |
|---|---|
midi |
fluidsynth, timidity, mido, pretty_midi, music21, and related Python audio libraries |
excel |
openpyxl for reading and writing .xlsx files |
browser |
Playwright Chromium (headless, MCP-controlled) — see Browser MCP below |
image |
vips (re-encode/strip images), exiftool (inspect metadata), clamav/clamav-freshclam (scan for malware) — see Image plugin below |
To enable plugins, set PLUGINS in your .env file (comma-separated):
PLUGINS=midi,excelThen build with:
make opencode-build-pluginsThe browser plugin adds @playwright/mcp@0.0.72 — a local MCP server that lets the browse agent control a headless Chromium browser.
Opt-in:
# .env
PLUGINS=browsermake opencode-build-plugins
make opencode-runRuntime behavior:
- Headless by default. The browser runs without a visible UI (
PLAYWRIGHT_HEADLESS=true). - Enabled when active. The MCP entry is
enabled: truewhen the browser plugin is active — activating the plugin is the opt-in. - Tool access is user-configurable. By default all agents can use
playwright_*tools. Restrict or scope access viaopencode.jsoncif needed. - Configured agents need explicit tool access. If an agent in
opencode.jsonchas atoolsblock,playwright_*tools are not granted automatically — you must add them: - Non-persistent state. No browser profile or cache is retained across container restarts (non-persistent by design).
- Standard outbound network. The container uses the same outbound network as the base image; no extra network restrictions are added for browser traffic.
- Reaching host services. To access services running on the host machine from within the browser (e.g. a local dev server), use
host.docker.internalinstead oflocalhost.
Operations:
- Owner: Repo Maintainers
- Cadence: Monthly dependency/version review + immediate review on any OpenCode release,
@playwright/mcprelease, or CVE advisory. Review cadence: monthly.
The image plugin installs vips (via libvips-tools), exiftool (via libimage-exiftool-perl), and clamav/clamav-freshclam. vips, exiftool, and clamscan are CLI tools the agent invokes manually per file (see the recommended order below) — they do not run on a schedule or watch for files.
Opt-in:
# .env
PLUGINS=imagemake opencode-build-plugins
make opencode-runStartup behavior: every container start runs docker/plugins/image/image.entrypoint.sh as root, before the privilege drop to the opencode user:
freshclam --stdoutupdates the virus definitions. This requires network access to the ClamAV signature mirrors on every start — there is no startup-time skip or caching of "already up to date"; the update runs unconditionally. If it fails, container startup fails (fail-closed) rather than starting with stale or missing definitions.- The hook then proves the final
opencodeuser can actually load and use that database, by runninggosu opencode clamscan --no-summary -- /usr/bin/trueagainst a harmless, stable file. If this readiness check fails, startup also fails.
The virus definitions in /var/lib/clamav persist across container recreations only when the image plugin's Compose fragment is applied (i.e. you build/run through docker/compose-with-plugins.sh, which the Makefile targets do) — it declares the named volume clamav_db mounted at /var/lib/clamav. Without that fragment, freshclam re-downloads the full database on every start.
Recommended first-pass order for an untrusted image (virus definitions are already current thanks to the startup hook above):
- Scan the original file:
clamscan "/path/to/input.jpg" - Decode and re-encode with
vips, stripping metadata explicitly. The[strip]option must be set per output format (JPEG, PNG, WebP):vips copy "/path/to/input.jpg" "/path/to/output.jpg[strip]"
- Inspect the re-encoded output with ExifTool — some technical fields (image dimensions, color profile, format-level tags) are always present and expected; the goal is confirming no unexpected metadata survived, not an empty report:
exiftool "/path/to/output.jpg" - Scan the re-encoded output again:
clamscan "/path/to/output.jpg"
clamscan exit codes: 0 = clean, 1 = virus/malware detected, 2 = error (e.g. file access, corrupted definitions). Treat a non-zero exit as a signal to stop and investigate manually — do not script automatic deletion of flagged files.
Important caveat: ClamAV scanning supplements the vips re-encode step; it does not prove a file is safe or uncompromised. Signature-based scanning only catches known threats, and image parsers can have undiscovered vulnerabilities. Re-encoding through vips (which discards the original byte stream and rebuilds pixel data) is the primary defense; ClamAV is a secondary check, not a guarantee.
Each plugin lives in its own subdirectory docker/plugins/<name>/ and may include up to five files:
| File | Purpose | Required |
|---|---|---|
<name>.dockerfile |
apt/system dependencies injected into the image | Yes |
<name>.package.json |
npm deps merged into .opencode/config/package.json at build time |
No |
<name>.opencode.jsonc |
MCP config fragment injected into opencode.jsonc at build time |
No |
<name>.entrypoint.sh |
startup hook run as root before the privilege drop (see Image plugin for an example) | No |
<name>.compose.yml |
Compose fragment layered onto compose.yml via docker/compose-with-plugins.sh |
No |
Copy the template for the Dockerfile layer:
cp docker/plugins/plugin.dockerfile.dist docker/plugins/myplugin/myplugin.dockerfileThe template documents the available package managers (apt-get, pip) and their conventions for this base image.
If you already use opencode locally, you can reuse existing credentials and skip signing in again:
# macOS / Linux
cp ~/.local/share/opencode/auth.json .opencode/share/auth.jsonOtherwise, run make opencode-run, open http://localhost:4096, and authenticate in the UI.
Credentials are written automatically to .opencode/share/auth.json.
Note:
auth.jsonmay contain provider tokens. It is covered by.gitignoreand is not committed.
.opencode/config/ is mapped to the opencode config directory inside the container.
The committed template is .opencode/config/opencode.jsonc.base.dist. Copy it to get started:
cp .opencode/config/opencode.jsonc.base.dist .opencode/config/opencode.jsonc.baseEdit opencode.jsonc.base to customize behavior — providers, models, permissions, agents.
When you run make opencode-build-plugins, this file is used to generate opencode.jsonc.
Note: Do not edit
opencode.jsoncdirectly — it is a generated file and will be overwritten on the next plugin build. Editopencode.jsonc.baseinstead.
If you are not using plugins, you can create opencode.jsonc directly:
{
"$schema": "https://opencode.ai/config.json",
"autoupdate": false,
"share": "disabled",
"enabled_providers": ["github-copilot"],
"permission": {
"bash": "ask",
"*": "allow"
}
}Both files are gitignored, so local customization does not affect others.
This repo uses .opencode/config/AGENTS.md to define default skill-loading behavior.
concise-preciseis loaded by default for user-facing responses.karpathy-guidelinesis loaded on top for non-trivial code changes.
This means response style stays concise by default, while coding workflow guidance is added when implementation tasks are complex.
The permission field controls which tool calls require approval.
The example above asks for confirmation on every bash command while allowing everything else.
To require approval for additional tools:
{
"permission": {
"bash": "ask",
"edit": "ask",
"write": "ask",
"*": "allow"
}
}See the permissions docs for all options.
The container mounts /var/run/docker.sock so opencode can run Docker commands on the host.
Socket permissions are handled automatically at startup by docker/entrypoint.sh:
it reads the socket owner GID and adds the opencode user to that group before dropping privileges.
Mounting the Docker socket grants the container full access to the host Docker daemon, so this setup does not provide meaningful isolation from the host.
| Host OS | Typical socket GID |
|---|---|
| macOS (Docker Desktop) | 0 (root) |
| Linux (Docker Engine) | 999 or varies |
This repo includes custom slash commands, a reusable skill system, and a PDF extraction tool.
See OpenCode commands, skills, and tools for the full command catalog and skill/tool reference.
If you only need the defensive baseline in a project:
/security-profile init
/security-profile refreshHappy agentic coding! Suggestions welcome!