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
-
Set the variables from
.env.examplein Coolify or Dokploy. -
Point the domain at port
4096and deploy. -
Log in to a model provider — the UI cannot do it, so use a shell:
docker compose exec opencode opencode auth loginIt is interactive, which is why it is not an environment variable;
execgives it the TTY it needs. The credentials land on theopencode-homevolume and survive a redeploy. -
Open the domain and sign in with
OPENCODE_SERVER_USERNAMEandOPENCODE_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.
--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.
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. |
GIT_NAME / GIT_EMAIL |
Git author and committer identity for the agent's commits. |
TZ is set in compose.yml rather than here: it is a property of this setup,
not of whoever deploys it, and Compose interpolation would let a TZ exported
by the deploying shell win over the .env file anyway.
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.
Anything written outside those two volumes — an apk add from the agent's own
terminal — is lost on the next deploy. Add it to the Dockerfile instead.
Networking
Listens on 4096, published nowhere — the platform maps the domain to it. See
the root README for why.
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 a terminal covers a one-off. Something needed on every deploy
belongs in the Dockerfile, since /usr is not on a volume.
ENTRYPOINT stays the image's own opencode, so command: in compose.yml
is just the subcommand and its flags.
Related
- paseo — runs the opencode CLI, among others, in a terminal