--- name: docker description: Optimize Docker images with multi-stage builds, distroless bases, BuildKit cache mounts, multi-arch builds, compose watch, security hardening (non-root, seccomp, capabilities drop), and vulnerability scanning via docker scout/trivy. Use when user asks to write a Dockerfile, optimize image size, set up docker-compose, debug containers, harden container security, or scan for CVEs. Do NOT use for Kubernetes deployments (use kubernetes), CI/CD pipeline design (use ci-cd), or Terraform (use terraform). triggers: - "write a Dockerfile" - "dockerize" - "containerize" - "optimize image" - "multi-stage build" - "docker-compose" - "docker build" - "Dockerfile" - "docker image" - "docker container" - "docker compose" - "docker scout" - "docker security" - "container security" negatives: - "Kubernetes" - "CI/CD pipeline" - "Terraform" - "K8s" - "deploy to cluster" license: MIT compatibility: opencode metadata: workflow: infrastructure audience: devops version: "3.0.0" author: shokunin allowed-tools: Read Bash Write
Docker Architect
Production-grade Dockerfiles, multi-stage builds, cache optimization, security scanning, and local development. Applies Google's distroless philosophy and Docker BuildKit best practices.
Decision Framework
Before containerizing, answer:
- Does the app need process isolation? → Docker
- Will it deploy to Kubernetes? → Docker + distroless + non-root
- Is it a monolith with simple deployment? → Docker Compose
- Is it a static site? → Consider nginx:alpine single-stage
- Is the team already using Docker Compose in dev? → Start there, add K8s when needed
- Is the app latency-sensitive (sub-ms)? → Bare metal or VM; container overhead matters at extreme scale
Workflow
Quick start: docker init
For new projects, run docker init in the project root. It auto-detects the language/framework and generates a Dockerfile, .dockerignore, and compose.yaml with best-practice defaults. Always review and harden the output — the generated files are a starting point, not production-ready.
Step 1: Identify stack and choose template
| Stack | Base image | Build stage | Runtime |
|---|---|---|---|
| Node.js | node:22-slim | Full SDK | gcr.io/distroless/nodejs |
| Go | golang:1.23-alpine | Full SDK | scratch |
| Python | python:3.12-slim | Full SDK | python:3.12-slim |
| Rust | rust:1.78-slim | Full SDK | gcr.io/distroless/cc |
Decision: If the stack is listed above, use the corresponding production Dockerfile below. If not, apply the golden template in Step 2.
Step 2: Apply golden template
Use multi-stage with this exact structure:
Stage 1 (deps): COPY lock files → install production deps (--mount=type=cache)
Stage 2 (build): COPY source → compile
Stage 3 (runtime): minimal base → COPY artifacts from stages 1-2 → USER nonroot → HEALTHCHECK
If the project is a Go binary, skip Stage 1 (Go has no runtime deps) and go straight to Stage 2.
If the project has native dependencies (node-gyp, C extensions), use apt-get in the builder stage, NOT the runtime stage.
Step 3: Apply BuildKit optimizations
# syntax=docker/dockerfile:1.4
FROM node:22-slim AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci --omit=dev
FROM node:22-slim AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
COPY src ./src
RUN npm run build
FROM gcr.io/distroless/nodejs22-debian12
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
EXPOSE 3000
USER nonroot
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD ["node", "-e", "require('http').get('http://localhost:3000/health', r => process.exit(r.statusCode===200?0:1))"]
CMD ["dist/index.js"]
Run scripts/optimize-dockerfile.sh on any existing Dockerfile to receive optimization suggestions.
Step 4: Configure compose for local dev
services:
app:
build: .
ports: ["3000:3000"]
develop:
watch:
- action: sync+restart
path: ./src
target: /app/src
depends_on: [db]
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
db:
image: postgres:16-alpine
volumes: ["pgdata:/var/lib/postgresql/data"]
volumes: { pgdata: }
Run docker compose watch for hot-reload.
See assets/docker-compose.template.yml for the full template with all services.
Step 5: Build for multiple platforms
docker buildx build \
--platform linux/amd64,linux/arm64 \
--cache-from=type=gha \
--cache-to=type=gha,mode=max \
--tag registry/app:latest \
--push .
See references/multi-arch.md for QEMU setup and platform-specific optimizations.
Step 6: Scan for vulnerabilities
# Using the provided script
scripts/scan-image.sh registry/app:latest
# Or manually:
docker scout cves registry/app:latest
trivy image registry/app:latest
If critical CVEs are found: either switch base image (e.g., distroless), or add apt-get to install patched deps in builder stage.
Error Handling
| Error | Cause | Fix |
|---|---|---|
failed to solve with frontend dockerfile.v0 |
Missing syntax directive | Add # syntax=docker/dockerfile:1.4 as first line |
exec /usr/bin/node: exec format error |
Wrong platform | Build with --platform linux/amd64 matching the target |
permission denied at runtime |
Missing USER nonroot or wrong file permissions |
Add USER nonroot and COPY --chown=nonroot:nonroot |
| Layer cache miss every build | Changing files copied before lock files | Always COPY package.json BEFORE source code |
docker compose watch not working |
Docker Engine < 24 | Upgrade Docker Engine or use docker compose up --watch |
Pre-Flight Checklist
Before deploying a Docker image:
-
.dockerignoreexists and excludesnode_modules/,.git/,.env*,Dockerfile*,*.log - Multi-stage build with separate build and runtime stages
- Runtime stage uses distroless or minimal base (
node:22.14-slim, notnode:22) -
USER nonroot(or equivalent) — never runs as root -
HEALTHCHECKdefined with appropriate interval - Secrets use
--mount=type=secret, neverENVorARG -
npm ci --omit=dev(or language equivalent) for production dependencies -
docker scout quickviewortrivy imagescan passes with zero HIGH/CRITICAL CVEs - Image size verified:
docker images --format "{{.Size}}"— should be <200MB for most apps -
docker compose watchtested in development (sync+restart for code changes) - Container starts and passes healthcheck within 30 seconds
- Logs go to stdout/stderr (no log files inside container)
-
docker compose downanddocker compose upsuccessfully recreates from scratch
Anti-Patterns
| Pattern | Problem | Fix | Because |
|---|---|---|---|
| Single-stage build | Final image contains build tools, SDKs, source code — 5x larger | Multi-stage: build stage → distroless runtime | Every tool in the image is an attack surface. Minimize blast radius. |
COPY . . before npm install |
Cache miss on every code change, full rebuild | Copy package files first, install deps, then copy source | Docker caches by layer. Source changes should invalidate only the last COPY. |
latest tag |
Image changes silently on pull | Pin full version tag (22.14-slim, not 22-slim) |
latest means "whatever was pushed last". A patch update can break your app. |
| Root user in container | Compromised process = host root access | USER nonroot with distroless or RUN useradd |
Container escape bugs exist. Non-root limits damage to the container. |
| Secrets in build args | docker history reveals them. BuildKit --secret exists for this |
RUN --mount=type=secret in BuildKit |
Build args are stored in image metadata. Anyone with image access can extract them. |
No .dockerignore |
Copies .env, .git/, node_modules/ into build context |
Add .dockerignore with common exclusions |
200MB of node_modules in build context = slow builds + potential secret leaks. |
| No healthcheck | Orchestrator can't detect app failures | HEALTHCHECK --interval=30s CMD curl -f http://localhost/health |
Without healthcheck, Swarm/K8s only detects process crashes, not app hangs. |
npm install in production |
Installs devDependencies (testing frameworks, linters, TypeScript) | npm ci --omit=dev or npm ci --production |
Dev deps add 100-200MB to the image. They also increase CVEs from unused packages. |
Pinning only major version (22-slim) |
Can auto-update to a new minor version that breaks your app | Pin to exact version (22.14-slim) or use digest pinning |
Reproducibility: the same Dockerfile should produce the same image every time. |
| Multi-stage with wrong base | Runtime stage uses node instead of distroless | Use gcr.io/distroless/nodejs22-debian12 or node:22.14-slim |
Distroless removes shells, package managers, and utilities — nothing for an attacker to exploit. |
Review Format (Required)
When reviewing Dockerfiles, use Before | After | Why format:
| Before | After | Why |
|---|---|---|
FROM node:22-slim |
FROM node:22.14-slim@sha256:abc... |
Floating tags (22-slim) auto-update. Pin to immutable digest for reproducibility. |
COPY . . before npm ci |
COPY package*.json ./ then npm ci then COPY . . |
Docker caches each COPY layer. Copying source before deps invalidates cache on every code change. |
CMD ["npm", "start"] |
Use node server.js directly |
Avoids npm overhead in production. Use process manager (dumb-init, tini) for signal forwarding. |
No .dockerignore |
.dockerignore with node_modules/, .git/, *.log, Dockerfile* |
Reduces build context size by 60-90%. Prevents leaking secrets from local .env. |
Go multi-stage template
# syntax=docker/dockerfile:1.4
FROM golang:1.24-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /server ./cmd/server
FROM gcr.io/distroless/static-debian12
COPY --from=builder /server /server
EXPOSE 8080
USER nonroot
HEALTHCHECK --interval=30s CMD ["/server", "-health"] || exit 1
ENTRYPOINT ["/server"]
Key decisions:
CGO_ENABLED=0for static binary (no glibc dependency)-ldflags="-s -w"strips debug symbols (reduces binary by 30%)gcr.io/distroless/static-debian12for CA certs + timezone data (needed for HTTPS/TLS)- If the binary needs nothing: use
scratch(smaller, but no CA certs)
Python multi-stage template
# syntax=docker/dockerfile:1.4
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip pip install --user -r requirements.txt
FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /root/.local /root/.local
COPY src ./src
ENV PATH=/root/.local/bin:$PATH
EXPOSE 8000
RUN useradd -m app && chown -R app /app
USER app
HEALTHCHECK --interval=30s CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:8000", "src.main:app"]
Key decisions:
pip install --useravoids polluting /usr/local in buildergunicornwith multiple workers handles concurrent requestsuseraddcreates a non-root user (distroless Python not available)
Rust multi-stage template
# syntax=docker/dockerfile:1.4
FROM rust:1.85-slim AS builder
WORKDIR /app
RUN apt-get update && apt-get install -y musl-tools && rm -rf /var/lib/apt/lists/*
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs
RUN --mount=type=cache,target=/usr/local/cargo/registry cargo build --release --target x86_64-unknown-linux-musl
RUN rm -rf src
COPY src ./src
RUN cargo build --release --target x86_64-unknown-linux-musl
FROM scratch
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/server /server
EXPOSE 8080
ENTRYPOINT ["/server"]
Key decisions:
musl-toolsfor static linking (no glibc dependency)- Dummy main.rs trick: compiles deps first, then source (cache deps)
scratchbase: smallest possible, no shell, no tools
Seccomp profiles
Docker applies a default seccomp profile that blocks 44/300+ syscalls. Customize for your app:
{
"defaultAction": "SCMP_ACT_ERRNO",
"architectures": ["SCMP_ARCH_X86_64"],
"syscalls": [
{ "names": ["read","write","open","close","fstat","mmap","mprotect","munmap","brk","rt_sigaction","rt_sigprocmask","rt_sigreturn","ioctl","pread64","pwrite64","readv","writev","access","pipe","select","sched_yield","mremap","msync","mincore","madvise","shmget","shmat","shmctl","dup","dup2","pause","nanosleep","getitimer","setitimer","alarm","getpid","sendfile","socket","connect","accept","sendto","recvfrom","sendmsg","recvmsg","shutdown","bind","listen","getsockname","getpeername","socketpair","setsockopt","getsockopt","clone","fork","vfork","execve","exit","wait4","kill","uname","semget","semop","semctl","shmdt","msgget","msgsnd","msgrcv","msgctl","fcntl","flock","fsync","fdatasync","truncate","ftruncate","getdents","getcwd","chdir","fchdir","rename","mkdir","rmdir","creat","link","unlink","symlink","readlink","chmod","fchmod","chown","fchown","lchown","umask","gettimeofday","getrlimit","getrusage","sysinfo","times","preadv","pwritev","rt_sigtimedwait","futex","set_robust_list","get_robust_list","epoll_wait","epoll_ctl","epoll_create","epoll_pwait","epoll_create1","eventfd","signalfd","timerfd_create","timerfd_gettime","timerfd_settime","prctl","getcpu","process_vm_readv","process_vm_writev"], "action": "SCMP_ACT_ALLOW" }
]
}
Usage: docker run --security-opt seccomp=profile.json myapp
CVE scanning pipeline
# Full scan pipeline
docker build -t myapp:latest .
trivy image --severity HIGH,CRITICAL --exit-code 1 myapp:latest
docker scout cves --exit-code myapp:latest
# CI integration (GitHub Actions)
- uses: aquasecurity/trivy-action@master
with:
image-ref: myapp:latest
format: sarif
output: trivy-results.sarif
severity: HIGH,CRITICAL
exit-code: 1
# Continuous monitoring
docker scout enroll myorg/myapp
docker scout watch myapp:latest
If CVEs found: switch base image to newer distroless tag, rebuild, re-scan. Track with docker scout recommendations.
Sources
- Dockerfile best practices (docs.docker.com)
- BuildKit documentation
- Google distroless images
- Trivy vulnerability scanner
- Docker Scout documentation
- SLSA framework (slsa.dev)
Checklist
- Skill loads without errors in the AI agent
- YAML frontmatter is valid (description, compatibility, audience)
- Workflow section provides clear step-by-step instructions
- Error handling section covers common failure modes
- All referenced files (references/, scripts/, assets/) exist
- Skill triggers correctly for intended use cases
- No broken links or missing resources