·

Docker MCP With Claude Code CLI and VS Code

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

Claude Code connects to local stdio MCP servers natively, which makes it a good fit for Docker MCP — the server runs as a subprocess talking straight to the Docker socket, and Claude Code manages its lifecycle for you. This topic covers wiring up mcp-server-docker, then walks through three workflows you'll actually use day to day: diagnosing a broken build from its layer log, inspecting a running container's environment and network from the terminal, and getting the AI to rewrite a Dockerfile for size and cache efficiency.


Installing and Connecting Docker MCP to Claude Code

Prerequisites
- Docker Engine or Docker Desktop running locally, with your user able to run docker ps without sudo
- Claude Code CLI installed (npm install -g @anthropic-ai/claude-code)
- uv/uvx installed for running the Python-based mcp-server-docker (curl -LsSf https://astral.sh/uv/install.sh | sh)

Step 1: Add the server

claude mcp add --scope project docker -- uvx mcp-server-docker

--scope project writes the entry to .claude/settings.json in the repo, which is the right default for a Docker integration — you almost always want it scoped to a specific project's Dockerfile and compose stack, not globally active in every session on your machine.

Step 2: Verify it started

claude mcp list

If it shows ✗ failed to connect, the most common cause is socket permissions:

docker ps

Fix by adding your user to the docker group (see Topic 1 for why that's not a trivial permission), then restart your shell session before retrying claude mcp add.

Step 3: Confirm the tool catalog inside a session

claude
List your available Docker tools.

Expect to see list_containers, inspect_container, fetch_container_logs, exec_in_container, list_images, build_image, and the compose_* family. If some are missing, check uvx mcp-server-docker --help — tool availability can differ slightly by installed version.

VS Code extension

The Claude Code VS Code extension reads the same .claude/settings.json, so no separate MCP configuration is needed there. Open the panel (Cmd+Shift+P → "Claude Code: Open Panel") and the docker server is already connected. The advantage inside the editor: Claude Code can see your open Dockerfile and docker-compose.yml at the same time it's calling live container tools, so a prompt like "does this file match what's actually running?" has both sides of the comparison available without you pasting anything.

// .claude/settings.json — resulting entry
{
  "mcpServers": {
    "docker": {
      "command": "uvx",
      "args": ["mcp-server-docker"]
    }
  }
}

Tips
- Commit .claude/settings.json to the repo so every teammate gets the same Docker MCP wiring without repeating setup — the server command has no secrets in it, since auth is just OS-level socket access.
- Run claude mcp list at the start of every session where you'll be doing Docker work — a silently disconnected server produces confusing "I don't have that tool" responses mid-task.
- If you use a non-default Docker context, export DOCKER_HOST in the same shell before running claude, or add it under env in the settings entry — the MCP server inherits it at process start, not per-call.


Diagnosing a Failing Container Build from Layer Logs

Start from a Dockerfile that's failing on a real, common error: a lockfile drift between package.json and package-lock.json.

FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["node", "dist/server.js"]
Build the image at tag myapp:dev from ./Dockerfile. If it fails, show me
the exact failing step and the last 30 lines of that layer's output.

Claude Code calls build_image, which streams back something shaped like this:

Step 4/8 : RUN npm ci
 ---> Running in 8f3a2c1d9e4b
npm ERR! code EUSAGE
npm ERR! `npm ci` can only install packages when your package.json and
npm ERR! package-lock.json are in sync. Please update your lock file with
npm ERR! `npm install` before continuing.
npm ERR!
npm ERR! Invalid: lock file's react@18.2.0 does not satisfy react@18.3.1
The command '/bin/sh -c npm ci' returned a non-zero exit code: 1

The agent reads that output directly — no copy-pasting a terminal scrollback — and can immediately correlate it against package.json in your working tree:

Read package.json and package-lock.json. Confirm the version mismatch
from the build error and tell me exactly which entries are out of sync.

The fix here is almost always regenerating the lockfile, but the useful part of the workflow is that Claude Code can verify the fix actually resolves the build rather than just asserting it should:

Run npm install locally to regenerate the lockfile for the react
mismatch, then rebuild myapp:dev and confirm the npm ci step succeeds.

For build failures that aren't dependency drift — a missing file in COPY, a bad RUN command, an unset build arg — the same pattern applies: get the exact layer and line from build_image's output, cross-reference the Dockerfile, and let the agent iterate. A representative multi-cause session:

Step 6/8 : COPY --from=build /app/dist ./dist
COPY failed: file not found in build context or excluded by .dockerignore: stat /app/dist: file does not exist

That specific error — dist missing from a build stage — usually means the build stage's RUN npm run build step either failed silently earlier in the log (scroll up, don't assume the last error is the root cause) or the stage name in --from=build doesn't match what's declared with AS build above it. Ask the agent to check both before proposing a Dockerfile edit.

Tips
- Ask for "the last 30 lines of the failing layer," not the full log — full multi-stage build output can run to hundreds of lines and burns context for no benefit once you've found the failure.
- When a COPY --from=<stage> fails, always have the agent scroll up to the named stage's own build output first — the real failure is often earlier and silent, not at the COPY line itself.
- Rebuild with --no-cache (ask the agent to pass it) if you suspect a stale cached layer is hiding whether your fix actually worked.


Inspecting Running Containers, Env Vars, and Networks from the Terminal

Once a container is running, the value shifts from build diagnostics to runtime state. A single inspect_container call replaces several manual docker inspect --format invocations.

Inspect the container named "api". Show me its exit code and restart
count if it's not running, its full environment variables, and which
networks it's attached to.

Representative inspect_container payload shape (trimmed):

{
  "Id": "3f2a9c1b8e...",
  "Name": "/api",
  "State": {
    "Status": "restarting",
    "Running": false,
    "ExitCode": 1,
    "OOMKilled": false,
    "RestartCount": 7,
    "StartedAt": "2026-08-21T09:14:02Z",
    "FinishedAt": "2026-08-21T09:14:03Z"
  },
  "Config": {
    "Env": [
      "NODE_ENV=production",
      "PORT=3000",
      "DATABASE_URL="
    ]
  },
  "NetworkSettings": {
    "Networks": {
      "myapp_default": {
        "IPAddress": "172.19.0.3",
        "Gateway": "172.19.0.1"
      }
    }
  }
}

Two things jump out immediately once the AI has this structured payload: RestartCount: 7 confirms a crash loop rather than a one-off failure, and DATABASE_URL= is present but empty — a strong signal the app is crashing on startup because a required connection string never got injected. This is exactly the kind of correlation that's tedious to do by eye across separate CLI outputs but trivial once it's one JSON object in context.

The DATABASE_URL env var is empty on the "api" container. Check the
compose file and the .env file in this repo — where should that value
be coming from, and why isn't it reaching the container?

For network debugging between two containers that can't reach each other:

Inspect containers "api" and "worker". Are they on the same Docker
network? If not, tell me which networks each is attached to and what
I'd need to change in the compose file to put them on the same one.

exec_in_container is the natural follow-up when inspection alone doesn't explain the symptom — for example, confirming DNS resolution actually works between two services on the same user-defined network:

Exec into the "api" container and run "getent hosts db" to confirm it
can resolve the database service by its Compose service name.

Tips
- Ask for env vars and network state in the same prompt when debugging connectivity issues — a missing env var and a network mismatch produce near-identical symptoms (service unreachable), and checking both at once saves a round trip.
- RestartCount combined with FinishedAt timestamps close together is your fastest crash-loop signal — ask the agent to compute the interval between restarts, since a tight loop (sub-second) usually means the process crashes before even binding its port.
- Be deliberate before asking the agent to exec_in_container with anything beyond a read-only diagnostic command — it's a real shell inside that container's namespace, not a sandboxed query.


Prompting AI to Optimize a Dockerfile for Size and Build Cache

Here's a realistic starting point — functional, but not something you'd want shipping to production:

FROM node:20
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
EXPOSE 3000
CMD ["node", "dist/server.js"]
Review this Dockerfile for image size and build cache efficiency. List
every issue you find, then rewrite it as a multi-stage build. Explain
the size impact of each change.

A useful response names the concrete problems before touching the file:

  • FROM node:20 pulls the full Debian-based image (~1.1 GB) instead of node:20-alpine (~180 MB) or a slim variant.
  • COPY . . before npm install invalidates the dependency-install cache layer on every source change, not just on dependency changes — the single most common Dockerfile cache mistake.
  • npm install instead of npm ci doesn't respect the lockfile exactly and is slower and less reproducible in CI.
  • No multi-stage split means devDependencies, the TypeScript compiler, and build tooling all ship in the final runtime image even though only the compiled dist/ output is needed at runtime.
  • No non-root user — the process runs as root inside the container by default.

FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev

FROM node:20-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup -S app && adduser -S app -G app
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY package.json ./
USER app
EXPOSE 3000
CMD ["node", "dist/server.js"]

Ask the agent to verify the claim, not just assert it:

Build both the old and new Dockerfiles as myapp:before and myapp:after.
Compare their final image sizes and show me the layer breakdown for
myapp:after with docker history.

A typical docker history result for the optimized image, confirming the runtime stage carries no build tooling:

IMAGE          CREATED BY                                      SIZE
a1b2c3d4e5f6   CMD ["node" "dist/server.js"]                   0B
<missing>      USER app                                        0B
<missing>      COPY package.json ./                             412B
<missing>      COPY --from=build /app/dist ./dist              1.4MB
<missing>      COPY --from=deps /app/node_modules ./node_modules 62MB
<missing>      RUN addgroup -S app && adduser -S app -G app    4.1kB

Real numbers from this kind of change land around 1.1 GB → 150–180 MB, mostly from the Alpine base plus dropping devDependencies and the toolchain from the final layer. The build-cache win is separate from size: on an unchanged dependency set, deps and the dependency-install portion of build now hit cache even when you only edit application source, cutting a typical CI build from a full npm ci (30–60s on a cold cache) down to a few seconds.

Tips
- Always ask for a before/after size comparison built from the actual Dockerfiles, not an estimate — cache state and base image patch versions shift numbers by tens of MB between runs.
- Push the "copy only manifest files before installing" pattern explicitly in your prompt if the agent's first draft still does COPY . . before install — it's the single highest-leverage fix and worth stating as a requirement, not hoping the model infers it.
- For compiled languages (Go, Rust), ask specifically about a distroless or scratch final stage — the size wins are even larger than Alpine, but come with the CA-certificate gotcha covered in Topic 4.


Tips

Tips
- Scope the Docker MCP connection to --scope project per repository rather than a single global connection — it keeps DOCKER_HOST and socket context unambiguous when you're juggling multiple projects with different Docker setups.
- Keep the VS Code panel and terminal CLI sessions in sync by using the same .claude/settings.json — switching between them mid-task should never require reconnecting the Docker server.
- Default to reviewing build_image and inspect_container output before granting the agent permission to act on exec_in_container or remove_image — read-then-verify is a faster debugging loop than act-then-check for anything touching a running container.