Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,14 @@ ARG PNPM_VERSION=12.11.2
ARG TARGETARCH
ENV PNPM_HOME=/opt/pnpm
ENV PATH=${PNPM_HOME}/bin:${PATH}
COPY pnpm-workspace.yaml /opt/pnpm-config/pnpm/config.yaml
RUN case "$TARGETARCH" in amd64) arch=x64 ;; arm64) arch=arm64 ;; *) exit 1 ;; esac \
&& mkdir -p /opt/pnpm-cli "$PNPM_HOME" \
&& curl --fail --silent --show-error --location \
"https://registry.npmjs.org/@pnpm/exe.linux-${arch}/-/exe.linux-${arch}-${PNPM_VERSION}.tgz" \
| tar -xz --strip-components=1 -C /opt/pnpm-cli \
&& ln -s /opt/pnpm-cli/pnpm /usr/local/bin/pnpm \
&& pnpm --allow-build=@opencode/cli add --global \
&& XDG_CONFIG_HOME=/opt/pnpm-config pnpm --allow-build=@opencode/cli add --global \
"@opencode/cli@${OPENCODE_VERSION}" "typescript@${TYPESCRIPT_VERSION}" \
&& opencode --version

Expand Down
182 changes: 42 additions & 140 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,188 +1,90 @@
# OpenCode Server

Self-hosted [OpenCode V2](https://opencode.ai/v2/docs) web UI and API with Gemini, GitHub
Copilot, and WakaTime. Image: `ghcr.io/icco/opencode-server:main` (amd64/arm64).
Self-hosted [OpenCode V2](https://opencode.ai/v2/docs/) web UI and API. The image `ghcr.io/icco/opencode-server:main` supports amd64 and arm64.

## Quick start
## Run

From this checkout, with Docker Compose and OpenSSL installed:

```sh
mkdir -p "$HOME/.ssh"
export OPENCODE_PASSWORD="$(openssl rand -hex 32)"
docker compose up -d
```

For this local run, `OPENCODE_PASSWORD` is a random password of at least 32
characters. Open <http://localhost:4096> and log in as
`opencode`. Keep repositories under `/data/workspace`.

The same `OPENCODE_PASSWORD` authenticates commands run with `docker compose exec`.
The icco.me deployment loads it and integration tokens from Google Secret Manager
through its existing updater; no production `.env` file is used.

The `/data` volume preserves workspaces, sessions, credentials, and caches.
Back it up. Host bind mounts must be writable by UID/GID 1000.
Open <http://localhost:4096> and sign in as `opencode` with that password. Keep the password in your deployment's secret store and export it for later Compose commands; startup requires at least 32 characters.

## GitHub CLI and Git access
The `/data` volume preserves repositories, sessions, credentials, and caches. Keep repositories in `/data/workspace` and back up the volume. Bind mounts must be writable by UID/GID 1000. Compose also mounts the host's `~/.ssh` read-only; keys must be readable by UID 1000, and `known_hosts` must be maintained on the host.

Authenticate `gh` once for HTTPS Git access (separate from Copilot login):
To update the image or apply changed environment variables:

```sh
docker compose exec opencode gh auth login --hostname github.com --git-protocol https --web
docker compose exec opencode gh auth status
docker compose pull
docker compose up -d --force-recreate opencode
```

Credentials persist in `/data/.config/gh`. For automation, inject `GH_TOKEN` into
the container environment; it overrides stored logins.

SSH remotes use the host's `${HOME}/.ssh`, mounted read-only at `/data/.ssh`.
Create it before startup, make keys readable by UID 1000 with SSH-compatible
permissions, and add required hosts to `known_hosts` on the host.

## Connect providers
## Connect accounts

Connect both providers through the Web UI or the running server:
Connect providers in OpenCode or run:

```sh
docker compose exec opencode opencode auth login github-copilot --server http://127.0.0.1:4096
docker compose exec opencode opencode auth login google --server http://127.0.0.1:4096
```

- **Copilot:** choose GitHub Copilot and complete device login. Requires a subscription.
- **Gemini:** choose Google → **Manually enter API Key**. Organization-backed Code
Assist can use OAuth: open the login URL and paste the full localhost redirect
URL into the prompt. Set `OPENCODE_GEMINI_PROJECT_ID` in the container environment.
Consumer OAuth is discontinued; see the [Gemini plugin docs](https://github.com/jenslys/opencode-gemini-auth).
- **GitHub Copilot:** complete device login with a subscribed account.
- **Gemini:** choose the API-key method or set `GOOGLE_GENERATIVE_AI_API_KEY`. Organization-backed Code Assist OAuth also needs `OPENCODE_GEMINI_PROJECT_ID`; see the [Gemini plugin's account requirements](https://github.com/jenslys/opencode-gemini-auth).
- **Git access:** set `GH_TOKEN` or run `docker compose exec opencode gh auth login --hostname github.com --git-protocol https --web`. This is separate from Copilot login; stored credentials persist in `/data/.config/gh`.
- **WakaTime:** put your API key in `/data/.wakatime.cfg`, owned by `1000:1000` with mode `600`, using the [plugin's configuration format](https://github.com/angristan/opencode-wakatime#prerequisites).

For automated hosting, inject `GOOGLE_GENERATIVE_AI_API_KEY` into the container environment.
## MCP integrations

For [WakaTime](https://github.com/angristan/opencode-wakatime), create
`/data/.wakatime.cfg` owned by `1000:1000`, mode `600`:
Set the variables below before creating the container. For remote servers, open `/mcps`, select the server, and sign in when prompted; OAuth credentials persist in `/data`.

```ini
[settings]
api_key = <your key from https://wakatime.com/api-key>
```
| Integration | Setup |
| --- | --- |
| [Context7](https://mcp.context7.com/mcp) | Remote library documentation. |
| [Grafana](https://github.com/grafana/mcp-grafana) | `GRAFANA_URL` and `GRAFANA_SERVICE_ACCOUNT_TOKEN`; bundled tools are read-only. |
| [Karakeep](https://docs.karakeep.app/integrations/mcp) | `KARAKEEP_API_KEY` and `KARAKEEP_API_ADDR` (defaults to `https://hoard.natwelch.com`); includes write tools. |
| [Lunch Money](https://github.com/akutishevsky/lunchmoney-mcp) | `LUNCHMONEY_API_TOKEN` from [developer settings](https://my.lunchmoney.app/developers); required to start its MCP server. |
| [Notion](https://developers.notion.com/guides/mcp/overview) | Remote server at `https://mcp.notion.com/mcp`. |
| [Robinhood](https://robinhood.com/us/en/support/articles/onboarding-an-external-agent/) | Remote trading server at `https://agent.robinhood.com/mcp/trading`; complete account onboarding during sign-in. |

## MCP integrations

- **Lunch Money:** inject `LUNCHMONEY_API_TOKEN` using a token from
[developer settings](https://my.lunchmoney.app/developers). The pinned
[Lunch Money MCP](https://github.com/akutishevsky/lunchmoney-mcp) v3.0.0 provides
finance queries and updates through Lunch Money's v2 API. Recreate the container
after setting the token.
- **Grafana:** the image bundles [Grafana MCP](https://github.com/grafana/mcp-grafana)
v2.0.2 for read-only dashboards, alerts, annotations, Loki logs (LogQL), and
Prometheus metrics (PromQL). Set `GRAFANA_URL` and
`GRAFANA_SERVICE_ACCOUNT_TOKEN` in the container environment, then recreate it. Use a
Grafana service account with the Viewer role and access to the required
datasources. Include Grafana's subpath in the URL when applicable, and use an
endpoint that accepts service-account authentication without a browser-login
proxy. Loki and Prometheus use Grafana's existing datasource configuration.
- **Context7:** current library documentation via `https://mcp.context7.com/mcp`.
If OpenCode reports authentication is needed, open `/mcps`, select Context7,
and sign in. OAuth credentials persist in `/data`.
- **Karakeep:** the official [MCP](https://docs.karakeep.app/integrations/mcp) v0.33.1
uses `KARAKEEP_API_ADDR` (Compose defaults to `https://hoard.natwelch.com`) and
`KARAKEEP_API_KEY` from Karakeep's **Settings → API Keys**. Inject the key and
recreate the container. It supports search/read and **create, update, and delete**
operations. Verify with a small bookmark search; connection status alone does
not validate the key.
- **Notion:** Notion's hosted [MCP](https://developers.notion.com/guides/mcp/overview) at `https://mcp.notion.com/mcp`. Open `/mcps`, select Notion, and sign in once. OAuth credentials persist in `/data`.

All five integrations use V2 Code Mode. The default **Build** agent can use them. Without a Lunch Money token its server cannot start. Check connections with:
Check connections with:

```sh
docker compose exec opencode opencode api get /api/mcp --server http://127.0.0.1:4096
```

Grafana can advertise tools before credentials are configured; verify access by
listing datasources and running a small query, not just checking MCP connection
status. To turn an integration off, set `disabled: true` on its entry under
`mcp.servers` in your replacement config.

## Superpowers, worktrees, and quota
Grafana and Karakeep can connect before their credentials are validated; check a small read query too. Disable unused integrations with `disabled: true` in their `mcp.servers` entries; see [MCP configuration](https://opencode.ai/v2/docs/mcp-servers).

[`Superpowers`](https://github.com/obra/superpowers) v7.0.0 replaces Orchestra,
pinned to its full Git commit for reproducible installs. The default **Build**
agent uses native skills for planning, debugging, TDD, review, and verification.
Superpowers registers its skills and injects its bootstrap into top-level sessions;
native subagents can load skills without restarting the controller workflow.
## Agents and configuration

Skills in [`skills/`](skills) ship in the image at `/etc/opencode/skills` and update with it: `commit-push-pr` for the commit and PR workflow, and `go-repo` for Go repository scaffolding, lint, and release plumbing.
- **Build** is the default agent and can use all configured tools. **General** can implement and test; **Explore** is read-only. Both subagents may use Context7 but deny Grafana, Lunch Money, Karakeep, Notion, Robinhood, and further delegation. Tool permissions do not sandbox shell access.
- [Superpowers](https://github.com/obra/superpowers) supplies planning, debugging, testing, and review skills. The image also ships [`commit-push-pr` and `go-repo`](skills/). [`AGENTS.md`](AGENTS.md) requires repository edits in `/data/workspace/.worktrees/<repository>/<task>`.
- [OpenCode Quota](https://github.com/slkiser/opencode-quota) provides `/quota` and `/quota_status`.
- Defaults and pinned plugins live in [`opencode.jsonc`](opencode.jsonc). The image converts it to `/etc/opencode/opencode.json`; project `opencode.json(c)` files can override settings. See the [V2 configuration guide](https://opencode.ai/v2/docs/config).
- Each startup copies the image's [`AGENTS.md`](AGENTS.md) to `/data/.config/opencode/AGENTS.md`, replacing local edits. Change instructions in this repository and rebuild.

Only Build receives the blanket tool allow. General can implement and test, while Explore remains read-only; both native subagents are denied Grafana, Lunch Money, Karakeep, Notion, and further delegation. They may use Context7 through Code Mode, whose nested calls enforce each tool's permissions. These are tool boundaries, not OS isolation: General's shell still runs with the server user's authority.
To replace the image defaults, install dependencies, edit `opencode.jsonc`, run `node scripts/build-config.mjs runtime.json`, and mount the output read-only at `/etc/opencode/opencode.json`. Restart the container after replacing it.

Repository edits must run in Git worktrees under the shared workspace root:
`/data/workspace/.worktrees/<repository>/<task>`. Create the task branch before
editing and move the session there. OpenCode's native worktree API also uses
`/data/workspace/.worktrees` as its parent (and appends its requested/generated name).
The directory setting chooses placement; the global `AGENTS.md` supplies the
always-use-worktrees rule. It does not sandbox arbitrary shell commands.
## Hosting

[`OpenCode Quota`](https://github.com/slkiser/opencode-quota) provides `/quota` and
`/quota_status`; remaining balances do not influence routing. It replaces the
deprecated Cardinal fork; existing quota configuration remains compatible. Gemini Code Assist
quota requires [organization setup](https://github.com/slkiser/opencode-quota/blob/main/docs/readme/providers.md#gemini-cli).

Old persistent `orchestra.jsonc` files are left untouched for rollback, but the
new image does not load Orchestra or generate its agents. Existing sessions that
selected `orch-lead` should switch to **Build** after deploying the image.

### Additional plugin proposals

Research checked on 2026-10-11; these are **not installed**:

| Candidate | Recommendation | V2 and deployment considerations |
| --- | --- | --- |
| [opencode-dcp](https://github.com/arafays/opencode-dcp) | Best next trial for long Copilot sessions: model-directed compression and pruning. | Explicit V2 implementation. Pin a compatible version and test alongside Superpowers/native compaction; summaries can drop important context and alter prompt-cache behavior. |
| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | Useful for unattended tasks if you want a server-side webhook/notification command. | Explicit dual V1/V2 support. Web clients do not receive its desktop sounds/popups; remote alerts need an explicitly configured server command. Native terminal notifications may already suffice. |
| [opencode-security-guard](https://www.npmjs.com/package/opencode-security-guard) | Optional shell-approval layer, not a default addition. | Supports V2 `>=2.0.14 <3`, but sends eligible command text to OpenRouter/Jev and needs a credential. Requires a privacy/cost decision; classifications are not a sandbox. |

Keep Quota, WakaTime, and the current MCP integrations. Avoid adding another
orchestrator such as Oh My OpenCode alongside Superpowers. The popular
[`@tarquinen/opencode-dcp`](https://github.com/Opencode-DCP/opencode-dynamic-context-pruning)
is distinct from the V2-native `opencode-dcp` above; do not infer V2 compatibility
from its own package major version.

## Public hosting

Use HTTPS. With Caddy on the host:

```caddyfile
code.example.com {
reverse_proxy 127.0.0.1:4096 {
flush_interval -1
}
}
```
Compose binds only to `127.0.0.1:4096`. Put an HTTPS reverse proxy in front for remote access. A containerized proxy can share a network and use `opencode:4096` instead of the host port mapping.

For a container proxy, join its network, remove the Compose `ports` mapping,
and proxy to `opencode:4096`.
The container runs as UID 1000 with dropped capabilities and resource limits. Login grants code execution and access to stored credentials; this is a trusted-user service, not a multi-tenant sandbox.

The container runs non-root with dropped capabilities and resource limits.
Keep these controls. **Login grants code execution and
access to stored credentials.** Shared networks permit service-to-service access;
outbound host/LAN access is unrestricted. This is not a multi-tenant sandbox.
## Development

## Configuration and development
The image includes Go, TypeScript, pnpm, go-task, golangci-lint, and shell tools. See [`Dockerfile`](Dockerfile) for pinned versions. Additional Go tools and pnpm global installs persist under `/data/go` and `/data/.local/share/pnpm`.

- Includes Go, TypeScript, pnpm, go-task, golangci-lint, and common shell tools; see [`Dockerfile`](Dockerfile) for versions. Go tools and pnpm global installs persist under `/data/go` and `/data/.local/share/pnpm`.
- Defaults live in [`opencode.jsonc`](opencode.jsonc), loaded at
`/etc/opencode/opencode.json` after JSONC conversion. To replace this file,
edit the source config, run `node scripts/build-config.mjs runtime.json`, and
mount `runtime.json` there read-only. Project `opencode.json(c)` files can override settings normally.
- [`AGENTS.md`](AGENTS.md) is the global instruction file. Each startup copies it to `/data/.config/opencode/AGENTS.md` and overwrites local edits, so change it here instead. OpenCode V2 loads the global file before project instructions and detects ambient edits before the next model request. These are model instructions, not hard permission enforcement. V2 ignores the old `instructions` array and does not run language servers; use each project's lint, typecheck, and compiler commands.
- Restart OpenCode after config changes. After environment/password changes, run
`docker compose up -d --force-recreate opencode`.
- Versions are pinned in `Dockerfile` and `opencode.jsonc`. CI tests both
architectures and publishes `main` with provenance attestations.
With the Node.js and pnpm versions pinned in `Dockerfile` and `package.json`:

```sh
pnpm install --frozen-lockfile --ignore-scripts --no-optional
pnpm test
node scripts/test-router-integration.mjs
docker build -t opencode-server .
bash scripts/smoke-test.sh opencode-server
gh attestation verify oci://ghcr.io/icco/opencode-server:main --owner icco
```

The integration check needs the OpenCode version pinned in `Dockerfile`; it uses isolated data on port 4197 (`ROUTER_TEST_PORT` overrides) and requests no inference. It checks every plugin, Superpowers and image skill registration, Build/MCP permissions, worktree settings, Lunch Money/Karakeep stdio handshakes, and quota commands. Image smoke tests also check Grafana and need Node.js 26, Docker, Bash, curl, jq, OpenSSL, and `timeout`. CI runs them on amd64 and arm64.
Smoke tests require Docker, Bash, Node.js, curl, jq, OpenSSL, and `timeout`. They exercise the real server, authentication, plugins, permissions, MCP connections, and quota commands without model inference. CI runs both architectures before publishing `main` with provenance attestations.
2 changes: 2 additions & 0 deletions opencode.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
{ "action": "lunchmoney_*", "resource": "*", "effect": "deny" },
{ "action": "karakeep_*", "resource": "*", "effect": "deny" },
{ "action": "notion_*", "resource": "*", "effect": "deny" },
{ "action": "robinhood_*", "resource": "*", "effect": "deny" },
{ "action": "subagent", "resource": "*", "effect": "deny" },
{ "action": "skill", "resource": "*", "effect": "allow" },
{ "action": "execute", "resource": "*", "effect": "allow" },
Expand All @@ -35,6 +36,7 @@
{ "action": "lunchmoney_*", "resource": "*", "effect": "deny" },
{ "action": "karakeep_*", "resource": "*", "effect": "deny" },
{ "action": "notion_*", "resource": "*", "effect": "deny" },
{ "action": "robinhood_*", "resource": "*", "effect": "deny" },
{ "action": "subagent", "resource": "*", "effect": "deny" },
{ "action": "skill", "resource": "*", "effect": "allow" },
{ "action": "execute", "resource": "*", "effect": "allow" },
Expand Down
13 changes: 13 additions & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
minimumReleaseAgeStrict: true
minimumReleaseAgeExclude:
- '@opencode/ai@2.0.27'
- '@opencode/cli-darwin-arm64@2.0.27'
- '@opencode/cli-darwin-x64-baseline@2.0.27'
- '@opencode/cli-darwin-x64@2.0.27'
- '@opencode/cli-linux-arm64-musl@2.0.27'
- '@opencode/cli-linux-arm64@2.0.27'
- '@opencode/cli-linux-x64-baseline-musl@2.0.27'
- '@opencode/cli-linux-x64-baseline@2.0.27'
- '@opencode/cli-linux-x64-musl@2.0.27'
- '@opencode/cli-linux-x64@2.0.27'
- '@opencode/cli-windows-arm64@2.0.27'
- '@opencode/cli-windows-x64-baseline@2.0.27'
- '@opencode/cli-windows-x64@2.0.27'
- '@opencode/cli@2.0.27'
- '@opencode/client@2.0.27'
- '@opencode/plugin@2.0.27'
- '@opencode/protocol@2.0.27'
Expand Down
Loading
Loading