·

Docker MCP With Gemini CLI

Set up Docker MCP in Gemini CLI so your AI agent can manage containers, images, and logs right from your editor.

Gemini CLI supports local stdio MCP servers through a settings.json file, the same shape used for most command-based MCP integrations. Since Docker MCP's auth model is OS socket permissions rather than an API token, the config here is close to the simplest one in this course — no header, no bearer token, just a command. This topic covers that setup, reading container health and logs conversationally, a full multi-stage build fix, and where Gemini CLI's behavior actually differs from Claude Code's on the same tool calls.


Installing and Connecting Docker MCP to Gemini CLI

Prerequisites
- Gemini CLI installed per the official install guide
- Docker Engine running, socket reachable by your user
- uv installed for uvx mcp-server-docker

Gemini CLI MCP configuration

Gemini CLI reads MCP server definitions from ~/.gemini/settings.json:

{
  "mcpServers": {
    "docker": {
      "command": "uvx",
      "args": ["mcp-server-docker"]
    }
  }
}

If you need a non-default Docker context, set it explicitly rather than assuming Gemini CLI's subprocess inherits your shell's exported variables:

{
  "mcpServers": {
    "docker": {
      "command": "uvx",
      "args": ["mcp-server-docker"],
      "env": {
        "DOCKER_HOST": "unix:///var/run/docker.sock"
      }
    }
  }
}

For a project-specific setup, place the same structure in .gemini/settings.json at the repo root instead — Gemini CLI merges project-level settings over the global ones.

Verify the connection

gemini
What Docker MCP tools are available? List all running containers.

You should see the image/container/logs/exec/compose tool families and a live container list back within the same turn.

Tips
- Restart Gemini CLI after editing settings.json — it's read at session start, not watched for changes.
- There's no secret to rotate here, unlike most other MCP servers in this course — the "credential" is whatever OS-level Docker access the running user already has, so review that access itself periodically instead.
- If gemini mcp reports the server as unreachable, run uvx mcp-server-docker directly in a terminal first — a broken uv install or a socket permission error surfaces more clearly outside Gemini CLI's wrapper.


Reading Container Logs and Health Status from Gemini CLI

Gemini CLI tends to produce well-organized tabular summaries when asked for status across multiple containers, which is a good fit for the "what's actually running and is it healthy" class of question you ask constantly during local development.

List all containers with their status, health check result, and uptime.
Format as a table.
| NAME          | STATUS   | HEALTH     | UPTIME     |
|---------------|----------|------------|------------|
| myapp-api     | running  | healthy    | 14 minutes |
| myapp-db      | running  | healthy    | 14 minutes |
| myapp-cache   | running  | starting   | 8 seconds  |
| myapp-worker  | exited   | n/a        | -          |

The exited row is the one worth immediately following up on:

The worker container exited. Fetch its logs and its exit code.
Exit code: 137
--- last 20 lines ---
<application startup logs, then nothing — process killed mid-run>

Exit code 137 (128 + signal 9, SIGKILL) with no application-level error message in the log is the classic signature of an out-of-memory kill, not an application crash. Confirm it directly rather than guessing:

Inspect the worker container's full state. Was OOMKilled true?
"State": {
  "Status": "exited",
  "ExitCode": 137,
  "OOMKilled": true,
  "FinishedAt": "2026-08-21T10:02:41Z"
}

With OOMKilled: true confirmed, the conversation moves to a memory-limit question rather than an application-logic one:

Does this container have a memory limit set? Read the compose file and
check the "mem_limit" or "deploy.resources.limits.memory" fields for
the worker service.
services:
  worker:
    build: ./worker
    mem_limit: 256m

If the worker service genuinely needs more than 256 MB under real load — common for anything doing batch processing or holding a large in-memory cache — the fix is raising the limit and re-verifying under the same load, not silently removing the limit entirely:

Raise worker's mem_limit to 512m in the compose file, bring the stack
up again, and monitor its memory usage with docker stats for one
minute to confirm it stays under the new limit.

Tips
- Exit code 137 without an application error in the logs should immediately point you at OOMKilled, not at application logic — checking inspect_container's State.OOMKilled field takes one call and rules out an entire class of wrong hypotheses.
- Ask for health status alongside uptime, not status alone — a container that's running but stuck on starting health for several minutes is a different problem (failing healthcheck) than one that's simply slow to boot.
- When raising a memory limit as a fix, always ask the agent to verify with docker stats under load afterward — a limit bump that "should" fix it is not the same as confirmed headroom.


Practical Example: Fixing a Broken Multi-Stage Build with Gemini CLI

Scenario: A Go service builds fine but the resulting container fails at runtime with a TLS error whenever it makes an outbound HTTPS call.

FROM golang:1.22 AS builder
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /out/server ./cmd/server

FROM gcr.io/distroless/static-debian12
COPY --from=builder /out/server /server
ENTRYPOINT ["/server"]

Step 1: Reproduce and capture the exact error.

Build this Dockerfile as svc:dev and run it. It calls an external HTTPS
API on startup. Show me the exact error output.
2026/08/21 10:41:02 http: TLS handshake error from 172.19.0.1:41022:
x509: certificate signed by unknown authority

Step 2: Reason about the distroless base image.

This is running on gcr.io/distroless/static-debian12. Does that image
include CA certificates? What's the difference between the "static"
and "base" distroless variants?

The correct answer here matters: distroless/static is deliberately minimal — no libc, no CA bundle, nothing beyond what a fully static binary needs. It's the right choice for size, but only if you explicitly bring your own CA certificates along, since the base image has none to offer.

Step 3: Fix by copying certs from the builder stage.

FROM golang:1.22 AS builder
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /out/server ./cmd/server

FROM gcr.io/distroless/static-debian12
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY --from=builder /out/server /server
ENTRYPOINT ["/server"]
Update the Dockerfile to copy /etc/ssl/certs/ca-certificates.crt from
the builder stage into the distroless final stage. Rebuild and confirm
the HTTPS call succeeds this time.

Step 4: Confirm and compare size impact.

Build the fixed image as svc:fixed. Compare its size against svc:dev
and against a version built on gcr.io/distroless/base-debian12 instead
(which includes certs and glibc by default).
svc:dev (static, broken)        18.2MB
svc:fixed (static + certs)      18.4MB
svc:alt (base-debian12)         24.7MB

The static + explicit cert copy comes out barely larger than the broken version and meaningfully smaller than switching to base-debian12, which is the better trade-off if the certificate is the only missing piece — switching base images entirely is the wrong fix when a one-line COPY solves it.

Tips
- distroless/static images have no shell, no package manager, and no CA bundle by design — treat "no shell to debug with" as expected, not a bug, and do investigation from the builder stage or a temporary debug image instead.
- x509: certificate signed by unknown authority immediately after switching to a distroless or scratch base is almost always a missing CA bundle, not an actual certificate problem — check the base image's contents before suspecting your TLS config.
- Compare size against the actual alternative base image, not an estimate — the gap between static and base distroless variants is real but easy to overstate from memory.


Comparing Docker MCP Behavior Between Gemini CLI and Claude Code

Both clients call the same underlying mcp-server-docker tools, so the raw data returned is identical — the differences are in how each client uses and presents that data.

Response formatting. Gemini CLI defaults to more consistently tabular output for multi-item results (container lists, image lists) without being asked, which suits status-check style prompts well. Claude Code tends toward narrative summaries by default and needs an explicit "format as a table" instruction to match that style — a small thing, but noticeable across a long troubleshooting session.

Tool-call chaining depth. In practice, Claude Code more readily chains three or four Docker tool calls in a single turn without an intermediate check-in (inspect → logs → exec → propose fix), while Gemini CLI more often pauses after one or two calls to summarize findings before continuing — which some developers prefer for auditability, and others find slows down a fast iterative debugging loop.

Context retention across a session. Both retain tool-call history within a session, but Gemini CLI's default context window management is more aggressive about summarizing older tool outputs, which is generally fine for Docker MCP since most payloads (container lists, inspect results) are naturally re-fetchable — you rarely need the exact original JSON minutes later, just the current state.

File-system correlation. Claude Code and its VS Code extension have an edge when the workflow needs tight correlation between a running container's state and local Dockerfile/compose source — its file-access model in an open workspace is more immediate. Gemini CLI handles this fine from the terminal but benefits from being told the exact file path rather than inferring it from an open editor, since there's no editor context to draw on.

Bottom line: for pure Docker MCP tool-calling correctness, both clients are equivalent — they're calling the same server. The differences are workflow ergonomics, not capability. Pick based on whether you want narrative or tabular defaults, and whether you're already living in one client for other MCP integrations in your stack.

Tips
- If you split Docker debugging work across both clients (e.g., Gemini CLI for quick status checks, Claude Code for deep fix sessions), keep a written note of the current hypothesis when switching — neither client shares session context with the other.
- Ask Gemini CLI explicitly to "propose the full fix now instead of checking in after each step" if its default pacing feels slower than you want for time-sensitive incident debugging.
- Don't assume tool output differs between clients if something looks unexpected — check the raw mcp-server-docker version and Docker daemon version first, since that's the actual source of truth both clients are querying.


Tips

Tips
- Docker MCP's Gemini CLI config has no secret to protect, which makes it one of the safer integrations to commit — but that also means access control lives entirely at the OS/socket layer, so don't treat the simple config as "nothing to secure."
- Use exit codes as your first diagnostic signal before reading full logs — 137 (OOM), 1 (generic app error), and 139 (segfault) each point you toward a different investigation path immediately.
- When a fix touches a base image (distroless, alpine, slim), always verify with an actual rebuild and a real network/functional test — base image behavior differences (missing certs, missing shell, missing libc) don't show up as build failures, only as runtime failures.