Skip to content

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.

  • 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_on health conditions instead of sleep hacks.
  • 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.

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 biggest
wins for size and security before changing anything.

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 .dockerignore and a HEALTHCHECK.
  • Combine RUN layers and clean the package-manager cache in the same layer.

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 stage
FROM node:20-alpine AS builder
RUN apk add --no-cache python3 make g++
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production stage
FROM node:20-alpine AS production
RUN apk add --no-cache dumb-init
RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
USER nodejs
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node healthcheck.js
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "dist/index.js"]

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 stage
FROM golang:1.24-alpine AS builder
RUN apk add --no-cache git ca-certificates
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o server ./cmd/server
FROM gcr.io/distroless/static:nonroot
COPY --from=builder /app/server /server
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/server"]

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:

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 production
RUN apk update && apk upgrade && rm -rf /var/cache/apk/*
RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
WORKDIR /app
COPY --chown=nodejs:nodejs package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --chown=nodejs:nodejs . .
USER nodejs
HEALTHCHECK --interval=30s --timeout=3s --retries=3 CMD node healthcheck.js || exit 1
CMD ["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:true

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/CD
on:
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/app

Containers fail in ways that look like application bugs. These are the four that burn the most time, and the recovery move for each.