Run OpenCode, code-server, and a Playwright MCP browser sidecar with a shared filesystem and persistent data.
This setup is meant for people who want a browser-based AI coding agent and VS Code always available on a VPS or home server.
- OpenCode web UI served directly by the
opencodebinary and code-server from one container - Playwright MCP sidecar for browser automation from the agent
- GitHub CLI available in the container for
ghcommands - Shared
/reposworkspace between both apps - Persistent OpenCode data, toolchains, and VS Code extensions
- Prewired MCP config for Context7 and Playwright
- Traefik-ready HTTPS routing for separate subdomains
- SSH key mounting for private Git access and inbound SSH login via
authorized_keys - Preconfigured safeguards that block OpenCode from reading common secret files
You need:
- Docker and Docker Compose
- At least one LLM provider API key such as
ANTHROPIC_API_KEY,OPENAI_API_KEY, orGEMINI_API_KEY - A server or machine where the container can keep running
- Traefik with a
proxynetwork if you want the included domain-based routing - Optional:
GH_TOKENorGITHUB_TOKENif you wantghpre-authenticated
git clone <your-repo-url> opencode-docker
cd opencode-docker
cp .env.example .env
mkdir -p repos share agents config/mise config/opencode
docker compose up -dThen edit .env and set at least:
OPENCODE_SERVER_PASSWORDANTHROPIC_API_KEYor another supported provider keyOPENCODE_DOMAINandCODE_SERVER_DOMAINif you are using TraefikSSH_KEY_PATHif your SSH keys are not in~/.ssh
The included OpenCode config points to the Playwright MCP sidecar at http://playwright-mcp:8931/mcp, so browser automation is available as soon as the stack starts.
After startup:
- OpenCode:
https://<OPENCODE_DOMAIN> - code-server:
https://<CODE_SERVER_DOMAIN>
| Variable | What it does |
|---|---|
OPENCODE_SERVER_PASSWORD |
Required password for the OpenCode web UI |
OPENCODE_SERVER_USERNAME |
Username for the OpenCode web UI |
CODE_SERVER_PASSWORD |
Optional separate password for code-server |
ANTHROPIC_API_KEY / OPENAI_API_KEY / GEMINI_API_KEY |
LLM provider credentials |
PUID / PGID |
Match container permissions to your host user |
OPENCODE_DOMAIN |
Domain for OpenCode |
CODE_SERVER_DOMAIN |
Domain for code-server |
CERT_RESOLVER |
Traefik certificate resolver |
SSH_KEY_PATH |
Host path to SSH keys; if authorized_keys exists there it is used for inbound SSH login |
GH_TOKEN / GITHUB_TOKEN |
Optional token for GitHub CLI and API access |
See .env.example for the full list.
./repos-> your repositories./config-> full~/.configpersistence including OpenCode config, installed skills, and mise config./share-> full~/.local/sharepersistence including OpenCode data, code-server data, and mise installs./agents-> compatibility config and skills for tools that use~/.agents
GitHub CLI auth also persists under ./config when you log in with gh auth login inside the container.
- This setup is intended for personal use, not multi-tenant hosting
- OpenCode is configured to deny reads for files such as
.env, SSH keys,*.pem, and*.key - SSH keys are mounted read-only and copied into the container at startup with the correct permissions
- If
authorized_keysexists in the mounted SSH directory, the container uses it for inbound SSH access on port22 - SSH password authentication is disabled and root login is disabled
- Playwright MCP runs as a separate internal service and is only exposed on the private Compose network by default
- If both
OPENCODE_SERVER_PASSWORDandCODE_SERVER_PASSWORDare empty, code-server can run without auth
- The stack includes a
playwright-mcpservice usingmcr.microsoft.com/playwright/mcp - OpenCode is preconfigured to connect to it through MCP at
http://playwright-mcp:8931/mcp - The container runs headless Chromium with
--no-sandbox - If you need a different image tag, set
PLAYWRIGHT_MCP_IMAGEin.env
If you do not use Traefik, remove the labels and networks sections from docker-compose.yml and access the mapped ports directly:
ports:
- "4096:4096"
- "8080:8080"4096 serves both the OpenCode API and the web UI directly from the opencode process.
- Permission errors on mounted folders: set
PUIDandPGIDto match your host user - SSH access not working: verify
SSH_KEY_PATH, confirm anauthorized_keysfile exists there, and make sure the files are readable by that user - code-server auth issue: set
OPENCODE_SERVER_PASSWORDeven if you leaveCODE_SERVER_PASSWORDempty - Browser actions failing unexpectedly: check
docker compose logs playwright-mcpand confirm the sidecar is healthy - Toolchains reinstalling or changing: check your mise config (
./config/mise/config.tomlon the host) and restart the container - GitHub CLI not authenticated: set
GH_TOKENorGITHUB_TOKEN, or rungh auth loginin the container - OpenCode page still tries to reach the internet: restart the container so mise upgrades OpenCode to the current release
The image contains only system packages and mise. OpenCode, GitHub CLI, and code-server are not baked in; mise installs them on container start and keeps them current:
- They are declared as
latestin the image's system config (system/mise.toml->/etc/mise/config.toml) - Tools in your own mise config (Node, Python, ...; see Pinning versions for where it lives) are upgraded within their declared version range (e.g.
node = "22"follows 22.x) - mise itself is self-updated (at most once per 24h)
- Downloads are cached in
./share, so restarts without new releases are fast - The first start needs network access and downloads ~300 MB; the container will not start if the initial install fails. Later starts continue with the cached versions if an upgrade fails
- New releases are picked up after mise's 24h release-age delay
- Set
AUTO_UPDATE=falseto skip upgrades and only install missing tools - Set
GITHUB_TOKENto avoid GitHub API rate limits when resolving releases
If you do not want the latest releases, pin them in your own mise config (your entry takes precedence over the image default of latest). It lives in the ./config volume:
- On the host:
./config/mise/config.toml, next to yourdocker-compose.yml(create it if it does not exist) - In the container:
/home/opencode/.config/mise/config.toml
Add the versions under [tools]:
[tools]
opencode = "1.18.33"
gh = "2.101.0"
"github:coder/code-server" = "4.139.1"Alternatively, run docker exec -u opencode opencode mise use -g opencode@1.18.33 on the host; it writes the same file. Run it as the opencode user, otherwise it ends up in root's config and has no effect.
Pinned exact versions are never upgraded. Loose versions like opencode = "1.18" follow patch releases only. To stop all upgrades, including mise itself and floating toolchains such as node = "22", set AUTO_UPDATE=false in your .env. Missing tools are still installed on start.
A plain docker compose restart opencode updates everything. You only need to pull a new image for changes to the image itself (system packages, entrypoint).