Skip to content

Claude Code generates Dockerfiles, Docker Compose stacks, Kubernetes manifests, and Helm charts from the repository it runs in: the lockfile, the build scripts, the health endpoint, and the ports the code opens. The output earns trust only through mechanical gates: hadolint, a Trivy scan, kubeconform schema validation, and a smoke deploy to a throwaway kind cluster.

Your service works on your laptop, and now it has to run in a cluster. The Dockerfile a tutorial gives you often builds a gigabyte-plus image that runs as root, and the first manifest often has no resource limits, no readiness probe, and a database password in an environment variable. You want Claude Code to write all of it, but you cannot read 600 lines of YAML on every change.

This page is for the developer who containerizes a service and the tech lead who owns the team’s container standards. It covers writing and proving the container artifacts with Claude Code. Shipping them is on deployment automation with Claude Code, the pipeline that builds them is on pipeline configuration with Claude Code, and the cross-tool version of this workflow (Cursor and Codex included) is Docker and Kubernetes containerization. To run Claude Code itself inside a container, see development container setup.

What you’ll walk away with from containerizing with Claude Code

Section titled “What you’ll walk away with from containerizing with Claude Code”
  • A CLAUDE.md section that encodes your container standards once, so every Dockerfile and manifest Claude writes follows them.
  • Permission rules and a PostToolUse hook that lint every container file Claude edits and keep it away from shared clusters and registries.
  • Copy-paste prompts for a multi-stage Dockerfile, a development Compose stack, hardened Kubernetes manifests, and a Helm chart.
  • A five-gate verification sequence that proves the artifacts work without a line-by-line read, and names who signs off.
  • The ways agent-written container config fails, and how to recover from each.

Set the container rules before the first prompt

Section titled “Set the container rules before the first prompt”

Claude’s defaults are plausible, not yours: a floating node:latest tag, a root user, and probes on paths your service does not serve. Write the rules into the project’s CLAUDE.md once. Every later prompt inherits them, and the last line turns “Claude wrote a Dockerfile” into “Claude wrote a Dockerfile and ran the checkers on it”.

## Containers
- Base images: pin by tag AND digest (`node:22-bookworm-slim@sha256:...`). Resolve digests with
`docker buildx imagetools inspect IMAGE:TAG`; never write a digest from memory.
- Every Dockerfile is multi-stage. The runtime stage runs as UID 10001, has no build tools,
and declares a HEALTHCHECK that calls GET /healthz.
- The service reads config from env vars only. Secrets come from Kubernetes Secrets
(created by External Secrets), never from values.yaml, ConfigMaps or ENV lines.
- Manifests live in deploy/k8s/, the Helm chart in deploy/chart/.
- Every container sets resources.requests and limits, runAsNonRoot, readOnlyRootFilesystem,
allowPrivilegeEscalation: false, and drops ALL capabilities.
- Probes: /healthz = process alive (liveness), /readyz = dependencies reachable (readiness).
- You may use the kube context `kind-dev` (kind cluster `dev`) only. Never push images or touch any other context.
- After editing container files, run: hadolint, trivy config, kubeconform (see scripts/check-containers.sh).

Keep Claude away from shared clusters and registries

Section titled “Keep Claude away from shared clusters and registries”

Container work sits one command away from production: docker push, helm upgrade, and kubectl delete all work from the same terminal. Two layers keep a session contained.

The boundary is the credential. Start Claude Code with a kubeconfig that holds only the local cluster, so there is no production context to reach:

Terminal window
# Terminal: create a local cluster and a kubeconfig that contains nothing else
kind create cluster --name dev --kubeconfig "$HOME/.kube/kind-dev.yaml"
KUBECONFIG="$HOME/.kube/kind-dev.yaml" claude

The deny list catches the common spellings. Add this to .claude/settings.json and commit it:

{
"permissions": {
"deny": [
"Bash(docker push *)",
"Bash(helm upgrade *)",
"Bash(helm uninstall *)",
"Bash(kubectl delete *)"
],
"ask": [
"Bash(kubectl apply *)",
"Bash(helm install *)"
]
}
}

A PostToolUse hook runs after each edit and, with exit code 2, puts the linter output in front of Claude, which fixes it on the next step. Save the script as .claude/hooks/lint-containers.sh, run chmod +x on it, and register it in the same .claude/settings.json. The full event and handler reference is on Claude Code hooks.

#!/usr/bin/env bash
# .claude/hooks/lint-containers.sh (PostToolUse, matcher "Edit|Write"); needs jq, hadolint, kubeconform
set -uo pipefail
file=$(jq -r '.tool_input.file_path // empty')
[[ -f $file ]] || exit 0
case "$file" in
*Dockerfile*)
out=$(hadolint "$file" 2>&1) || { echo "hadolint failed on $file:" >&2; head -n 30 <<<"$out" >&2; exit 2; } ;;
*/deploy/k8s/*.yaml|*/deploy/k8s/*.yml)
out=$(kubeconform -strict -summary -ignore-missing-schemas "$file" 2>&1) || { echo "kubeconform failed on $file:" >&2; head -n 30 <<<"$out" >&2; exit 2; } ;;
esac
exit 0
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/lint-containers.sh" }
]
}
]
}
}

The edit has already happened when a PostToolUse hook runs, so the hook cannot block it; exit code 2 hands the linter output back to Claude as feedback. Test the hook once by asking Claude to add FROM node:latest to a scratch Dockerfile: the next reply should quote hadolint’s DL3007 warning and pin the tag.

Generate a production Dockerfile from the repository

Section titled “Generate a production Dockerfile from the repository”

Ask for the Dockerfile in terms of your repository’s facts. Claude reads the lockfile to pick the package manager, the build script to find the output directory, and the server code to find the port and the health route. Ask it to finish with a table: that table is what you review, not the Dockerfile.

For a Go service, expect a static binary copied into a scratch or distroless stage. For Python, expect a virtual environment built in one stage and copied into the runtime stage. If the table shows a large image, iterate with the number, not a vague request:

The runtime image is 480 MB. List the five largest layers with `docker history myapp:test`,
explain what each contains, and remove everything the runtime stage does not execute.

Build a Compose stack for local development

Section titled “Build a Compose stack for local development”

Compose is where developers run the service with its dependencies. The one line that matters most is depends_on with condition: service_healthy. Without it, the app starts before PostgreSQL accepts connections and crashes on boot.

docker compose up --wait blocks until every service with a health check reports healthy, so the prompt ends with evidence rather than a claim.

Kubernetes YAML is where omissions hide: a missing readinessProbe still applies cleanly and then serves traffic before the app is ready. Put every requirement in the prompt and ask for a summary table you can check against your platform standards.

The CPU limit is omitted on purpose: a CPU limit throttles a pod even when the node has idle cores, which shows up as latency spikes rather than errors. Keep the memory limit, because memory cannot be throttled, only killed.

Once the raw manifests pass on kind, a chart parameterizes them per environment. Convert working manifests rather than generating a chart from nothing: the diff between the rendered chart and the proven manifests is then your review surface.

Verify the container artifacts without reading every line

Section titled “Verify the container artifacts without reading every line”

Every gate below runs mechanically, in the session and again in CI. Together they turn “the YAML looks right” into evidence.

  1. Lint the Dockerfile. hadolint catches unpinned base images, apt-get without cleanup, and shell-form CMD that swallows signals. The hook above runs it on every edit.

  2. Scan the image and the config. Trivy runs twice: trivy image --severity HIGH,CRITICAL --exit-code 1 myapp:test for vulnerable packages, and trivy config deploy/ for misconfigurations such as a root user or a missing readOnlyRootFilesystem. docker scout cve myapp:test is an alternative for the image scan if your team already uses Docker Scout.

  3. Validate every manifest against the Kubernetes schema. kubeconform with -strict rejects unknown fields, which is how a misspelled readinessprobe gets caught instead of being silently ignored. Custom resources such as ExternalSecret have no built-in schema, so -ignore-missing-schemas skips them; point -schema-location at a CRD schema catalog if you want them checked too. Pipe helm template output into it for the chart.

  4. Smoke-deploy to a throwaway cluster. A fresh kind cluster lacks what production provides, so prepare it first or the rollout never finishes:

    • Load the image. A kind node cannot pull an image that exists only in your local Docker: kind load docker-image myapp:test --name dev.
    • Switch off External Secrets. The cluster has no External Secrets CRDs, so values-kind.yaml sets externalSecret.enabled: false, image: myapp:test, and pullPolicy: IfNotPresent.
    • Create a stand-in Secret. kubectl create secret generic myapp-db --from-literal=DATABASE_URL='postgres://app:app@postgres:5432/app' points at the throwaway Postgres below.
    • Apply the dependencies. /readyz checks the database and Redis, so apply the throwaway Postgres and Redis from deploy/kind/ in the same namespace.

    Then install the chart, wait for the rollout, and run the chart test:

    Terminal window
    # Terminal: on the kind-dev context, after the preparation above
    helm install myapp deploy/chart -f deploy/chart/values-staging.yaml \
    -f deploy/chart/values-kind.yaml --wait
    kubectl rollout status deploy/myapp
    helm test myapp

    A rollout that finishes proves the probes, security context, and ports agree with the code. This is the check no linter replaces.

  5. Assign the sign-off. The service owner approves on the evidence from steps 1–4 and the tables Claude produced. A change to the security context, NetworkPolicy, RBAC, or secret wiring also needs the platform owner, the second reviewer your evidence bundle names for sensitive paths.

Put steps 1–3 in scripts/check-containers.sh so the hook, the session, and your CI job run the same commands. The image scan needs a built image, so CI runs trivy image after the build step:

#!/usr/bin/env bash
# scripts/check-containers.sh: one gate for the hook, the session, and CI
set -euo pipefail
find . -name 'Dockerfile*' -not -path './node_modules/*' -print0 | xargs -0 -r hadolint
trivy config --exit-code 1 deploy/
kubeconform -strict -summary -ignore-missing-schemas deploy/k8s/
for values in deploy/chart/values-*.yaml; do
helm template myapp deploy/chart -f "$values" |
kubeconform -strict -summary -ignore-missing-schemas
done

The pipeline side is on pipeline configuration with Claude Code.

Debug a live cluster with the Kubernetes MCP server

Section titled “Debug a live cluster with the Kubernetes MCP server”

For “why is this pod crash-looping” questions, the Kubernetes MCP server (community, containers GitHub org; npm kubernetes-mcp-server 0.0.67 and about 2.1k GitHub stars, both read 2026-09-26) gives Claude structured access to pods, logs, and events instead of parsing kubectl output. Its runtime settings live in a TOML file, and read_only = true is the setting to start with:

Terminal window
# Terminal: ~/.config/k8s-mcp.toml contains the line read_only = true
claude mcp add -s user kubernetes -- npx -y kubernetes-mcp-server@0.0.67 --config ~/.config/k8s-mcp.toml

Scope it to your user (-s user) rather than the project, because the kubeconfig it reads is yours. With it connected, a prompt such as “Why is the checkout pod in namespace staging in CrashLoopBackOff? Read its logs and events, then propose a manifest fix as a diff; do not apply it.” returns a diagnosis grounded in the cluster’s own events. Other infrastructure servers are compared on infrastructure MCP servers.

What breaks when Claude writes your container config?

Section titled “What breaks when Claude writes your container config?”

The base image tag or digest does not exist. Claude wrote a plausible node:22.9-alpine3.21 or a digest from memory, and the build fails on pull. Recovery: tell Claude to resolve every FROM with docker buildx imagetools inspect and paste the output into its reply. The CLAUDE.md rule above prevents the repeat.

Every code change rebuilds all dependencies. COPY . . comes before the dependency install, so any source edit invalidates the cache. Recovery: “Reorder the Dockerfile so the dependency layer depends only on the lockfile, then prove it: change a source file, rebuild, and show that the install step reports CACHED.”

The smoke deploy hangs in ImagePullBackOff. The kind node looked for myapp:test in a registry, because the image was never loaded into the cluster or the pull policy is Always. Recovery: run kind load docker-image myapp:test --name dev, set pullPolicy: IfNotPresent, and never let Claude “fix” it by pushing the image to a shared registry: the deny list and the CLAUDE.md rule both forbid that.

The smoke-deploy rollout never finishes. helm install fails with no matches for kind "ExternalSecret", or the pods sit in CreateContainerConfigError or never turn ready. The kind cluster is missing a CRD, a Secret, or a dependency that /readyz checks. Recovery: run kubectl get events --sort-by=.lastTimestamp and kubectl describe pod, then add the missing piece to the kind preparation (the values-kind.yaml flag, the stand-in Secret, or the throwaway Postgres and Redis), not to the chart’s production defaults.

Pods restart with OOMKilled. The memory limit is below the real working set. Recovery: read kubectl describe pod for the last state, then ask Claude to size requests and limits from the container_memory_working_set_bytes metric your cluster already exports, not from a guess. Ongoing dashboards are covered in monitoring with Claude Code.

Claude makes a failing probe pass by loosening it. Asked to fix a failing readiness probe, the agent raises failureThreshold to 60 or points the probe at /healthz. The pod goes green and still serves traffic before the database connects. Recovery: state in the prompt that probe paths and thresholds are fixed and the fix must be in the application; review any diff that touches a probe as a sensitive change.

The readiness probe returns 200 while the app cannot serve. A /readyz that only returns 200 proves the process is up. Recovery: “Make /readyz check that the database pool has a free connection and Redis answers PING, each with a 500 ms timeout, and return 503 with the failing dependency named in the body.”

The chart works in staging and fails in production. A template references a key that only values-staging.yaml defines. Recovery: “Render the chart with each values file, list every .Values key the templates use, and show which values file leaves each one undefined.” Then add the production render to the kubeconform gate.

Compose volumes are owned by root on the host. The container’s UID differs from yours, so files it writes are read-only for you. Recovery: “Add a USER_UID build argument defaulting to 1000 for the development stage only; keep UID 10001 in the runtime stage.”

The image fails with exec format error on another machine. It was built for arm64 on an Apple silicon laptop and runs on amd64 nodes. Recovery: build with docker buildx build --platform linux/amd64,linux/arm64, and have CI publish the multi-architecture manifest rather than any laptop.

Where to go next after containerizing with Claude Code

Section titled “Where to go next after containerizing with Claude Code”