Docker & Container Patterns
Your image is 1.2 GB, the build takes eight minutes on every CI run, and a docker scout scan just flagged 40 CVEs in the base layer. Meanwhile the container won’t shut down cleanly because PID 1 is swallowing SIGTERM, and your “production” Compose file is the same one you use for local hot-reload. None of this is exotic — it’s the default state of a Dockerfile that grew organically. This recipe walks the AI through fixing each of those, with prompts you can paste directly.
What You’ll Walk Away With
Section titled “What You’ll Walk Away With”- A multi-stage Dockerfile pattern that separates build deps from runtime and ships a non-root image under 150 MB.
- Copy-paste prompts for shrinking an image, hardening it against CVEs, and adding correct healthchecks and signal handling.
- A Compose orchestration pattern with
depends_onhealth conditions instead ofsleephacks. - A GitHub Actions pipeline that builds multi-arch, scans with Trivy, and deploys — with each step actually parseable.
- The failure modes (cache busting, distroless debugging, healthcheck false-negatives, multi-arch breaks) and how to recover.
Quick Setup
Section titled “Quick Setup”Point your tool at the repo and let it read the existing Dockerfile, package.json/pyproject.toml, and any docker-compose.yml before it proposes changes. The setup is identical across all three tools; only the invocation differs.
Open the repo, enter Agent mode, and reference the files explicitly so it grounds on your real build:
@Dockerfile @package.json — audit this image and tell me the three biggestwins for size and security before changing anything.Run from the project root so it has filesystem context:
claude "Audit @Dockerfile and @package.json. List the three biggest wins forimage size and security, with the line numbers, before you change anything."Codex runs on GPT-5.5 by default across the App, CLI, IDE, and Cloud. Start a session from the repo root with a positional prompt:
codex "Audit the Dockerfile and package.json in this repo. List the threebiggest wins for image size and security with line numbers, then wait."For an unattended, scripted audit (CI or a worktree), use codex exec with the low-friction preset:
codex exec --full-auto "Audit the Dockerfile, report size and CVE wins as a checklist"Before you let the agent touch your Dockerfile, give it the house rules. Put them in .cursor/rules/docker.mdc (Cursor’s current project-rules format; the old single-file .cursorrules is legacy) or in CLAUDE.md:
- Pin base images to a digest or explicit version — never
:latest. - Multi-stage builds: build deps never reach the runtime stage.
- Run as a non-root user; drop the default capabilities.
- Always ship a
.dockerignoreand aHEALTHCHECK. - Combine
RUNlayers and clean the package-manager cache in the same layer.
Multi-Stage Builds
Section titled “Multi-Stage Builds”The single highest-leverage pattern: compile or install in a fat builder stage, then copy only the artifacts into a slim runtime stage. The mistake we see most is reinstalling dependencies in the final stage (or the cp -R node_modules && npm ci dance), which both bloats the image and fights npm ci’s habit of wiping node_modules on every run. The clean split is: full install to build, --omit=dev install into runtime.
Paste the prompt above into Agent mode with @Dockerfile and @package.json attached. Review the generated stages in the diff view, then ask it to justify any layer you don’t recognize.
# Build stageFROM node:20-alpine AS builderRUN apk add --no-cache python3 make g++WORKDIR /appCOPY package*.json ./RUN npm ciCOPY . .RUN npm run build
# Production stageFROM node:20-alpine AS productionRUN apk add --no-cache dumb-initRUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001WORKDIR /appCOPY package*.json ./RUN npm ci --omit=dev && npm cache clean --forceCOPY --from=builder --chown=nodejs:nodejs /app/dist ./distUSER nodejsEXPOSE 3000HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD node healthcheck.jsENTRYPOINT ["dumb-init", "--"]CMD ["node", "dist/index.js"]Claude Code edits the file in place and shows you the diff. Useful when you want it to rewrite an existing Dockerfile rather than generate a fresh one:
claude "Rewrite @Dockerfile as a multi-stage build: full npm ci in the builder,npm ci --omit=dev in the runtime stage, non-root user, dumb-init as PID 1,HEALTHCHECK on /health. Show me the diff and the expected final image size."For Python, the same split applies — full install in the builder, copy site-packages into a slim runtime. Codex handles polyglot repos well across its surfaces; in the IDE or App, point it at the service directory:
codex "Generate a multi-stage Dockerfile for this Python 3.12 app using Poetry.Builder installs deps with 'poetry install --no-interaction --only main' into avenv; runtime stage is python:3.12-slim, copies the venv, runs as a non-rootuser, adds a HEALTHCHECK, and starts gunicorn with 4 workers."Codex’s worktree support is handy here: spin up a worktree per service so it can generate Dockerfiles for several services in parallel without colliding.
Container Optimization
Section titled “Container Optimization”Once it builds, make it small. Distroless and scratch images strip the shell and package manager entirely; Go binaries with CGO_ENABLED=0 and -ldflags="-w -s" can land in single-digit megabytes. The trick is asking the AI to explain each cut so you don’t ship a “smaller” image that’s missing CA certificates or a timezone database.
Run docker history --human image:tag first, paste the output into the chat, and let Cursor target the fattest layers specifically rather than guessing.
# Go service, distroless final stageFROM golang:1.24-alpine AS builderRUN apk add --no-cache git ca-certificatesWORKDIR /appCOPY go.mod go.sum ./RUN go mod downloadCOPY . .RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o server ./cmd/server
FROM gcr.io/distroless/static:nonrootCOPY --from=builder /app/server /serverCOPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/USER nonroot:nonrootEXPOSE 8080ENTRYPOINT ["/server"]Have Claude Code add BuildKit cache mounts so repeat builds skip dependency downloads — the single biggest win for CI build time:
claude "Add BuildKit cache mounts to @Dockerfile so npm/go dependency downloadsare cached across builds. Use 'RUN --mount=type=cache' and add the'# syntax=docker/dockerfile:1' directive at the top. Explain the cache key."Codex can run the build and read the actual layer sizes back, then iterate — ask it to measure rather than estimate:
codex "Build this image, run 'docker history' to find the three largest layers,then rewrite the Dockerfile to shrink them. Re-build and confirm the new size."Docker Compose Orchestration
Section titled “Docker Compose Orchestration”Compose is where local-dev convenience and production correctness collide. Two rules the AI tends to get wrong unless you push: use depends_on with condition: service_healthy (not bare depends_on, which only waits for the container to start, not to be ready), and drop the top-level version: field — it’s obsolete in Compose V2 and only emits a warning.
services: api: build: ./api ports: - "8080:8080" environment: - DATABASE_URL=postgresql://postgres:${DB_PASSWORD}@postgres:5432/app - REDIS_URL=redis://redis:6379 depends_on: postgres: condition: service_healthy redis: condition: service_healthy networks: [app-network]
postgres: image: postgres:16-alpine environment: - POSTGRES_PASSWORD=${DB_PASSWORD:?set DB_PASSWORD} volumes: - postgres-data:/var/lib/postgresql/data networks: [app-network] healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 10s timeout: 5s retries: 5
redis: image: redis:7-alpine networks: [app-network] healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 5s retries: 5
networks: app-network: driver: bridge
volumes: postgres-data:For local development, keep a separate docker-compose.override.yml that adds hot-reload mounts and debug ports — Compose merges it automatically. Ask Claude Code to generate the override, not to mutate your production file:
claude "Create a docker-compose.override.yml that adds to my existing services:bind mounts for hot reload, NODE_ENV=development, the 9229 debugger port, and amailhog container for email testing. Don't touch the base compose file."Codex can validate the file before you commit it — docker compose config resolves variables and surfaces schema errors:
codex "Generate the compose file per my spec, then run 'docker compose config'to validate it parses with no warnings, and fix anything it flags."Security Hardening
Section titled “Security Hardening”A non-root user is table stakes. The next layer is a read-only root filesystem, dropped Linux capabilities, and a scan in the loop. Have the AI fix the Dockerfile and the runtime constraints, because some of these (read-only FS, dropped caps) live in the run command or Compose security_opt, not the image.
Cursor’s agent can run docker scout cves image:tag inline and then patch the specific advisories it finds — paste the scan output so it targets real vulnerabilities, not hypothetical ones.
FROM node:20-alpine AS productionRUN apk update && apk upgrade && rm -rf /var/cache/apk/*RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001WORKDIR /appCOPY --chown=nodejs:nodejs package*.json ./RUN npm ci --omit=dev && npm cache clean --forceCOPY --chown=nodejs:nodejs . .USER nodejsHEALTHCHECK --interval=30s --timeout=3s --retries=3 CMD node healthcheck.js || exit 1CMD ["node", "--max-old-space-size=512", "index.js"]Runtime constraints (Compose):
services: app: read_only: true tmpfs: - /tmp cap_drop: - ALL security_opt: - no-new-privileges:trueWire scanning into a pre-push hook so a vulnerable image never reaches the registry. Claude Code is the natural fit because hooks are its bread and butter:
claude "Write a git pre-push hook that runs 'docker scout cves --exit-code--only-severity critical,high' against the image we build, and blocks the pushif any critical or high CVE is found. Put it in .git/hooks/pre-push."For Docker secrets, never bake them into layers. Have Codex wire up file-based secrets and an entrypoint that loads them:
codex "Set up Docker Compose secrets for db_password and jwt_secret sourced fromfiles, expose them as *_FILE env vars, and write a POSIX docker-entrypoint.shthat reads each file into an env var then execs the main command. No secrets inthe image or in 'docker history'."CI/CD Integration
Section titled “CI/CD Integration”The payoff is a pipeline that builds multi-arch, scans, and deploys without a human. The most common copy-paste failure here is a GitHub Actions step that defines both uses: and run: — Actions rejects that with “a step cannot have both the uses and run keys.” Build/auth tooling goes in one step (uses:), the commands that consume it go in the next (run:).
name: Docker CI/CDon: push: branches: [main]env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }}jobs: build-and-test: runs-on: ubuntu-latest permissions: contents: read packages: write security-events: write steps: - uses: actions/checkout@v4 - uses: docker/setup-buildx-action@v3 - uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - id: meta uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | type=sha type=ref,event=branch - uses: docker/build-push-action@v5 with: context: . platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.meta.outputs.tags }} cache-from: type=gha cache-to: type=gha,mode=max - name: Trivy scan uses: aquasecurity/trivy-action@master with: image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }} format: sarif output: trivy-results.sarif - uses: github/codeql-action/upload-sarif@v3 with: sarif_file: trivy-results.sarif
deploy-staging: needs: build-and-test runs-on: ubuntu-latest steps: - name: Install doctl uses: digitalocean/action-doctl@v2 with: token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }} - name: Deploy run: | doctl kubernetes cluster kubeconfig save staging-cluster kubectl set image deployment/app app=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }} kubectl rollout status deployment/appClaude Code shines in headless CI — you can run it inside the pipeline to triage a failed scan:
claude -p "Read trivy-results.sarif. Summarize the critical and high findings,map each to the Dockerfile line that introduced it, and propose the minimalbase-image bump or package pin to clear them." --output-format jsonCodex’s GitHub integration can open the fix as a PR directly from a Cloud task, or you can drive it from the CLI. For a scripted SBOM-and-scan gate:
codex exec --full-auto "Add an SBOM generation step (docker buildx with--sbom=true) and a build provenance attestation to .github/workflows/docker-ci.yml,keeping the existing scan job intact."When This Breaks
Section titled “When This Breaks”Containers fail in ways that look like application bugs. These are the four that burn the most time, and the recovery move for each.