Skill v1.0.1
currentAutomated scan95/100+5 new
version: "1.0.1"
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 → compileStage 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.4FROM node:22-slim AS depsWORKDIR /appCOPY package.json package-lock.json ./RUN --mount=type=cache,target=/root/.npm npm ci --omit=devFROM node:22-slim AS builderWORKDIR /appCOPY package.json package-lock.json ./RUN --mount=type=cache,target=/root/.npm npm ciCOPY src ./srcRUN npm run buildFROM gcr.io/distroless/nodejs22-debian12WORKDIR /appCOPY --from=deps /app/node_modules ./node_modulesCOPY --from=builder /app/dist ./distEXPOSE 3000USER nonrootHEALTHCHECK --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+restartpath: ./srctarget: /app/srcdepends_on: [db]healthcheck:test: ["CMD", "curl", "-f", "http://localhost:3000/health"]db:image: postgres:16-alpinevolumes: ["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 scriptscripts/scan-image.sh registry/app:latest# Or manually:docker scout cves registry/app:latesttrivy 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.4FROM golang:1.24-alpine AS builderWORKDIR /appCOPY go.mod go.sum ./RUN --mount=type=cache,target=/go/pkg/mod go mod downloadCOPY . .RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /server ./cmd/serverFROM gcr.io/distroless/static-debian12COPY --from=builder /server /serverEXPOSE 8080USER nonrootHEALTHCHECK --interval=30s CMD ["/server", "-health"] || exit 1ENTRYPOINT ["/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.4FROM python:3.12-slim AS builderWORKDIR /appCOPY requirements.txt .RUN --mount=type=cache,target=/root/.cache/pip pip install --user -r requirements.txtFROM python:3.12-slimWORKDIR /appCOPY --from=builder /root/.local /root/.localCOPY src ./srcENV PATH=/root/.local/bin:$PATHEXPOSE 8000RUN useradd -m app && chown -R app /appUSER appHEALTHCHECK --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.4FROM rust:1.85-slim AS builderWORKDIR /appRUN 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.rsRUN --mount=type=cache,target=/usr/local/cargo/registry cargo build --release --target x86_64-unknown-linux-muslRUN rm -rf srcCOPY src ./srcRUN cargo build --release --target x86_64-unknown-linux-muslFROM scratchCOPY --from=builder /app/target/x86_64-unknown-linux-musl/release/server /serverEXPOSE 8080ENTRYPOINT ["/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 pipelinedocker build -t myapp:latest .trivy image --severity HIGH,CRITICAL --exit-code 1 myapp:latestdocker scout cves --exit-code myapp:latest# CI integration (GitHub Actions)- uses: aquasecurity/trivy-action@masterwith:image-ref: myapp:latestformat: sarifoutput: trivy-results.sarifseverity: HIGH,CRITICALexit-code: 1# Continuous monitoringdocker scout enroll myorg/myappdocker 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