Skip to content

Containerization with AI agents means Claude Code, Codex, or Cursor writes the Dockerfile and Kubernetes manifests while a fixed gate proves the result: hadolint, a Trivy scan, a non-root and size check, a server-side dry run, and a read-only cluster connection for debugging. The agent drafts; the gate, not a line-by-line read, decides whether the change ships.

This page is for developers who own a service’s image and deployment, and for tech leads who want every agent-written container change to arrive with the same evidence. Your image is 1.2 GB, Trivy reports 40 HIGH findings in the base layer, and the pod in staging just restarted with exit code 137. You can hand all three problems to an agent, as long as you decide in advance what “done” means and let tools check it.

What you’ll walk away with from agent-driven containerization

Section titled “What you’ll walk away with from agent-driven containerization”
  • A prompt that turns a single-stage Dockerfile into a distroless multi-stage build, plus a critique prompt that attacks the result
  • A five-command image gate you run locally or in CI, so you judge the image by its scan and its behaviour instead of by reading it
  • A manifest-hardening prompt that returns a diff that passes the Kubernetes restricted Pod Security Standard
  • An exit-137 investigation that starts from evidence, not from guesses
  • A read-only Kubernetes MCP setup with a least-privilege ServiceAccount, and the Docker MCP Gateway in its current profile model

How do you write the container standard down once?

Section titled “How do you write the container standard down once?”

Agents repeat whatever the project tells them on every session. Put the container rules in the project instructions before you ask for the first Dockerfile, so you do not re-type them in every prompt.

Add a section to CLAUDE.md at the repository root (when a project has no CLAUDE.md, Claude Code reads AGENTS.md instead, from v2.1.277, on both release channels):

## Containers
- Multi-stage builds only. The final stage is distroless and runs as a non-root user.
- Builder and runtime share the same libc (Debian builder for a Debian-based distroless runtime).
- Pin base images by digest (`@sha256:…`), never `latest`. Never COPY `.env`, `.git` or credentials.
- Done means: hadolint clean, Trivy has no fixable HIGH/CRITICAL, `kubectl apply --dry-run=server` passes.
- Image gate: hadolint, `docker image inspect` size and `.Config.User`, `trivy image --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1`, smoke test on /health.

A tag such as :nonroot floats, so the image it names can change under an unchanged Dockerfile. A digest cannot. Let Renovate or Dependabot bump the digest through a pull request, so every base-image change passes the same gate as a code change.

The last line matters most. It turns “make it secure” into a definition of done that the agent can run itself and report on. For the structure of project instructions in general, see how to write a CLAUDE.md or AGENTS.md the agent follows.

Generate a hardened multi-stage Dockerfile

Section titled “Generate a hardened multi-stage Dockerfile”

Hand the agent the current Dockerfile and your real constraints: runtime, port, build command, and health endpoint. The prompt is the same in all three tools; only the invocation differs. Save the copy-paste prompt below as prompts/dockerfile.txt, so the CLI tabs send the full brief rather than a one-line summary.

From the repository root, pass the saved prompt. Claude Code reads Dockerfile and package.json off disk:

Terminal window
claude "$(cat prompts/dockerfile.txt)"

The result should look close to this:

FROM node:24-trixie-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build && npm prune --omit=dev
FROM gcr.io/distroless/nodejs24-debian13:nonroot
WORKDIR /app
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
EXPOSE 3000
CMD ["dist/server.js"]

The sample shows tags for readability; the file you commit carries @sha256:… digests after each image name.

Three details in that file are where agents most often go wrong:

  • Matching libc. The distroless Node.js images are Debian-based. An agent that builds on node:*-alpine (musl) and copies node_modules into a Debian runtime ships native modules that fail to load at start-up. Keep the builder on the same Debian release as the runtime.
  • CMD is a script path, not node …. The distroless Node.js image’s entrypoint is already /nodejs/bin/node, so CMD ["node", "dist/server.js"] makes Node load node as the script and exit.
  • No HEALTHCHECK. Kubernetes ignores the Dockerfile HEALTHCHECK instruction and uses the probes in the Pod spec instead. If the image also runs under plain Docker or Compose, a HEALTHCHECK has to call /nodejs/bin/node with a script, because the image has no shell and no curl.

Generation is half the loop. Before the gate runs, make the agent attack its own output: a second pass with a different brief finds problems the first pass rationalised.

How do you prove the image is good without reading it?

Section titled “How do you prove the image is good without reading it?”

Run a gate that checks behaviour, not style. Each command below either passes or fails, which means you, CI, or the agent itself can run it and nobody has to read the Dockerfile line by line.

  1. Lint the Dockerfile. Run docker run --rm -i hadolint/hadolint < Dockerfile. hadolint flags unpinned bases, apt-get without cleanup, and shell-form commands.

  2. Build and record the size.

    Terminal window
    docker build -t api:candidate .
    docker image inspect api:candidate --format '{{.Size}}'

    Compare the size with the image you are replacing; the prompt asked the agent for both numbers, so check them yourself.

  3. Confirm the image runs as non-root. Run docker image inspect api:candidate --format '{{.Config.User}}'. An empty result or 0 means root. The :nonroot distroless tag sets UID 65532.

  4. Scan for fixable vulnerabilities. Run trivy image --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1 api:candidate. The non-zero exit code is what makes this a gate in CI rather than a report nobody reads.

  5. Smoke-test the behaviour.

    Terminal window
    docker run -d --rm --name api-smoke -p 3000:3000 api:candidate
    curl -fsS --retry 10 --retry-connrefused --retry-delay 1 \
    http://localhost:3000/health && rc=0 || rc=$?
    docker stop api-smoke
    test "$rc" -eq 0

Put the same five steps in CI and attach their output to the pull request. That output belongs in the evidence bundle an agent’s pull request must carry, and the reviewer judges the bundle using the triage protocol for agent pull requests. The human sign-off is on the evidence and on any change to the gate itself, not on the Dockerfile’s text. To wire the gate into a build-scan-sign-deploy pipeline, see CI/CD pipelines.

Harden a Kubernetes Deployment with a reviewable diff

Section titled “Harden a Kubernetes Deployment with a reviewable diff”

Agents like to emit a 200-line Deployment with every field set, and you cannot review what you cannot read. Scope each request to one resource and one concern, and ask for a diff instead of a rewrite.

A diff shows you the lines that changed instead of a wall of YAML the agent reproduced from memory and may have altered elsewhere. The seccompProfile line is the one agents most often leave out, and the restricted profile rejects a Pod without it.

Two commands verify the diff against the real cluster rather than against the agent’s memory:

Terminal window
# Validates against the live API server, including admission. Changes nothing.
kubectl apply --dry-run=server -f k8s/api/deployment.yaml -n staging
# Shows what would change compared with what is running now.
kubectl diff -f k8s/api/deployment.yaml -n staging

For the dry run to test Pod security, the namespace has to carry the Pod Security labels. Pod Security Admission enforces only on Pods; for a Deployment it warns. Label the namespace for both:

Terminal window
kubectl label namespace staging \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/warn=restricted

With the warn label, the dry run of the Deployment prints Warning: would violate PodSecurity "restricted:latest": … and names each missing field. Treat that warning as a failure: in CI, fail the step when the dry-run output contains would violate PodSecurity. Without the check, the Deployment applies cleanly and its ReplicaSet then fails to create any Pods.

Exit code 137 means the process received SIGKILL (128 + 9). When kubectl describe shows Reason: OOMKilled, the kernel killed the container for exceeding its memory limit. Finding out why means correlating the limit, the real usage, the logs before the kill, and what changed. Give the agent that evidence rather than asking it to speculate.

  1. Collect the evidence into one file.

    Terminal window
    kubectl describe pod -l app=api -n staging > /tmp/pod.txt
    kubectl logs -l app=api -n staging --previous --tail=200 >> /tmp/pod.txt
    kubectl top pod -l app=api -n staging >> /tmp/pod.txt
    git log --oneline -15 -- src/ package.json >> /tmp/pod.txt

    kubectl top needs metrics-server in the cluster. If it errors, the other three sources are still enough to start.

  2. Hand the bundle to the agent and ask for ranked hypotheses. In Claude Code and Codex, name the path in the prompt below. In Cursor, attach it with @pod.txt. The request is the same in all three tools.

  3. Apply the smallest fix, then verify it. After the rollout, watch kubectl get pod -l app=api -n staging -w until the restart count stops climbing. Then compare kubectl top with the new limit and confirm headroom under real traffic, not just at start-up.

The last line matters. Raising the limit is the fix an agent reaches for first, and it hides a leak until the next, larger kill. When the root cause is a leak, send the fix through progressive delivery, so a canary catches a wrong diagnosis before every pod does.

Connect the agent to live cluster state with MCP

Section titled “Connect the agent to live cluster state with MCP”

Pasting files works. A Model Context Protocol (MCP) server goes further: the agent queries the live cluster itself, so “why is this pod restarting?” is answered from current events, not from a copy you made ten minutes ago. Two servers matter here. The full reference, including TOML configuration and the Terraform server, is on the infrastructure MCP page.

kubernetes-mcp-server (from containers/kubernetes-mcp-server; about 2.1k GitHub stars and npm version 0.0.67 on 2026-09-26) is a native Go server for Kubernetes and OpenShift. Its --help in v0.0.67 lists, among others, --kubeconfig, --read-only (expose only tools marked read-only), --disable-destructive, --toolsets (default core,config), --config, and --port. It has no --audit-log, --rbac-mode, --namespace-filter, or --context flag (checked against v0.0.67). Access control comes from the Kubernetes identity in the kubeconfig you pass, not from a switch on the server.

Start with a least-privilege identity. This Role lets the agent read Pods, logs, events, and Deployments in one namespace and nothing else:

apiVersion: v1
kind: ServiceAccount
metadata: { name: agent-reader, namespace: staging }
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata: { name: agent-reader, namespace: staging }
rules:
- apiGroups: [""]
resources: ["pods", "pods/log", "events", "services"]
verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
resources: ["deployments", "replicasets"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata: { name: agent-reader, namespace: staging }
subjects: [{ kind: ServiceAccount, name: agent-reader, namespace: staging }]
roleRef: { kind: Role, name: agent-reader, apiGroup: rbac.authorization.k8s.io }

Prove the boundary before you connect anything: kubectl auth can-i delete pods -n staging --as=system:serviceaccount:staging:agent-reader must print no. Build a kubeconfig for that ServiceAccount (for example with a token from kubectl create token agent-reader -n staging --duration=8h) and save it as ~/.kube/agent-reader.yaml. Then register the server:

Terminal window
claude mcp add kubernetes -e KUBECONFIG=$HOME/.kube/agent-reader.yaml \
-- npx -y kubernetes-mcp-server@0.0.67 --read-only

Pin the version you verified (0.0.67 here) and bump it deliberately: this server holds cluster credentials, so it follows the same no-latest rule as your base images.

--read-only and the Role are two independent locks: if a prompt or a poisoned log line talks the agent into a write, the server does not expose the tool, and the API server would refuse it anyway. Keep the default toolsets unless you need Helm (--toolsets core,config,helm); every extra toolset adds tool definitions to the agent’s context on every request.

The Docker MCP Toolkit is a Docker Desktop feature powered by the docker mcp CLI plugin (docker/mcp-gateway, about 1.6k GitHub stars on 2026-09-26). The gateway runs each MCP server from Docker’s catalog in its own container, manages their secrets and OAuth tokens (docker mcp secret, docker mcp oauth), and serves one profile of servers to every client. It is how you run other MCP servers in isolation; it is not a tool for building your images. There is no mcp/docker-toolkit image to point a client at.

The current README (2026-09-26) is organised around profiles:

Terminal window
docker mcp catalog pull mcp/docker-mcp-catalog
docker mcp profile create --name container-tools \
--server catalog://mcp/docker-mcp-catalog/SERVER_NAME
docker mcp gateway run --profile container-tools

Replace SERVER_NAME with a server listed in the catalog. Without --profile, the gateway uses the default profile. Outside Docker Desktop (Docker CE, WSL2), run docker mcp feature enable profiles first. Then point each client at the gateway over stdio:

Terminal window
claude mcp add docker -- docker mcp gateway run --profile container-tools

Docker’s README also documents docker mcp client connect claude-code --profile container-tools --global, which writes the entry for you.

For the security model behind both servers, including tool poisoning and why a read-only default matters, see MCP security.

Should the container standard be a skill or an MCP server?

Section titled “Should the container standard be a skill or an MCP server?”

Use a skill when the agent needs repeatable know-how and no live state: your Dockerfile template, the image gate commands, and the hostile-review checklist packaged as an Agent Skill (a folder with a SKILL.md) that Claude Code, Codex, and Cursor can all load. Use an MCP server when the agent has to read something that changes: pods, events, logs. The two combine well: a container-standards skill writes and checks the change, and the read-only Kubernetes server shows what the cluster is doing. To package and share the skill, see installing and managing skills.

When agent-written container configs break

Section titled “When agent-written container configs break”
  • The container exits at start-up with Error loading shared library or an ELF load error from a native module. The builder and runtime use different libc or Node.js majors. Recovery: rebuild on node:24-trixie-slim for the nodejs24-debian13 runtime, and add the check to the hostile-review prompt.
  • The container exits immediately with Error: Cannot find module '/app/node'. CMD repeats the entrypoint, so Node treats node as the script path. Recovery: CMD ["dist/server.js"].
  • The app crashes with EROFS after hardening. readOnlyRootFilesystem: true blocks writes to the app directory. Recovery: mount an emptyDir wherever the app writes (/tmp, a cache directory) and ask the agent to find every write path in the code before it sets the flag.
  • The dry run warns would violate PodSecurity "restricted:latest", or the Deployment applies but no Pods appear. Pod Security Admission is doing its job; the message names the missing field, usually seccompProfile or capabilities. For a Deployment that already applied, kubectl describe replicaset -l app=api -n staging shows the same message under Events. Recovery: paste the message back to the agent verbatim and re-run the dry run.
  • The agent emits a removed API. Recovery: give it the cluster version, have it run kubectl api-versions, and keep the server-side dry run in the gate.
  • The Kubernetes MCP server connects, but every tool returns forbidden. The Role is narrower than the question, or the kubeconfig token expired. Recovery: run kubectl auth can-i --list -n staging --as=system:serviceaccount:staging:agent-reader, widen the Role by one resource at a time, and never swap in an admin kubeconfig to make the error go away.
  • docker mcp gateway run starts but exposes no tools. The profile is empty or the Toolkit is not enabled. Recovery: enable the MCP Toolkit in Docker Desktop (or docker mcp feature enable profiles elsewhere), add a server with docker mcp profile server add container-tools --server catalog://mcp/docker-mcp-catalog/SERVER_NAME, and check it with docker mcp profile show container-tools.
  • Someone proposes skipping permissions “because it is in a container”. A devcontainer with network access to your registry and cluster is not a disposable sandbox. Keep the tool’s normal permission prompts, or its sandbox, unless the container is disposable and network-restricted; see sandboxing and permission modes.

Where to go next after containerizing with agents

Section titled “Where to go next after containerizing with agents”