A reproducible sandbox for testing the opencode-lms plugin end-to-end
against a real LM Studio server. Spin it up, point it at your LMS, and
you get a working OpenCode server with this plugin loaded — without
touching your host's OpenCode config.
Useful when:
- You're iterating on the plugin and want a clean OpenCode runtime to smoke-test against.
- You're evaluating the plugin and want to try it before installing it into your host environment.
- Something behaves differently against the real OpenCode loader than against the unit tests, and you need to reproduce.
- Ubuntu 24.04 base, non-root
coderuser (UID 1000). - OpenCode CLI installed from
https://opencode.ai/install. - The plugin source from the parent directory is copied in at build
time via BuildKit's
additional_contexts,npm ci && npm run buildruns inside the image, and the Dockerfile pre-populates OpenCode's plugin cache at~/.cache/opencode/packages/@hellogravel/opencode-lms@file:/home/coder/opencode-lms/so the runtime's "auto" installer finds it ready to import. The generated config references it as"plugin": ["@hellogravel/opencode-lms@file:/home/coder/opencode-lms"]— the<name>@file:<path>form is the npm spec OpenCode actually parses (a barefile:/...entry silently fails to install). - The container's
opencode.jsoncis regenerated on every start by the entrypoint from env vars. Persistent state (logs, memory) lives in theopencode-datanamed volume; the config is deliberately not persisted so you always know exactly what you're testing. - Networked to LM Studio via
host.docker.internal(the Docker host) by default. Override withLMS_BASE_URLto point at any reachable server. - The server requires
OPENCODE_SERVER_PASSWORDfor auth.
- Docker with BuildKit (default in modern Docker Desktop).
- LM Studio running somewhere the container can reach. From a Mac with
LMS on the same machine,
host.docker.internal:1234works. For LMS on another host on your LAN, setLMS_BASE_URL=http://<ip>:1234. ~/sandboxon the host (mounted as the container's/workspace— the scratch dir for any code edits the agent makes).- A populated
.env— see.env.example.
cd opencode-lms/docker
mkdir -p ~/sandbox
cp .env.example .env
echo "OPENCODE_SERVER_PASSWORD=$(openssl rand -hex 16)" >> .env
chmod 600 .env
# Edit .env: set LM_STUDIO_API_KEY (if your LMS has auth on)
# and LMS_BASE_URL (if your LMS is not on the Docker host).docker compose up --build -d # build + start (detached)
docker compose up --build # foreground; Ctrl-C stops cleanly
docker compose logs -f # follow logs — watch for plugin init
docker compose exec opencode bash # shell into the container
docker compose down # stop, keep volumes
docker compose down -v # stop and wipe volumes
docker compose build --no-cache # rebuild from scratchThe OpenCode server listens on http://localhost:4096. Use the
password from .env to authenticate.
On first request to an unloaded model:
[opencode-lms docker] wrote /home/coder/.config/opencode/opencode.jsonc with:
model=lmstudio/google/gemma-4-e4b
baseURL=http://host.docker.internal:1234
apiKey=(set, 35 chars)
...
[opencode-lms] LM Studio plugin initialized
[opencode-lms] Discovered 12 model(s) at http://host.docker.internal:1234
[opencode-lms] Auto-loading model google/gemma-4-e4b
[opencode-lms] Load started (google/gemma-4-e4b)
[opencode-lms] Loading google/gemma-4-e4b: 20%
[opencode-lms] Loading google/gemma-4-e4b: 100%
[opencode-lms] Model loaded in 4.3s
If you see a "health check failed" block instead, the URL list there tells you what the plugin actually tried — usually that's a network or auth issue with the LMS endpoint and not the plugin itself.
| Env var | Default | What it controls |
|---|---|---|
OPENCODE_SERVER_PASSWORD |
required | OpenCode server auth password |
LM_STUDIO_API_KEY |
(empty) | Bearer token sent to LM Studio; required if LM Studio has auth on |
LMS_BASE_URL |
http://host.docker.internal:1234 |
LM Studio base URL (no /v1 suffix). Override when LMS isn't on the Docker host. |
LMS_MODEL |
lmstudio/google/gemma-4-e4b |
Default OpenCode model |
LMS_AUTO_DOWNLOAD |
false |
If true, an unknown model triggers a download via /api/v1/models/download |
LMS_DISABLE_AUTO_LOAD |
false |
If true, the plugin won't auto-load missing models |
To switch models without rebuilding:
LMS_MODEL=lmstudio/qwen/qwen3.6-35b-a3b docker compose up -d- Self-contained build context. The
additional_contextsentry for the plugin source is..(the package root). No host-specific paths —git clone && docker compose up --buildworks from anywhere. - Plugin pre-staged in opencode's cache. A bare
file:plugin spec doesn't parse; the canonical form is<name>@file:<path>. We pre-populate~/.cache/opencode/packages/@hellogravel/opencode-lms@file:/.../with the right shape so OpenCode's auto-install finds it ready and skips the npm registry fetch — fast startup, works offline. - Config regenerated every start. Deterministic surface: whatever
is in
.envalways wins. Theopencode-datavolume still accumulates logs / memory / session state across restarts. - Config written under
options. OpenCode'sProviderConfigschema only carriesoptionsas an open bucket — top-level fields it doesn't recognize get silently stripped. The plugin reads fromoptions.*first with top-level fallback.
docker/
├── Dockerfile # Single-stage Ubuntu + node + opencode + plugin build
├── docker-compose.yml # additional_context resolves to '..' (the package root)
├── entrypoint.sh # Writes opencode.jsonc from env vars on every start
├── .dockerignore
├── .env # local-only, gitignored at the repo root
├── .env.example # template
└── README.md # this file
Volumes managed by compose:
opencode-config—/home/coder/.config/opencode(config is overwritten on start; anything else OpenCode drops here persists)opencode-data—/home/coder/.local/share/opencode(logs, memory, runtime state)