BlogCI/CD

Dockerfile layer caching: order matters more than you think

Order your instructions so dependency layers stay cached and only your code layer rebuilds — seconds, not minutes.

Apr 8, 2025·4 min readBeginner·By the SecOpsLog team · command-tested

Docker does not rebuild from scratch every time — it reuses a layer cache until an instruction or its inputs change. Put COPY . . before RUN npm ci and every commit busts the dependency layer, reinstalling the internet for forty-five seconds of vanity. Put the lockfile first and only your code layer rebuilds; CI minutes and developer patience come back the same afternoon.

You will spot cache-busting order, rewrite a Node Dockerfile the fast way, and add a BuildKit cache mount so package downloads survive even when the RUN layer invalidates. For how layers are stored and squashed, see Docker in depth — caching is where that theory pays rent.

How a cached Docker build reuses layers

Each instruction creates a layer hash from its command plus the checksum of copied files. Change anything upstream and every downstream layer rebuilds.

1FROMbase layer (cached)2COPY lockfilehash stable across commits3RUN install depsCACHED until lock changes4COPY sourceonly this busts on code edits5RUN buildrecompiles app only6Cache mountnpm/apt store persists7Push imageregistry stores final layers

The slow pattern everyone writes first

Copying the entire build context early means any README tweak, test fixture, or .git object changes the checksum Docker uses for that layer. Everything below it — dependency install, asset compilation, security scans wired as RUN steps — runs again. In CI this shows up as identical npm ci logs on every push.

Dockerfile (slow)
FROM node:20-slim
WORKDIR /app
COPY . . # any file change busts cache below
RUN npm ci # reinstalls on every commit
RUN npm run build
CMD ["node", "dist/server.js"]

Lockfiles first, source last

Copy only manifest files that declare dependencies, install, then copy the rest. Python gets the same treatment with requirements.txt or pyproject.toml plus lock; Go uses go mod download after go.mod/go.sum. The volatile layer should be as late as possible.

Dockerfile (fast)
# syntax=docker/dockerfile:1
FROM node:20-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci
COPY . .
RUN npm run build
CMD ["node", "dist/server.js"]
bash — cache hit on a code-only changelive
DOCKER_BUILDKIT=1 docker build -t app .
=> CACHED [3/6] COPY package.json package-lock.json ./
=> CACHED [4/6] RUN npm ci
=> exporting to image — finished in 3.1s (was 48s)
only COPY . . and npm run build ran fresh

Cache mounts when the RUN layer must rebuild

Sometimes you must bust the RUN layer — a new env var, a changed build flag — but you still do not want to re-download npm's cache from npmjs.org. BuildKit's RUN --mount=type=cache keeps /root/.npm, /var/cache/apt, or /go/pkg/mod on the builder between builds even when the layer hash changes. Enable BuildKit in CI; it is default in recent Docker engines.

Add a .dockerignore that excludes .git, local node_modules, and test artifacts from the build context. Smaller contexts hash faster and prevent accidental cache busts when someone adds a large fixture file that the Dockerfile never needed. Treat context size as part of cache strategy, not an afterthought.

Cache discipline
Cache busting habits
COPY . . before installs
ARG before deps without defaults
Changing WORKDIR mid-file
No .dockerignore bloat
Cache friendly habits
Lockfiles copied alone
Multi-stage isolates compile
BuildKit cache mounts
Pin base images by digest
Cached layers can hide stale dependencies
A cache hit means Docker skipped the RUN step — it does not know npm advisory DB updated overnight. Still scan images in CI and rebuild without cache on schedule or when lockfiles change. Speed is not an excuse to ship known CVEs.

Where this goes next

Layer order is the free win; multi-stage and distroless shrink what you push after the build is fast. Wire BuildKit secrets for npm tokens, export SBOMs from the same pipeline, and treat cache keys in GitHub Actions or GitLab the same way — hash the lockfile, not the commit SHA alone.

Go deeper in a courseDocker in depthImages, layers, BuildKit, storage drivers, and engine internals.View course

Related posts