A reproducible dev container for running multiple AI coding agent harnesses against local LLMs served by Docker Model Runner. The container ships the harnesses; the host (via Docker Desktop) ships the models. One model serves every harness.
Currently bundled harnesses:
- GitHub Copilot CLI — wired to DMR's OpenAI-compatible endpoint.
- Claude Code — wired to DMR's Anthropic-compatible endpoint.
Both talk to the same local model (default: ai/gpt-oss) over HTTP. No API
keys, no usage costs, no data leaves the machine for inference.
Each harness has different ergonomics, tools, and prompting style, but they all want a model behind an HTTP API. DMR exposes the same underlying model through three different API shapes simultaneously:
Docker Model Runner (ai/gpt-oss)
├── /engines/v1 OpenAI-compatible ◄── GitHub Copilot CLI
├── /anthropic/v1 Anthropic-compatible ◄── Claude Code
└── /api Ollama-compatible ◄── (future harnesses)
So you can compare harnesses head-to-head without paying for cloud inference, without rate limits, and without leaking your repo to a third party.
Worth calling out because it's a real differentiator:
Claude Code's WebSearch and WebFetch tools live in the harness, not in
the model. Pointing Claude Code at a local model via ANTHROPIC_BASE_URL
does not strip those tools — the local model can still ask the harness to
search the web or fetch a URL, and the harness performs the call client-side
and feeds results back into the conversation.
By contrast, GitHub Copilot CLI in COPILOT_OFFLINE=true mode is fully
network-isolated and has no equivalent web tool. So if you want to keep
inference local and still let the agent browse external docs, Claude Code
is the harness for that workflow.
-
Docker Desktop with Docker Model Runner enabled (Settings → Beta features → "Enable Docker Model Runner").
-
VS Code with the Dev Containers extension.
-
A model pulled on the host. The default is
ai/gpt-oss:# On the host (not inside the container): docker model pull ai/gpt-ossOr pull it from the Docker Desktop GUI under Models.
-
Clone this repo.
-
Open it in VS Code.
-
Command Palette → Dev Containers: Reopen in Container.
-
Wait for the build + post-create script to finish. You should see two green probes (
/engines/v1/modelsand the Anthropic endpoint) and a confirmation thatai/gpt-ossis loaded. -
Run a harness:
claude # Claude Code, talking to ai/gpt-oss via DMR copilot # GitHub Copilot CLI, talking to ai/gpt-oss via DMR
That's it. No /login, no cloud accounts, no API keys.
Copilot CLI is configured with COPILOT_OFFLINE=true, which fully isolates
it from the network. Even though the model is local, Copilot CLI still wants
to verify a GitHub Copilot entitlement on first launch. Do this once with
offline mode temporarily disabled:
COPILOT_OFFLINE=false copilot
# inside the TUI: /login (complete the device flow once)
# exit, then run plain `copilot` for all future sessionsClaude Code needs no equivalent step — ANTHROPIC_BASE_URL +
ANTHROPIC_AUTH_TOKEN skip the Anthropic login flow entirely.
Inside the container, both harnesses point at the same DMR instance running
on the host. The magic hostname model-runner.docker.internal is auto-injected
by Docker Desktop into every container when DMR is enabled — that's the only
URL that works from inside the container. (The host-side
http://localhost:12434 does not work from in here, because localhost
inside a container means the container itself.)
Copilot CLI (.devcontainer/devcontainer.json):
Claude Code (.devcontainer/devcontainer.json):
"ANTHROPIC_BASE_URL": "http://model-runner.docker.internal/anthropic",
"ANTHROPIC_AUTH_TOKEN": "not-needed",
"ANTHROPIC_MODEL": "ai/gpt-oss",
"ANTHROPIC_SMALL_FAST_MODEL": "ai/gpt-oss"Both harnesses are installed via official devcontainer features, so the container itself stays minimal — just a Node 22 base image plus the two features and a small post-create probe script.
Pull a different model on the host:
docker model pull ai/qwen2.5Then either edit .devcontainer/devcontainer.json to change COPILOT_MODEL
and ANTHROPIC_MODEL, or override per-invocation:
copilot --model ai/qwen2.5
claude --model ai/qwen2.5Model management is intentionally a host-side concern. The standalone
docker model CLI plugin would conflict with Docker Desktop's DMR if run
inside the container, so the container talks to DMR purely over HTTP.
If DMR is unavailable on your machine — or you just want to try a different
GGUF without rebuilding — there's an opt-in second backend that runs
llama.cpp's llama-server
inside the devcontainer. It serves the OpenAI Chat Completions API and
the Anthropic Messages API
on the same port, so a single process replaces DMR's dual-shim role with no
proxy layer.
DMR is still the default and recommended path. llama.cpp is the escape hatch.
From inside the running container:
source scripts/use-llama-cpp.shsource is required (not just running it) so the env-var overrides land in
your current shell. The script:
- Detects the architecture and downloads a prebuilt
llama-serverrelease on first run (cached under.llama-cache/). - Downloads the chosen GGUF on first run via
llama-server -hf(also cached under.llama-cache/). - Starts the server in the background on
127.0.0.1:8080and waits for/health. - Exports
COPILOT_PROVIDER_BASE_URL/ANTHROPIC_BASE_URLoverrides pointing at the local server.
After it finishes, claude and copilot in this shell talk to
llama-server. Open a new terminal (or unset the overrides) to go back to DMR.
Override the model with LLAMA_MODEL before sourcing:
LLAMA_MODEL=unsloth/gemma-4-E2B-it-GGUF \
source scripts/use-llama-cpp.sh.llama-cache/ on the host is bind-mounted into the container at
/home/node/.llama-cache and is in .gitignore. The llama-server binary
and downloaded GGUFs live there, so subsequent container rebuilds reuse the
cache instead of re-downloading multi-GB blobs.
This server runs CPU-only. Docker Desktop does not pass GPU/Metal through to Linux containers on macOS, so throughput will be much lower than DMR (which runs natively on the host with Metal/CUDA acceleration). Use this backend for portability and experimentation, not for raw speed.
.devcontainer/
└── devcontainer.json # base image, features, env vars, llama-cache mount
scripts/
├── post-create.sh # probes DMR + prints launch hints (runs on container create)
└── use-llama-cpp.sh # opt-in: switch to local llama.cpp backend
No Dockerfile, no docker-compose — the entire harness setup is one
devcontainer.json plus a verification script and an optional activation
script.
The pattern is: pick a harness, find the env vars it uses to point at a
custom endpoint, add them to containerEnv, and (if available) add an
official devcontainer feature for the install. Most agent CLIs accept either
an OpenAI-compatible or Anthropic-compatible base URL, so DMR can serve them
without any extra translation layer.