skills/dockerfile-build-cache-mastery/SKILL.md
Use when optimizing Docker builds with BuildKit, designing multi-stage Dockerfiles, leveraging cache mounts, building multi-arch images, choosing distroless vs alpine, or signing images with cosign. Triggers: docker build slow, layer cache not hitting, npm install / pip install reruns, multi-stage final image bloat, glibc vs musl issues, RUN --mount=type=cache, BUILDKIT_INLINE_CACHE, registry cache exporters, qemu emulation for arm64, COPY ordering for cache. NOT for Docker daemon admin, Kubernetes-specific image policies, container security scanning workflows, or Buildpacks (different paradigm).
npx skillsauth add curiositech/windags-skills dockerfile-build-cache-masteryInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
The Docker build is a series of hashable layers. Speed comes from invalidating as few as possible. Most "build is slow" stories are layer ordering mistakes that bust the cache on every commit.
flowchart TD
A[Docker build slow] --> B{BuildKit enabled?}
B -->|No| F1[FIX: DOCKER_BUILDKIT=1 or buildx]
B -->|Yes| C{Are deps copied before source?}
C -->|No, COPY . . first| F2[FIX: COPY lockfile → install → COPY source]
C -->|Yes| D{Cache mount on package manager dir?}
D -->|No| F3[FIX: RUN --mount=type=cache,target=/root/.npm]
D -->|Yes| E{Multi-stage build?}
E -->|No| F4[FIX: build stage + slim runtime stage]
E -->|Yes| G{CI cold builds slow?}
G -->|Yes| F5[FIX: --cache-to/--cache-from registry]
G -->|No| H{Image > 300MB or shipping build tools?}
H -->|Yes| F6[FIX: distroless or scratch runtime]
H -->|No| I{Building arm64 too?}
I -->|Via QEMU only| F7[FIX: native arm64 builder for compiled langs]
I -->|Native| J[Done]
Jump to your fire:
npm install reruns even when unchanged → Cache mountsdocker history → Secret mountsDOCKER_BUILDKIT=1 docker build .
# Or use buildx for multi-platform.
docker buildx build --platform linux/amd64,linux/arm64 -t myapp:1.0 --push .
BuildKit gives you parallel build steps, cache mounts, secret mounts, and SBOM generation. If you're not on BuildKit, none of the rest of this skill matters.
# WRONG — every code change rebuilds deps.
COPY . .
RUN npm install
# RIGHT — deps cached unless lockfile changes.
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
The cache key for each layer is (parent layer hash, instruction text, files referenced by COPY). Anything that invalidates parent layer hash cascades to all subsequent layers.
# syntax=docker/dockerfile:1.7
RUN --mount=type=cache,target=/root/.npm,sharing=locked \
npm ci
Cache mount survives across builds without becoming part of the image. For pnpm:
RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \
pnpm install --frozen-lockfile
For Go:
RUN --mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/go/pkg/mod \
go build -o /out/server ./cmd/server
sharing=locked is the safe default; private if the cache should be per-build.
# syntax=docker/dockerfile:1.7
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN --mount=type=cache,target=/root/.local/share/pnpm/store \
corepack enable && pnpm install --frozen-lockfile
FROM node:22-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN pnpm build
FROM gcr.io/distroless/nodejs22-debian12 AS runtime
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
USER nonroot
CMD ["dist/index.js"]
Final image excludes pnpm, source, dev deps, and the package manager itself. Distroless cuts the runtime to ~80MB.
docker buildx create --name multi --use
docker buildx build \
--platform linux/amd64,linux/arm64 \
--push \
-t ghcr.io/myorg/app:1.0 .
For native arm64 build performance, use a remote arm64 builder via Docker Build Cloud or a self-hosted runner. QEMU emulation works but is 5-10x slower for compiled languages.
# Push the cache as a separate manifest list to the registry.
docker buildx build \
--cache-to type=registry,ref=ghcr.io/myorg/app:cache,mode=max \
--cache-from type=registry,ref=ghcr.io/myorg/app:cache \
-t ghcr.io/myorg/app:1.0 \
--push .
CI machines start cold; pulling the registry cache is the difference between 10-min and 1-min builds.
mode=max exports cache for all stages; default exports only final-image layers.
| Image | Pros | Cons |
|-------|------|------|
| scratch | Smallest possible | No shell, no libc — only for static binaries (Go, Rust). |
| gcr.io/distroless/static | Tiny, no shell, glibc available | Same — static-friendly. |
| gcr.io/distroless/cc | Includes libgcc, libc++ | For C++/dynamically-linked compiled binaries. |
| gcr.io/distroless/nodejs22-debian12 | Node runtime + glibc | No shell — debugging via kubectl exec is harder. |
| alpine | Tiny, has shell + apk | musl instead of glibc — some binaries break. |
| debian-slim | glibc, has shell | Larger; slower pulls. |
For Node and Python, distroless gets you small + secure. For Go/Rust, scratch or static.
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
npm install
docker build --secret id=npmrc,src=$HOME/.npmrc .
The secret is mounted only for that RUN step and never lands in the image. Use for private registry tokens, npm auth.
.dockerignorenode_modules
.git
*.log
.env
dist
.vscode
*.tsbuildinfo
Without .dockerignore, COPY . . ships node_modules + .git + secrets to the daemon and into image-layer caching. Always present.
# Manifest first.
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
# Source second.
COPY src/ src/
COPY public/ public/
COPY tsconfig.json ./
RUN pnpm build
Don't COPY . . early. List the files needed for each step explicitly.
cosign sign --yes ghcr.io/myorg/app:1.0
In Kubernetes, an admission controller (cosigned, kyverno) verifies signatures before allowing pods. Supply-chain integrity at the image layer.
ARG VERSION
ENV APP_VERSION=$VERSION
LABEL org.opencontainers.image.version=$VERSION
docker build --build-arg VERSION=1.0.0 .
ARG is build-time only; ENV is runtime. Don't ARG secrets — they're visible in layer metadata.
COPY . . before npm installSymptom: Every code change rebuilds deps. Diagnosis: Cache invalidates on any source change. Fix: Copy lockfile, install, then copy source.
Symptom: Image >1GB; apt-get build-essential ships to production.
Diagnosis: No multi-stage.
Fix: Build in a stage, copy artifacts to a slim runtime stage.
.dockerignoreSymptom: Build context upload is 500MB; node_modules in the image.
Diagnosis: No .dockerignore.
Fix: Standard ignore list. git, node_modules, dist, *.log, .env.
--no-cache in CI by defaultSymptom: Builds always slow regardless of change.
Diagnosis: Someone added --no-cache to fix one flake; never removed.
Fix: Cache by default. Use --no-cache only for explicit "rebuild from scratch."
ENV or ARGSymptom: Secret visible in docker history or image inspect.
Diagnosis: ARG/ENV both persist in metadata.
Fix: Secret mounts (--mount=type=secret). Or runtime env injection.
Symptom: Binary works on Debian, segfaults on Alpine. Diagnosis: Alpine uses musl libc; some binaries assume glibc. Fix: Test on Alpine specifically. For Node native modules, install build deps. For Go, build with CGO_ENABLED=0 or pin a glibc-based base.
Scenario. Every PR runs CI for 14 minutes. The Dockerfile builds a Node monorepo (4 packages, ~600MB node_modules). Engineers are getting 4-5 builds queued, productivity tanking.
Novice would: Increase the CI runner size; bump from ubuntu-latest (4 vCPU) to ubuntu-latest-large (8 vCPU). Build time drops to 9 minutes — barely. Cost goes up 4x. The cache problem is unaddressed.
Expert catches:
docker build --progress=plain locally and read the cache-hit lines. First clue: [2/8] COPY . . invalidates on every commit. Second clue: no --mount=type=cache, so pnpm re-downloads packages every build.pnpm-lock.yaml first, install with --mount=type=cache,target=/root/.local/share/pnpm/store, then COPY . .. Local rebuild on small change drops from 8 min → 40 sec.--cache-to type=registry,ref=ghcr.io/org/app:cache,mode=max + matching --cache-from. Cold CI now pulls cache layers, build drops from 14 min → 2 min.deps → build → distroless runtime gets it to 220MB.hyperfine --warmup 1 'docker buildx build ...' ten times locally to confirm cache hits stable.Timeline. Novice spends $400/mo more on CI runners and the queue still backs up. Expert spends a 4-hour afternoon redoing the Dockerfile and CI cache wiring; build drops from 14 min → 2 min, image from 1.2GB → 220MB, monthly CI cost flat. The pattern then propagates across the org's other 12 services because the Dockerfile is small and copyable.
docker build from a clean cache, then again from a no-op commit; second build completes in ≤ 30 sec (cache works).--progress=plain log).DOCKER_BUILDKIT=1 set in CI env)..dockerignore present and excludes node_modules, .git, .env, dist. Verified: du -sh $(docker build --progress=plain . 2>&1 | grep 'transferring context') ≤ 5MB for typical apps.pnpm, gcc, apt, dev dependencies present.docker images --format 'table {{.Repository}}\t{{.Size}}' in CI.COPY order: manifest → install → source → build (verified by reading the Dockerfile in code review).--mount=type=cache) for the package manager. Grep for --mount=type=cache in CI passes.--cache-to/--cache-from). Verified by build time on a no-op PR ≤ 3 min.docker history --no-trunc <image> grepped for known-secret patterns; CI fails on hit.USER nonroot (or numeric UID) in runtime stage. Verified by docker inspect <image> | jq '.[0].Config.User' ≠ "" and ≠ "root".cosign verify succeeds in admission controller).docker buildx imagetools inspect <ref> shows both platforms.kubernetes-debugging-runbook for cluster-side issues.github-actions-matrix-patterns.data-ai
license: Apache-2.0 NOT for unrelated tasks outside this domain.
development
Use when designing caching strategies (cache-aside, write-through, write-behind), implementing distributed locks, building rate limiters, leaderboards, real-time streams (XADD/consumer groups), pub/sub, or tuning eviction policies. Triggers: thundering-herd on cache miss, dogpile on key expiry, Redlock vs SET-NX-PX choice, sliding-window rate limiter, hot-key on a single cluster slot, big-key blowup, MULTI/EXEC across slots, KEYS in production. NOT for Redis Cluster operations/admin (different domain), embedded KV (SQLite, leveldb), in-process LRU caches, or Memcached.
tools
Drawing the `'use client'` boundary correctly in React Server Components apps (Next.js App Router, RSC frameworks) — leaf-pushing, slot composition, serialization rules, and environment poisoning prevention. Grounded in react.dev and Next.js 16 docs.
development
Use when designing rate limiting for an API, choosing between token bucket / sliding window / leaky bucket / fixed window, implementing it in Redis, deciding edge (Cloudflare/Upstash) vs origin enforcement, sizing per-user vs per-IP vs per-endpoint quotas, returning the right 429 response with Retry-After, or fixing the boundary-burst bug in fixed-window limiters. Triggers: 429 too many requests, INCR + EXPIRE, ZADD + ZREMRANGEBYSCORE + ZCARD, X-RateLimit-Remaining header, Cloudflare WAF rate limiting rules, Upstash @upstash/ratelimit, leaky bucket shaping vs policing, distributed rate limiter consistency. NOT for DDoS mitigation specifically (different scale), CAPTCHA / bot management, full WAF design, or per-user quota billing.