Files

4.5 KiB

opencode

opencode served as a browser UI by opencode web, which starts the headless server and its web interface together. The agent runs in this container — there is no sandbox layer between it and the filesystem.

Built from a local Dockerfile, because the official image is the opencode binary on bare Alpine and nothing else: no shell, no git, no curl, no ssh. An agent whose main tool is "run a command" has nothing to run, so one apk layer puts them back.

Setup

For OpenCode Go (or Zen), set OPENCODE_API_KEY to the key from the OpenCode Console and skip the login below. opencode picks the key up from the environment for both the opencode-go and opencode providers — models.dev, where opencode gets its provider list, names OPENCODE_API_KEY as their variable — so no auth.json entry or config file is needed.

Any other provider needs a login, the one step the UI cannot do. After the first deploy, from a shell:

docker compose exec opencode opencode auth login

It is interactive, which is why it is not an environment variable; exec gives it the TTY it needs. The credentials land on the opencode-home volume and survive a redeploy.

Then open the domain and sign in with OPENCODE_SERVER_USERNAME and OPENCODE_SERVER_PASSWORD.

The container logs a Bun stack trace ending in Executable not found in $PATH: "xdg-open" on every start. opencode web tries to open the UI in a local browser; there isn't one. It is noise — the server is already listening by then, and the container keeps running. Installing xdg-utils would silence it at the cost of pulling X11 in and tripling the image, which is not worth it for a log line.

Authentication

OPENCODE_SERVER_PASSWORD is the only thing between the domain and a shell on this container. opencode's own words: "If OPENCODE_SERVER_PASSWORD is not set, the server will be unsecured." Unset, every request is served — and every request can ask the agent to run a command. Treat a blank value as publishing a root terminal.

Environment

Variable Purpose
OPENCODE_SERVER_PASSWORD Web UI and API login. Blank means no authentication at all. Generate with openssl rand -base64 24.
OPENCODE_SERVER_USERNAME Username to go with it. opencode falls back to opencode.
OPENCODE_API_KEY OpenCode Go / Zen API key. Optional; blank means log in instead.
GIT_NAME / GIT_EMAIL Git author and committer identity for the agent's commits.

Other model provider credentials are not variables — see Setup.

Storage

Volume Mount Holds
opencode-home /root Provider credentials, config, session database, caches
opencode-workspace /workspace Code the agent works on

The whole home directory is one volume because opencode spreads its state over four places under it — .config/opencode, .local/share/opencode (the SQLite session database), .local/state/opencode and .cache/opencode — and mounting them separately would only be three more chances to miss one. Dotfiles and anything else installed into $HOME persist as a side effect.

The container runs as root, which is what the upstream image does; $HOME is /root because of it.

WORKDIR /workspace in the Dockerfile is what makes the agent start in the workspace volume.

Networking

Listens on 4096; point the domain at it.

--hostname 0.0.0.0 in command: is what makes the service reachable at all; both web and serve bind 127.0.0.1 by default. --port 4096 is there because the default is 0, a random port, which the platform cannot map a domain to.

If the UI is loaded from a different origin than it is served from, add --cors <url> to command:. The default setup does not need it.

Image

FROM ghcr.io/anomalyco/opencode:latest, the vendor image. The opencode repo moved out of the sst organisation; ghcr.io/sst/opencode still exists but no longer serves anonymous pulls, so it is not the one to use.

The apk layer adds bash, git, curl and openssh-client — the floor for an agent that clones, commits and fetches. It carries no language toolchain: none is wanted often enough to justify rebuilding the image for everybody, and apk add from the agent's own terminal covers a one-off — but /usr is not on a volume, so it is gone on the next deploy. Something needed every time belongs in the Dockerfile.

ENTRYPOINT stays the image's own opencode, so command: in compose.yml is just the subcommand and its flags.