With OpenCode + vLLM (Qwen3.6 35B A3B) configs
Run OpenCode as a sandboxed, hardened non-root Docker container connected to a self-hosted vLLM inference server. No cloud API keys required.
- Docker + Docker Compose installed on your machine
- Access to a running vLLM server exposing an OpenAI-compatible API (e.g.
http://10.0.0.13:8000) - Your vLLM server must have the model loaded and
/v1/modelsresponding
Verify your vLLM is reachable before starting:
curl http://10.0.0.13:8000/v1/modelsYou should see your model ID in the response (e.g. qwen3.6-35b).
Use the exact "id" value from the response — e.g. qwen3.6-35b.
Finding your context size:
The max_model_len field in the /v1/models response is your context limit. Use that value for "context".
Create the following layout on your machine:
opencode-sandbox/
├── Dockerfile
├── compose.yml
├── start.sh
├── config/
│ ├── opencode.json
│ ├── AGENTS.md ← global sandbox rules (mounted read-only)
│ └── auth.json ← provider auth tokens (mounted read-only)
├── data/ ← opencode session state, persisted across runs
├── .opencode/ ← global sandbox commands and skills (mounted read-only)
└── workspace/ ← put your code projects here
# Get the code
git clone git@github.com:jammsen/docker-opencode-sandbox.git
# Build and launch
./start.sh
# Force a full rebuild (no layer cache) — useful when changing Dockerfile or feature toggles
./start.sh --no-cacheOn first launch OpenCode opens the TUI. Press / to open the command palette.
Inside the TUI:
- Press
/model— your model should appear under your provider name with an orange dot - Type
hello, what model are you?— the response should mention your model ID - Check the status bar at the bottom — it should show your configured model, for example
Qwen3.6 35B A3B · vLLM - Check the right panel —
$0.00 spentconfirms no cloud API is being used
Drop files into ./workspace/ on your host. They appear at /home/opencode/workspace/ inside the container. OpenCode treats this directory as HOME; the global config still lives under /home/opencode/.config/opencode/.
# Copy a project into the sandbox
cp -r ~/myproject ./workspace/myprojectUse scripts/reset-sandbox.sh only when you intentionally want to remove generated local state from ./workspace/ and ./data/. It preserves the .gitkeep placeholders and requires typing Yes, do as I say! before deleting anything.
| Mode | Shortcut | Token overhead | Best for |
|---|---|---|---|
| Build | default | ~10k tokens | Agentic file editing, multi-step tasks |
| Ask | tab |
~3-5k tokens | Questions, code review, explanations |
With a 32k context limit, Ask mode leaves significantly more room for your actual code and conversation.
The status bar shows X tokens (Y% used). Build mode consumes ~10,000 tokens just for the system prompt before you type anything. For large codebases, open only the files you need or use Ask mode.
Config not loading / provider picker appears on every launch
docker compose run --rm --entrypoint bash opencode -c \
"cat /home/opencode/.config/opencode/opencode.json"If this returns an error, check that docker compose is run from the same directory as docker-compose.yml and that ./config/opencode.json exists.
GID already exists error during build
Ubuntu 26.04 ships with a default user at UID/GID 1000. The Dockerfile handles this by renaming the existing user instead of creating a new one. Ensure you are using the Dockerfile exactly as provided above.
Model not responding / timeout
# Test vLLM connectivity from inside the container
docker compose run --rm --entrypoint bash opencode -c \
"curl -s http://YOUR_VLLM_IP:8000/v1/models"If this fails, your vLLM IP is unreachable from the container. Use the actual host IP — not localhost.
Tool calling loops or model halts mid-task
Some local models can struggle with long agentic tool-use loops. Mitigations:
- Prefer Ask mode for questions and code review that don't require file editing
- For Build mode, give explicit step-by-step instructions rather than open-ended goals
- Keep tasks scoped to one file or one function at a time
The container starts as root to handle setup (creating the user, fixing file ownership on mounted volumes), then permanently drops to an unprivileged user via gosu before your session begins. There is no way back to root after that point.
Restrictions in place:
no-new-privileges— once the container drops to the unprivileged user, no process inside the container can ever gain more permissions, even if it tries to run asudobinary or a binary with special file capabilities. The kernel enforces this hard, before any code in such a binary even runs.cap_drop: ALL— Linux capabilities are fine-grained units of root power (e.g. "change file ownership", "bind to privileged ports", "load kernel modules"). By default Docker grants containers a subset of these even without full root. Dropping all of them removes every one of those powers.cap_add: CHOWN, SETUID, SETGID, DAC_OVERRIDE— only the four capabilities the entrypoint actually needs for its setup phase are added back. Oncegosudrops to the non-root user, the kernel automatically clears the effective capability set on the UID transition, andno-new-privilegesblocks any path to reclaiming them.PUID/PGID— the in-container user is created at runtime with the same UID/GID as your host user. This ensures bind-mounted files in./workspaceand./datahave correct ownership on both sides of the mount.- Bridge networking only — isolated from the host network
- Writable filesystem access is limited to
./workspaceand./dataon the host. Config, commands, skills, and auth are mounted read-only.
The model runs entirely on your local vLLM server. No data leaves your network.
The image supports optional language runtimes controlled via build arguments. All toggles default to false.
| ARG | Default | Effect |
|---|---|---|
ENABLE_NODEJS |
false |
Installs Node.js and npm via apt |
ENABLE_PYTHON |
false |
Installs uv and the configured Python version |
ENABLE_RUST |
false |
Installs Rust via rustup |
PYTHON_VERSION |
3.13 |
Python version passed to uv python install |
Enable a toggle at build time Dockerfile rewrite or via command-line args:
docker compose build --build-arg ENABLE_PYTHON=true --build-arg PYTHON_VERSION=3.12
./start.sh --no-cache # rebuild with new togglesAll runtimes are installed at build time under the opencode user, so the container starts instantly with no language runtime downloads at startup. Base tooling includes ripgrep for OpenCode search tools and tzdata for correct Europe/Berlin timestamps. The tool binaries are on PATH and their data directories (CARGO_HOME, RUSTUP_HOME) are pinned via environment variables so they survive the HOME override that redirects opencode's session state to the mounted workspace.
Sandbox-wide commands and skills are mounted globally:
- ./config/AGENTS.md:/home/opencode/.config/opencode/AGENTS.md:ro
- ./.opencode/commands:/home/opencode/.config/opencode/commands:ro
- ./.opencode/skills:/home/opencode/.config/opencode/skills:roThis makes the commands available regardless of which project under ./workspace/ you open. Project-specific commands, skills, and AGENTS.md files can still live inside the project directory.
AGENTS.md is intentionally short: it gives global orientation. Repeatable process requirements live directly in the commands, because local models follow concrete command workflows more reliably than broad standing instructions.
Available sandbox commands:
/refactor-audit <target>— analyze refactor opportunities without editing files/refactor-apply <approved scope>— apply one focused approved refactor, verify it, and updateWORKLOG.md/git-commit— review, document, and commit approved changes using Conventional Commits
Skills are reusable on-demand capabilities for an agent. They use one directory per skill with a mandatory SKILL.md.
The included write-worklog skill provides a structured WORKLOG.md entry format for ad-hoc tasks. Command-driven workflows inline their own worklog format so they do not depend on automatic skill selection.