6.5

DMR, Compose models, and the docker/mcp-gateway service

A local model is an unauthenticated OpenAI-compatible endpoint on port 12434, Compose binds it to a service as two variables, and four things named gateway stay distinct.

Every model call in Part 5 and in this part ran with no API key and no cloud account. The model was ai/qwen3:4b on Docker Model Runner, on the same Mac as the agent.

When you finish this section, you can reach the runner from the host, a container, and a sandbox, and give it to a Compose service.

Docker Model Runner on port 12434

Docker Model Runner (DMR): the Docker component that pulls models from Docker Hub, an OCI registry, or Hugging Face. It serves them over OpenAI, Anthropic, and Ollama compatible APIs. Since Desktop 4.71.0, Docker Model Runner is off by default, and you turn it on in Settings. docker desktop enable model-runner --tcp <port> turns on host TCP, and the capture used port 12434.

the runner as docker model status --json reports itjsonFull log
{
  "running": true,
  "backends": {
…
    "llama.cpp": "Running: llama.cpp b9879-metal (sha256:<digest>) 72874f5",
…
  },
  "kind": "Docker Desktop",
  "endpoint": "http://model-runner.docker.internal/v1/",
  "endpointHost": "http://localhost/exp/vDD4.40/v1/"
}
The other backends are cut.

The Model Runner API has no authentication. Any client that reaches the port can pull, load, and run models. The base URL depends on where the caller runs, and figure 6.5 maps the five that this manual met:

CallerBase URLEvidence
a process on the hosthttp://localhost:12434capture
a container on Docker Desktophttp://model-runner.docker.internalcapture
a container on Docker Enginehttp://172.17.0.1:12434, not captured
a process on the host, through the Docker sockethttp://localhost/exp/vDD4.40capture, endpointHost
docker-agent inside a sandbox, through its proxyhttp://host.docker.internal:12434capture

After the base URL come path prefixes and whole endpoints. The OpenAI paths sit under /engines/v1/, and /engines/llama.cpp/v1/ names the engine. The Anthropic table lists /anthropic/v1/messages, while its examples call /v1/messages (conflict C79). No capture tested either path. Ollama clients use /api/. The capture also met the /v1/ root, which status reports as endpoint.

Fig. 6.5five base URLs for one Docker Model Runnerstructure
Five base URLs for one Docker Model Runner Left: five callers of Docker Model Runner. docker-agent on the host uses http://localhost:12434 once host TCP is enabled. The Compose service printer gets http://model-runner.docker.internal, injected as LLM_URL with the /v1/ path. A container on Docker Engine uses http://172.17.0.1:12434, from the docs and not captured. A host process that talks to the Docker socket reaches http://localhost/exp/vDD4.40, the endpointHost of docker model status. docker-agent inside a sandbox uses http://host.docker.internal:12434 through the sandbox proxy. Right: the runner with llama.cpp b9879-metal and ai/qwen3:4b, and what follows the base URL: the path prefixes /engines/v1/ for OpenAI, /engines/llama.cpp/v1/ with the engine name, and /v1/, the whole endpoints /anthropic/v1/messages and /v1/messages for Anthropic, and the prefix /api/ for Ollama. No path checks credentials. Docker Model Runner llama.cpp b9879-metal model ai/qwen3:4b paths after the base URL: /engines/v1/ OpenAI /engines/llama.cpp/v1/ /v1/ status endpoint /anthropic/v1/messages /v1/messages, examples /api/ Ollama docker-agent host process http://localhost:12434 TCP enabled on port 12434 printer Compose service http://model-runner.docker.internal injected as LLM_URL with /v1/ container Docker Engine http://172.17.0.1:12434 from the docs, not captured socket client host, Docker socket http://localhost/exp/vDD4.40 endpointHost in model status docker-agent inside a sandbox http://host.docker.internal:12434 through the sandbox proxy no path checks credentials: any client that reaches the port can run models
Five callers reach the same runner through five base URLs, and every path after them is open to any client that reaches the port. Read each row left to right, from the caller to the runner. The dashed caller comes from the docs and was not captured. The paths on the right follow any base URL.

The 8B tag ai/qwen3:latest failed to pull on the recording Mac, as docker-agent run, new, and doctor explains (the capture). With only the 4B tag pulled, doctor resolves auto to dmr/docker.io/ai/qwen3:4b (the capture). By default, Model Runner unloads an idle model after a few minutes, and provider_opts.keep_alive changes that time. provider_opts.context_size sets the context window through the runner's _configure endpoint. No capture ran docker model configure, so conflict C77 stays open.

An agent file can also name the runner in a providers: block. fixtures/agents/dmr.yaml sets base_url: http://localhost:12434/engines/llama.cpp/v1, and its run answered Hello (the capture). An explicit base_url bypasses the --record proxy, so that run is live on every capture (the capture).

Compose models

the Compose file of the captureyamlFull log
name: m101

models:
  llm:
    model: ai/qwen3:4b

services:
  mcp-gateway:
    image: docker/mcp-gateway
    use_api_socket: true
    command: ["--transport=streaming", "--port=8811"]

  printer:
    image: alpine:3.22
    depends_on: [mcp-gateway]
    models:
      llm:
        endpoint_var: LLM_URL
        model_var: LLM_MODEL
…
The command of printer is cut.

models: the top-level Compose element that declares a model, which a service binds by name. The short syntax derives LLM_URL and LLM_MODEL from the name llm, and endpoint_var and model_var choose the names. Compose v2.38 or later is required (our source record). docker compose up pulled and configured the model before it started a container:

the model step and what printer sawtextFull log
 llm Pulling
 llm Pulled
 llm Configuring
 llm Configured
…
printer-1 | LLM_MODEL=ai/qwen3:4b
printer-1 | LLM_URL=http://model-runner.docker.internal/v1/
printer-1 | {"object":"list","data":[{"id":"docker.io/ai/qwen3:4b","object":"model","created":0,"owned_by":"docker","dmr":{}}]}
printer-1 | wget: server returned error: HTTP/1.1 401 Unauthorized
The container steps, the gateway log, and the exit lines are cut.

Compose injected the /v1/ root, where the plan, read from the model-runner source, expected /engines/v1/. GET ${LLM_URL}models answered from inside the container all the same. Compose cannot start sandboxes (conflict C96), and no capture ran models: on the private engine inside a sandbox (conflict C95).

The docker/mcp-gateway service

The mcp-gateway service runs the open source MCP gateway with the host's Docker API socket, so that it can start MCP servers as containers. In the capture it found no profile and no server, listed 0 tools, and added its own management tools such as mcp-find and mcp-add. It printed Gateway URL: http://localhost:8811/mcp and a bearer token, and the request of printer without that token got 401 (the capture). Figure 6.6 draws the stack.

Fig. 6.6the capture compose.yaml with a model and a gatewayflow
The capture compose.yaml with a model and a gateway The top-level models element declares llm with model ai/qwen3:4b, and Compose asks Docker Model Runner to pull and configure it. Inside the Compose project m101, the service printer on alpine:3.22 binds llm with endpoint_var LLM_URL and model_var LLM_MODEL and receives LLM_MODEL=ai/qwen3:4b and LLM_URL=http://model-runner.docker.internal/v1/. Its GET of LLM_URL followed by models answers 200 with the model list. The service mcp-gateway runs docker/mcp-gateway with --transport=streaming --port=8811 and use_api_socket true, starts no server, prints a bearer token, and answers the printer request without that token with 401 Unauthorized. models: llm model: ai/qwen3:4b Docker Model Runner model-runner.docker.internal pull, configure COMPOSE PROJECT M101 printer, alpine:3.22 LLM_MODEL=ai/qwen3:4b LLM_URL=http://model-runner.docker.internal/v1/ endpoint_var, model_var GET LLM_URL models: 200 mcp-gateway, docker/mcp-gateway --transport=streaming --port=8811 use_api_socket: true, no server prints a bearer token, 0 tools 401 Unauthorized Docker Engine API the host socket no server started API socket
Compose pulls the model, injects two variables into printer, and runs the gateway with the API socket, which refuses a request without its bearer token. Read from the models element at the top left. Solid ink arrows are calls and the dashed rose arrow is the refused request.

Four things named gateway

Name in this manualWhat it isAddress in the captures
models gatewaythe address set by --models-gateway or DOCKER_AGENT_MODELS_GATEWAY, which sends all provider traffic through one models gateway URLhttp://localhost:12434/engines, which --record with dmr needs (the capture)
sbx MCP gatewayone host-side gateway per sandbox, set up by sbx mcp, separate from the MCP Toolkithttp://mcp-gateway.docker.internal/mcp inside the VM
MCP gateway servicedocker mcp gateway run or the docker/mcp-gateway image, mcp v0.44.1 on the recording hosthttp://localhost:8811/mcp inside the service
hosted MCP gatewaythe gateway of Docker AI Governance, "an invite-only feature"not captured

The Toolkit gateway runs each MCP server in its own container. Each MCP tool container has a limit of 2 GB, and each one gets 1 CPU. A docker-agent toolset with ref: docker:<name> takes that route, and runs the MCP server as a Docker container behind the MCP Gateway. The sbx MCP gateway is sbx mcp add, sbx mcp load, and --static-mcp, and the MCP gateways lesson covers the pattern.

Docker Sandboxes (sbx) and Docker Agent (docker-agent) sbx 0.47.0, docker-agent 1.149.0 · bf4169c · 2026-10-07 · Edition 2026.10