mirror of
https://github.com/tiennm99/composes.git
synced 2026-10-13 16:14:43 +00:00
feat: add openhands and opencode-web services
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.
This commit is contained in:
1 parent
3574c55e0a
commit
99f4f8b820
7 files changed
+262
No files matched your search
@@ -60,6 +60,8 @@ Each links to its own README for variables, ports, and storage.
|
||||
| [gitea-mirror-local](gitea-mirror-local/README.md) | Gitea + PostgreSQL + gitea-mirror, mirroring GitHub repos |
|
||||
| [netdata](netdata/README.md) | Netdata monitoring agent |
|
||||
| [ollama](ollama/README.md) | Ollama LLM server |
|
||||
| [opencode-web](opencode-web/README.md) | opencode coding agent, served as a browser UI |
|
||||
| [openhands](openhands/README.md) | OpenHands coding agent, running each session in a container it spawns |
|
||||
| [paseo](paseo/README.md) | Paseo coding-agent daemon and web UI |
|
||||
| [traffmonetizer](traffmonetizer/README.md) | TraffMonetizer bandwidth-sharing client |
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# Copy to .env and fill in. Never commit .env.
|
||||
#
|
||||
# cp .env.example .env
|
||||
|
||||
# Web UI and API login password. MUST NOT be blank -- opencode serves the web
|
||||
# interface, and with it a shell on this container, to anyone when
|
||||
# OPENCODE_SERVER_PASSWORD is unset.
|
||||
# Generate one with: openssl rand -base64 24
|
||||
OPENCODE_SERVER_PASSWORD=
|
||||
|
||||
# Username to pair with it. opencode defaults to `opencode` when unset.
|
||||
OPENCODE_SERVER_USERNAME=opencode
|
||||
|
||||
# Git identity for the commits the agent makes, as author and committer. git
|
||||
# reads these directly, so no `git config` step is needed.
|
||||
GIT_NAME=
|
||||
GIT_EMAIL=
|
||||
@@ -0,0 +1,10 @@
|
||||
FROM ghcr.io/anomalyco/opencode:latest
|
||||
|
||||
# Shell, git, HTTP and SSH clients for the commands the agent runs.
|
||||
RUN apk add --no-cache \
|
||||
bash \
|
||||
curl \
|
||||
git \
|
||||
openssh-client
|
||||
|
||||
WORKDIR /workspace
|
||||
@@ -0,0 +1,108 @@
|
||||
# opencode-web
|
||||
|
||||
[opencode](https://opencode.ai) 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
|
||||
|
||||
1. Set the variables from `.env.example` in Coolify or Dokploy.
|
||||
2. Point the domain at port `4096` and deploy.
|
||||
3. Log in to a model provider — the UI cannot do it, so use a shell:
|
||||
|
||||
```sh
|
||||
docker compose exec opencode-web 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.
|
||||
|
||||
4. 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.
|
||||
|
||||
`--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](#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](../README.md) 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
|
||||
|
||||
- [openhands](../openhands/README.md) — a coding agent that runs each session in a container it spawns
|
||||
- [paseo](../paseo/README.md) — runs the opencode CLI, among others, in a terminal
|
||||
@@ -0,0 +1,23 @@
|
||||
services:
|
||||
opencode-web:
|
||||
build: .
|
||||
command:
|
||||
- web
|
||||
- '--hostname'
|
||||
- 0.0.0.0
|
||||
- '--port'
|
||||
- '4096'
|
||||
environment:
|
||||
- OPENCODE_SERVER_PASSWORD=${OPENCODE_SERVER_PASSWORD}
|
||||
- OPENCODE_SERVER_USERNAME=${OPENCODE_SERVER_USERNAME}
|
||||
- TZ=Asia/Ho_Chi_Minh
|
||||
- GIT_AUTHOR_NAME=${GIT_NAME}
|
||||
- GIT_AUTHOR_EMAIL=${GIT_EMAIL}
|
||||
- GIT_COMMITTER_NAME=${GIT_NAME}
|
||||
- GIT_COMMITTER_EMAIL=${GIT_EMAIL}
|
||||
volumes:
|
||||
- 'opencode-home:/root'
|
||||
- 'opencode-workspace:/workspace'
|
||||
volumes:
|
||||
opencode-home:
|
||||
opencode-workspace:
|
||||
@@ -0,0 +1,87 @@
|
||||
# openhands
|
||||
|
||||
[OpenHands](https://openhands.dev) — 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
|
||||
|
||||
1. Point the domain at port `3000` and deploy. There is nothing to fill in —
|
||||
the service has no environment file.
|
||||
2. 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](../code-server/README.md) 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](../README.md) 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](../opencode-web/README.md) — a coding agent that runs in its own container instead of spawning one
|
||||
- [paseo](../paseo/README.md) — runs agent CLIs, including OpenHands' competitors, in a terminal
|
||||
@@ -0,0 +1,15 @@
|
||||
services:
|
||||
openhands:
|
||||
image: 'ghcr.io/openhands/openhands:latest'
|
||||
environment:
|
||||
- AGENT_SERVER_IMAGE_REPOSITORY=ghcr.io/openhands/agent-server
|
||||
- AGENT_SERVER_IMAGE_TAG=1.26.0-python
|
||||
- TZ=Asia/Ho_Chi_Minh
|
||||
- LOG_ALL_EVENTS=true
|
||||
extra_hosts:
|
||||
- 'host.docker.internal:host-gateway'
|
||||
volumes:
|
||||
- 'openhands-state:/.openhands'
|
||||
- '/var/run/docker.sock:/var/run/docker.sock'
|
||||
volumes:
|
||||
openhands-state:
|
||||
Reference in new issue
Block a user