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.jsonto 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.
- Runclaude mcp listat 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, exportDOCKER_HOSTin the same shell before runningclaude, or add it underenvin 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 aCOPY --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 theCOPYline 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.
-RestartCountcombined withFinishedAttimestamps 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 toexec_in_containerwith 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:20pulls the full Debian-based image (~1.1 GB) instead ofnode:20-alpine(~180 MB) or a slim variant.COPY . .beforenpm installinvalidates the dependency-install cache layer on every source change, not just on dependency changes — the single most common Dockerfile cache mistake.npm installinstead ofnpm cidoesn'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 compileddist/output is needed at runtime. - No non-root user — the process runs as
rootinside 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 doesCOPY . .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 adistrolessorscratchfinal 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 projectper repository rather than a single global connection — it keepsDOCKER_HOSTand 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 reviewingbuild_imageandinspect_containeroutput before granting the agent permission to act onexec_in_containerorremove_image— read-then-verify is a faster debugging loop than act-then-check for anything touching a running container.