openhands runs each agent session in a container it spawns through the host docker socket, so the socket is mounted read-write and host.docker.internal is resolved. No host workspace is exposed. opencode-web serves the opencode agent as a browser UI. The vendor image ships only the opencode binary on bare Alpine, so a local Dockerfile adds bash, git, curl and an ssh client.
3.8 KiB
openhands
OpenHands — a coding agent with a web UI. The app container is only the front half: every agent session runs in a separate agent-server container that this one creates on the host through the Docker socket. Nothing is built here; the upstream image is complete.
Setup
- Point the domain at port
3000and deploy. There is nothing to fill in — the service has no environment file. - Open the domain and set the LLM provider and API key in the UI. They are written to the state volume and survive a redeploy.
Docker socket
Mounted read-write, and it has to be: the app creates and destroys containers,
and connecting to a Unix socket needs write permission on it. The :ro mount
that code-server uses would fail here.
That is full control of the host daemon, which is root-equivalent — the agent can start a container that mounts anything. It is the price of the Docker runtime; the alternative is running OpenHands somewhere it does not share a daemon with anything else.
host.docker.internal is required for the same architecture. Agent-server
containers are siblings started by the host daemon, not members of this compose
project's network, and they publish their port on the host. The app reaches
them by that name, so without the extra_hosts entry the UI loads and every
session then fails to connect.
Workspace
There is none on the host. SANDBOX_VOLUMES is deliberately unset, so the
agent sees only the storage inside its own sandbox container and cannot touch
a host path. Clone into the sandbox from the agent's terminal instead.
Setting it would mean a host path — the sibling container's mounts are resolved by the host daemon, so a named volume or a path inside the app container is not a valid value.
Environment
Nothing is per-deployment, so there is no .env.example. What
compose.yml sets:
| Variable | Purpose |
|---|---|
AGENT_SERVER_IMAGE_REPOSITORY |
Where to pull the sandbox image from |
AGENT_SERVER_IMAGE_TAG |
Which sandbox image to run. -python is the Python/Node toolchain variant; -golang, -java and others exist |
TZ |
Timestamps in the UI and logs |
LOG_ALL_EVENTS |
Full agent event stream in the container logs, which is what makes a failed session diagnosable |
The app image tracks latest while the agent-server tag is pinned, which looks
inconsistent and is not. GHCR publishes no floating tag for
openhands/agent-server at all — its tags are version-and-toolchain triples
like 1.26.0-python — so there is nothing to track. Bump it by hand when the
app image moves far enough that the two disagree; the app pulls it on first
session, so a wrong tag shows up as a session that never starts, not as a
failed deploy.
Storage
| Volume | Mount | Holds |
|---|---|---|
openhands-state |
/.openhands |
Settings, LLM credentials, conversation history |
/.openhands is at the filesystem root, not under $HOME — the upstream
docker run mounts the host's ~/.openhands there, and only the container
side matters to a named volume.
Sandbox containers keep their own storage and are not covered by this volume.
Networking
Listens on 3000, published nowhere — the platform maps the domain to it. See
the root README for why.
Sandbox containers do publish on the host, on ports the app picks, because that
is how the app reaches them. They are not reachable through the domain; serving
a preview running inside a sandbox would need AGENT_SERVER_USE_HOST_NETWORK
and SANDBOX_CONTAINER_URL_PATTERN, which this setup does not configure.
Related
- opencode-web — a coding agent that runs in its own container instead of spawning one
- paseo — runs agent CLIs, including OpenHands' competitors, in a terminal