Home DevOps Docker Layer Caching: Stop Rebuilding Dependencies Every CI Run
Intermediate 7 min · July 11, 2026

Docker Layer Caching: Stop Rebuilding Dependencies Every CI Run

Docker layer caching explained: why your builds are slow, how caching works, and the exact Dockerfile patterns to cut CI time by 80%..

N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Notes here come from systems that actually shipped.

Follow
Production
production tested
July 11, 2026
last updated
258
articles · all by Naren
Before you start⏱ 25 min
  • Basic Dockerfile syntax (FROM, RUN, COPY, CMD)
  • Familiarity with docker build and docker push/pull commands
  • Understanding of CI/CD pipelines (GitHub Actions, Jenkins, etc.)
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer

Docker caches each instruction's result as a layer. If the instruction and its context (e.g., file contents) haven't changed, Docker reuses the cached layer instead of re-executing. To maximize cache hits, put infrequently changing steps (like installing OS packages) early in the Dockerfile, and frequently changing steps (like copying source code) late.

✦ Definition~90s read
What is Docker Layer Caching?

Docker layer caching is the mechanism that reuses intermediate filesystem snapshots (layers) from previous builds when the instructions and context haven't changed. It's what makes docker build fast after the first run — and what breaks silently when you order your Dockerfile wrong.

Imagine you're baking a layered cake.
Plain-English First

Imagine you're baking a layered cake. Each layer takes time to bake. If you bake the same bottom layer again tomorrow, you don't throw away yesterday's bottom layer — you reuse it and only bake the new top layers. Docker does the same: it reuses unchanged layers and only rebuilds the ones that changed. But if you change the recipe for the bottom layer (like a different flour), you have to rebake everything above it too.

You just pushed a one-line comment change to your README. Your CI pipeline kicks off. Ten minutes later, you're still waiting for Docker to reinstall every npm package from scratch. That's not just frustrating — it's burning money on compute time and killing developer velocity. The culprit? A Dockerfile that ignores layer caching.

Docker's build cache is the single most impactful lever for speeding up container builds. When used correctly, it can turn a 10-minute build into a 2-minute one. When ignored, it punishes every commit with unnecessary rebuilds of dependencies that haven't changed. This isn't a nice-to-have optimization — it's the difference between a CI pipeline that keeps up with your team and one that becomes the bottleneck.

By the end of this article, you'll be able to write Dockerfiles that maximize cache hits, diagnose why a cache is being invalidated, and set up multi-stage builds that keep your final images small without sacrificing build speed. You'll also know the edge cases where caching works against you — and how to avoid them.

How Docker Build Cache Actually Works

Every Dockerfile instruction creates a layer — a read-only snapshot of the filesystem at that point. When you run docker build, Docker checks if it has a cached layer matching the instruction and its context (the files copied, the command string, etc.). If the instruction text is identical and the context hasn't changed, Docker reuses the cached layer. Otherwise, it executes the instruction and all subsequent instructions are rebuilt.

This means the order of instructions is everything. Put stable, infrequently changing steps first: OS package installs, language runtime installs, dependency downloads. Put frequently changing steps last: copying application source code, running tests, building assets.

Here's the critical detail most tutorials skip: Docker uses the instruction string AND the checksum of the build context (files copied via COPY/ADD) to determine cache validity. If you COPY . ., any change to any file in the context directory invalidates that layer — and every layer after it. That's why separating dependency manifests from source code is so powerful.

Dockerfile.cache-optimizedDEVOPS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// io.thecodeforge — DevOps tutorial

# Stage 1: Base — rarely changes
FROM node:18-alpine AS base
WORKDIR /app

# Copy only dependency manifests first
COPY package.json package-lock.json ./

# Install dependencies (cached unless manifests change)
RUN npm ci --only=production

# Stage 2: Build — changes frequently
FROM base AS build
COPY . .
RUN npm run build

# Stage 3: Production — minimal image
FROM node:18-alpine AS production
WORKDIR /app
COPY --from=base /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/index.js"]
Output
Step 1/12 : FROM node:18-alpine AS base
---> a1b2c3d4e5f6
Step 2/12 : WORKDIR /app
---> Using cache
---> b2c3d4e5f6a1
Step 3/12 : COPY package.json package-lock.json ./
---> Using cache
---> c3d4e5f6a1b2
Step 4/12 : RUN npm ci --only=production
---> Using cache
---> d4e5f6a1b2c3
Step 5/12 : FROM base AS build
---> d4e5f6a1b2c3
Step 6/12 : COPY . .
---> e5f6a1b2c3d4
Step 7/12 : RUN npm run build
---> Running in 1234567890ab
... (build output)
Removing intermediate container 1234567890ab
---> f6a1b2c3d4e5
...
Senior Shortcut:
Use docker build --cache-from in CI to pull a previously built image as a cache source. This is essential when your CI runner doesn't share a local cache between builds.

The COPY Order Trap: Why Your Cache Is Always Missing

The most common cache invalidation mistake is a single COPY . . early in the Dockerfile. Every time any file changes — even a README — Docker sees a different checksum for the context and invalidates that layer. Since all subsequent layers depend on it, the entire build after that point is redone.

The fix is brutally simple: copy only the files that change infrequently first, then copy the rest later. For a Node.js app, that means:

  1. COPY package.json and package-lock.json
  2. RUN npm install
  3. COPY the rest of the source code

The same pattern applies to Python (requirements.txt), Go (go.mod, go.sum), Java (pom.xml or build.gradle), and every other ecosystem. The dependency manifest changes only when you add/remove a dependency — which is far less often than source code changes.

But there's a subtler trap: using COPY . . even after installing dependencies still invalidates the layer if any file in the context changes. That's fine — you want that layer to rebuild when source changes. The key is that the npm install layer before it remains cached.

Dockerfile.bad-orderDEVOPS
1
2
3
4
5
6
7
8
9
// io.thecodeforge — DevOps tutorial

# BAD: COPY everything first, then install
FROM node:18-alpine
WORKDIR /app
COPY . .  # Any file change invalidates this layer
RUN npm ci --only=production  # Always rebuilds
EXPOSE 3000
CMD ["node", "index.js"]
Output
Step 1/4 : FROM node:18-alpine
---> a1b2c3d4e5f6
Step 2/4 : WORKDIR /app
---> Using cache
---> b2c3d4e5f6a1
Step 3/4 : COPY . .
---> c3d4e5f6a1b2 # Cache miss on every change
Step 4/4 : RUN npm ci --only=production
---> Running in ... # Always runs
...
Production Trap:
If you use ADD with a URL or git repository, Docker does NOT cache based on the content — only the URL string. The layer will be rebuilt every time, even if the remote content hasn't changed. Use RUN curl ... instead, and manage caching manually.

Multi-Stage Builds: Speed Without Bloat

Multi-stage builds let you use multiple FROM statements in a single Dockerfile. Each stage can have a different base image. You can copy artifacts from earlier stages into later ones. This is the standard pattern for keeping final images small while still having a full build environment.

But multi-stage builds also affect caching. Each stage is cached independently. If you change source code, only the stages that COPY that source code are invalidated. The base stage with OS packages and the dependency install stage remain cached.

Here's the pattern I use in production: a base stage with the runtime and system dependencies, a build stage with dev dependencies and source code, and a production stage that copies only the runtime artifacts. The base stage almost never changes. The build stage changes frequently but is thrown away. The production stage is rebuilt from cached layers every time.

One gotcha: if you use COPY --from=build in the production stage, that COPY instruction's cache depends on the checksum of the source stage's filesystem. If the build stage produces different files (e.g., a new build hash), the COPY layer in production is invalidated. That's expected — you want the new artifact.

Dockerfile.multistageDEVOPS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// io.thecodeforge — DevOps tutorial

# Stage 1: Base — system deps, rarely changes
FROM node:18-alpine AS base
RUN apk add --no-cache tini

# Stage 2: Dependencies — cached unless package.json changes
FROM base AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production

# Stage 3: Build — rebuilds on source changes
FROM deps AS build
COPY . .
RUN npm run build

# Stage 4: Production — minimal final image
FROM node:18-alpine AS production
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY --from=base /sbin/tini /sbin/tini
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "dist/index.js"]
Output
Step 1/... : FROM node:18-alpine AS base
---> a1b2c3d4e5f6
Step 2/... : RUN apk add --no-cache tini
---> Using cache
---> b2c3d4e5f6a1
Step 3/... : FROM base AS deps
---> b2c3d4e5f6a1
Step 4/... : WORKDIR /app
---> Using cache
Step 5/... : COPY package.json package-lock.json ./
---> Using cache
Step 6/... : RUN npm ci --only=production
---> Using cache
Step 7/... : FROM deps AS build
---> c3d4e5f6a1b2
Step 8/... : COPY . .
---> d4e5f6a1b2c3 # Cache miss on source change
Step 9/... : RUN npm run build
---> Running in ...
...
Interview Gold:
Multi-stage builds also improve security: the final image doesn't contain build tools, compilers, or source code. If an attacker gains access to the container, they can't easily reverse-engineer your application.

Using BuildKit for Faster, Smarter Caching

Docker's classic builder has a cache that's tied to the local machine. If you switch machines or run in CI, the cache is empty. BuildKit (enabled by default in Docker 23.0+) introduces a more sophisticated cache system that can be exported and imported.

BuildKit's cache can be stored in a registry (inline with the image or as a separate cache manifest), on a local filesystem, or in cloud storage (S3, GCS, Azure Blob). This means your CI runners can share a cache, even if they're ephemeral.

To use registry caching, pass --cache-from and --cache-to flags:

``bash docker buildx build --cache-from=type=registry,ref=myregistry.com/myapp:cache \ --cache-to=type=registry,ref=myregistry.com/myapp:cache,mode=max \ -t myregistry.com/myapp:latest . ``

The mode=max flag caches every layer, not just those exported to the final image. This gives the best cache hit rate but uses more storage.

One caveat: BuildKit's cache export is still experimental in some configurations. Test it in your CI before relying on it.

build-cache-commands.shDEVOPS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// io.thecodeforge — DevOps tutorial

# Build with cache from registry, push cache back
docker buildx build \
  --cache-from=type=registry,ref=registry.example.com/myapp:cache \
  --cache-to=type=registry,ref=registry.example.com/myapp:cache,mode=max \
  -t registry.example.com/myapp:latest \
  --push .

# On a fresh CI runner, pull cache before building
docker pull registry.example.com/myapp:cache || true
docker buildx build \
  --cache-from=type=registry,ref=registry.example.com/myapp:cache \
  --cache-to=type=registry,ref=registry.example.com/myapp:cache,mode=max \
  -t registry.example.com/myapp:$CI_COMMIT_SHA .
Output
# First build on fresh runner:
#1 [internal] load build definition from Dockerfile
#1 sha256:...
#2 [internal] load metadata for docker.io/library/node:18-alpine
#3 [auth] library/node:pull token for registry.example.com
#4 [internal] load .dockerignore
#5 [internal] load build context
#6 [base 1/2] FROM docker.io/library/node:18-alpine@sha256:...
#7 [base 2/2] RUN apk add --no-cache tini
#8 [deps 1/3] WORKDIR /app
#9 [deps 2/3] COPY package.json package-lock.json ./
#10 [deps 3/3] RUN npm ci --only=production
#11 [build 1/2] COPY . .
#12 [build 2/2] RUN npm run build
#13 [production 1/4] WORKDIR /app
#14 [production 2/4] COPY --from=deps /app/node_modules ./node_modules
#15 [production 3/4] COPY --from=build /app/dist ./dist
#16 [production 4/4] COPY --from=base /sbin/tini /sbin/tini
#17 exporting to image
#18 exporting layers
#19 pushing manifest for registry.example.com/myapp:cache
# Second build (same source): all layers cached from registry
#1 [internal] load build definition from Dockerfile
#2 [internal] load metadata for docker.io/library/node:18-alpine
#3 [internal] load .dockerignore
#4 [internal] load build context
#5 [base 1/2] FROM docker.io/library/node:18-alpine@sha256:...
#6 [base 2/2] RUN apk add --no-cache tini # CACHED
#7 [deps 1/3] WORKDIR /app # CACHED
#8 [deps 2/3] COPY package.json package-lock.json ./ # CACHED
#9 [deps 3/3] RUN npm ci --only=production # CACHED
#10 [build 1/2] COPY . . # CACHED
#11 [build 2/2] RUN npm run build # CACHED
#12 [production 1/4] WORKDIR /app # CACHED
#13 [production 2/4] COPY --from=deps /app/node_modules ./node_modules # CACHED
#14 [production 3/4] COPY --from=build /app/dist ./dist # CACHED
#15 [production 4/4] COPY --from=base /sbin/tini /sbin/tini # CACHED
#16 exporting to image
#17 exporting layers
#18 pushing manifest for registry.example.com/myapp:cache
Never Do This:
Don't use --cache-from with a stale cache tag that points to an old image. The cache layers must match the current Dockerfile instructions. If you change the base image or reorder instructions, the old cache is useless and may cause confusing errors.

When Caching Breaks: Common Invalidation Scenarios

Even with a perfect Dockerfile, cache invalidation can happen for reasons outside your control. Here are the ones I've seen burn teams:

  1. Base image tags that move: If you use node:18 (without a specific digest), the tag can update to a new image. Docker sees a different base image checksum and invalidates everything. Always pin to a digest: node:18@sha256:abc123....
  2. Package manager lock files: If you don't commit your lock file (package-lock.json, yarn.lock, Gemfile.lock), the COPY instruction sees different content every time a dependency is resolved. Always commit lock files.
  3. Build arguments: ARG and --build-arg values are part of the cache key. If you pass a build arg that changes (like a version number), it invalidates the layer that uses it and all subsequent layers. Use build args sparingly.
  4. Timestamps in build output: If your build process embeds timestamps (e.g., new Date() in a config file), the output changes every build, invalidating the layer. Strip timestamps or use deterministic builds.
  5. Network-dependent instructions: RUN apt-get update fetches package lists that change daily. Even if the instruction is the same, the downloaded content differs. Consider pinning package versions or using a proxy cache.
Dockerfile.pinned-baseDEVOPS
1
2
3
4
5
6
7
8
9
// io.thecodeforge — DevOps tutorial

# Pin base image to a specific digest for reproducible builds
FROM node:18@sha256:abc123def456abc123def456abc123def456abc123def456abc123def456abc1
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production
COPY . .
CMD ["node", "index.js"]
Output
Step 1/5 : FROM node:18@sha256:abc123def456abc123def456abc123def456abc123def456abc123def456abc1
---> a1b2c3d4e5f6
Step 2/5 : WORKDIR /app
---> Using cache
...
Senior Shortcut:
Run docker pull node:18 periodically to get security updates, then update the digest in your Dockerfile. Automate this with Dependabot or Renovate.

Cache Mounts: Speed Up Package Installs Without Layers

Sometimes you want to cache data that changes frequently but shouldn't create new layers. For example, the npm cache directory (~/.npm) or the Maven local repository (~/.m2). Using a cache mount with BuildKit lets you persist these directories across builds without bloating your image.

Cache mounts are specified with --mount=type=cache,target=<path>. They're not part of the image — they're stored on the host (or in a named volume in CI). This means:

  • The cache survives between builds
  • It doesn't increase image size
  • It's not included in the final image layers

``dockerfile RUN --mount=type=cache,target=/root/.npm \ npm ci --only=production ``

This caches downloaded packages in /root/.npm. On subsequent builds, npm skips downloading packages that are already cached. The layer itself is still cached based on the instruction and context, but the cache mount makes the execution faster.

One warning: cache mounts are not automatically invalidated when dependencies change. If you update a package version, the new version will be downloaded and cached alongside the old one. Over time, the cache directory can grow large. Periodically clear it with docker builder prune --filter type=exec.cachemount.

Dockerfile.cache-mountDEVOPS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// io.thecodeforge — DevOps tutorial

FROM node:18-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./

# Use a cache mount for npm's cache directory
# This speeds up npm install when packages are already downloaded
RUN --mount=type=cache,target=/root/.npm \
    npm ci --only=production

FROM node:18-alpine AS production
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
CMD ["node", "index.js"]
Output
# First build: downloads packages, caches them
#9 [deps 3/3] RUN --mount=type=cache,target=/root/.npm npm ci --only=production
#9 ... downloading packages...
#9 DONE 12.3s
# Second build: uses cached packages, much faster
#9 [deps 3/3] RUN --mount=type=cache,target=/root/.npm npm ci --only=production
#9 ... using cache...
#9 DONE 1.2s
Production Trap:
Cache mounts are not supported by the classic builder. You must enable BuildKit (set DOCKER_BUILDKIT=1 or use docker buildx). In CI, ensure your Docker version supports BuildKit.

CI-Specific Caching Strategies

In CI, you don't have the luxury of a persistent local Docker cache. Every runner starts fresh. Without a shared cache, every build is a full rebuild. Here's how to fix that:

  1. Use Docker's registry cache (BuildKit): Push cache layers to your container registry alongside the image. Pull them at the start of the next build. This is the most reliable approach.
  2. Use CI-native caching: GitHub Actions, GitLab CI, and CircleCI offer cache actions that can store Docker layers. For example, GitHub Actions' docker/login-action and docker/build-push-action support cache-from and cache-to.
  3. Save and load the Docker build cache manually: Export the cache to a tar file and store it as a CI artifact. This is clunky but works with any CI system.
  4. Use a Docker registry as a cache proxy: Run a local Docker registry (or use Docker Hub) and push/pull cache images. This is essentially the same as option 1 but without BuildKit's --cache-from syntax — you just pull a previous build image and use --cache-from pointing to it.
.github/workflows/docker-build.ymlDEVOPS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// io.thecodeforge — DevOps tutorial

name: Docker Build with Cache

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Log in to registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ghcr.io/${{ github.repository }}:latest
          cache-from: type=registry,ref=ghcr.io/${{ github.repository }}:cache
          cache-to: type=registry,ref=ghcr.io/${{ github.repository }}:cache,mode=max
Output
# First run: no cache, full build
# ...
# Cache pushed to ghcr.io/org/repo:cache
# Second run: cache pulled, layers reused
# ...
# Build time: 2m (was 10m)
Senior Shortcut:
Use mode=max for cache-to only if you have ample registry storage. For most teams, mode=min (caches only layers in the final image) is sufficient and uses less space.

Debugging Cache Misses: Tools and Techniques

When your cache isn't hitting, you need to find out why. Here's my debugging toolkit:

  1. docker build --progress=plain: Shows every step and whether it used cache. Look for CACHED vs Running in ....
  2. docker build --no-cache-filter=<stage>: Invalidate cache for a specific stage only. Useful for testing if a stage is the culprit.
  3. docker history <image>: Shows the layers in an image and their sizes. If a layer is unexpectedly large, it might be caching something you didn't intend.
  4. docker buildx du: Shows disk usage by BuildKit cache. Helps identify if cache mounts are growing out of control.
  5. Compare checksums: If you suspect a COPY instruction is invalidating, compute the checksum of the context manually: find . -type f -exec md5sum {} \; | sort | md5sum. Compare with the previous build's checksum.
  6. Check .dockerignore: If you're not ignoring unnecessary files (node_modules, .git, etc.), they're part of the build context and can cause cache misses. A good .dockerignore is essential.
.dockerignoreDEVOPS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// io.thecodeforge — DevOps tutorial

# Ignore everything that's not needed for the build
.git
node_modules
npm-debug.log
Dockerfile
.dockerignore
.gitignore
README.md
*.md
test
coverage
.env
.env.*
*.log
Output
# Without .dockerignore: context size = 50MB, every file change invalidates COPY
# With .dockerignore: context size = 2MB, fewer cache misses
The Classic Bug:
Forgetting to add node_modules to .dockerignore means your local node_modules is copied into the image, then overwritten by npm install. This doubles the context size and can cause subtle bugs if the local and container versions differ.

When NOT to Use Docker Layer Caching

Layer caching is powerful, but it's not always the right tool. Here's when I'd skip it:

  1. One-off builds: If you're building an image once (e.g., a throwaway test environment), caching adds complexity without benefit. Just build without cache.
  2. Tiny images: If your image is <100MB and builds in <30 seconds, caching overhead isn't worth it. The cache lookup and storage management cost more than the rebuild.
  3. Highly dynamic dependencies: If your dependencies change every build (e.g., you're pulling the latest commit of a library), caching is useless. Consider pinning versions or using a different strategy.
  4. Security-sensitive builds: If you need to ensure no cached layers from previous builds are used (e.g., building with different secrets), use --no-cache or --secret mounts.
  5. Ephemeral CI with no shared cache: If your CI doesn't support cache import/export and you can't use registry caching, the default cache is lost after the runner dies. You're better off optimizing the Dockerfile for speed without relying on cache.
Senior Shortcut:
When in doubt, measure. Time your build with and without cache. If the difference is <20%, the caching complexity isn't worth it.
● Production incidentPOST-MORTEMseverity: high

The 15-Minute Build That Killed Deploys

Symptom
Every merge to main triggered a 15-minute Docker build. The team was averaging 3 deploys per day, but the build queue was backing up to 45 minutes.
Assumption
The team assumed the build was slow because the application had grown. They considered moving to a larger CI runner.
Root cause
The Dockerfile had COPY . . before RUN npm install. Every code change — even a whitespace edit — invalidated the npm install layer, forcing a full reinstall of all 1,200 packages.
Fix
Reordered the Dockerfile: COPY package.json package-lock.json ./ then RUN npm ci then COPY . .. Build time dropped from 15 minutes to 90 seconds.
Key lesson
  • Always copy dependency manifests before source code.
  • A single misplaced COPY can cost your team hours every week.
Production debug guideSystematic recovery paths for the failure modes engineers actually hit.3 entries
Symptom · 01
Build always runs npm install even when package.json hasn't changed
Fix
1. Check Dockerfile order: ensure COPY package.json package-lock.json ./ comes before RUN npm ci and COPY . . comes after. 2. Verify that package-lock.json is committed and not in .dockerignore. 3. Run docker build --no-cache once to reset, then rebuild to confirm cache hits.
Symptom · 02
Build cache is not shared between CI runners — every runner does a full build
Fix
1. Enable BuildKit: set DOCKER_BUILDKIT=1 or use docker buildx. 2. Use --cache-from and --cache-to with a registry (e.g., type=registry,ref=myregistry.com/myapp:cache). 3. In CI, pull the cache image before building: docker pull myregistry.com/myapp:cache || true.
Symptom · 03
Image size is unexpectedly large (e.g., 2GB for a Node app)
Fix
1. Use multi-stage builds: create a build stage with dev dependencies, copy only artifacts to a slim final stage. 2. Check for unnecessary files in the build context: review .dockerignore. 3. Run docker history <image> to identify large layers.
★ Docker Layer Caching Triage Cheat SheetFirst-response commands for when things go wrong — copy-paste ready.
Build is slow — no cache hits (`Running in ...` for every step)
Immediate action
Check Dockerfile instruction order
Commands
docker build --progress=plain . 2>&1 | grep -E '(CACHED|Running in)'
docker history <image-name> --no-trunc | head -20
Fix now
Reorder Dockerfile: COPY dependency manifests first, then RUN install, then COPY source.
Cache not shared between CI runners+
Immediate action
Enable BuildKit and registry caching
Commands
docker buildx create --use
docker buildx build --cache-from=type=registry,ref=myreg.io/app:cache --cache-to=type=registry,ref=myreg.io/app:cache,mode=max -t myreg.io/app:latest .
Fix now
Add cache-from and cache-to to your CI build step.
Image too large+
Immediate action
Check for unnecessary layers
Commands
docker history <image> --no-trunc | awk '{print $1, $7}'
docker image inspect <image> --format='{{json .RootFS.Layers}}' | jq '. | length'
Fix now
Refactor to multi-stage build. Use a slim base image (alpine, distroless).
Cache mount not working (npm still downloads every time)+
Immediate action
Verify BuildKit is enabled
Commands
docker info | grep -i buildkit
DOCKER_BUILDKIT=1 docker build --progress=plain .
Fix now
Set DOCKER_BUILDKIT=1 environment variable or use docker buildx.
FeatureClassic BuilderBuildKit
Cache storageLocal onlyLocal, registry, S3, GCS, Azure Blob
Cache exportNot supportedSupported via --cache-to
Cache mountNot supportedSupported
Concurrent buildsSerialParallel (with --load)
Default sinceDocker 1.0Docker 23.0 (experimental earlier)
Cache invalidation on ARG changeInvalidates layer using ARG and all afterSame
⚙ Quick Reference
8 commands from this guide
FileCommand / CodePurpose
Dockerfile.cache-optimizedFROM node:18-alpine AS baseHow Docker Build Cache Actually Works
Dockerfile.bad-orderFROM node:18-alpineThe COPY Order Trap
Dockerfile.multistageFROM node:18-alpine AS baseMulti-Stage Builds
build-cache-commands.shdocker buildx build \Using BuildKit for Faster, Smarter Caching
Dockerfile.pinned-baseFROM node:18@sha256:abc123def456abc123def456abc123def456abc123def456abc123def456...When Caching Breaks
Dockerfile.cache-mountFROM node:18-alpine AS depsCache Mounts
.githubworkflowsdocker-build.ymlname: Docker Build with CacheCI-Specific Caching Strategies
.dockerignore.gitDebugging Cache Misses

Key takeaways

1
Order your Dockerfile by frequency of change
stable layers first (OS, runtime), then dependencies, then source code.
2
Pin base images to digests, not tags, to prevent silent cache invalidation from upstream updates.
3
Use BuildKit's registry cache to share cache across CI runners
it's the most reliable way to speed up ephemeral builds.
4
Multi-stage builds are not just for image size
they also isolate cache domains, so changes in the build stage don't invalidate the base stage.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
How does Docker determine whether to use a cached layer for a RUN instru...
Q02SENIOR
When would you choose BuildKit's registry cache over a CI-native cache (...
Q03SENIOR
What happens to the cache when you change a build argument (ARG) value?
Q04JUNIOR
What is the purpose of a .dockerignore file in the context of layer cach...
Q05SENIOR
Your team's Docker build takes 10 minutes. After reordering the Dockerfi...
Q06SENIOR
How would you design a Docker build pipeline for a microservice that dep...
Q01 of 06SENIOR

How does Docker determine whether to use a cached layer for a RUN instruction?

ANSWER
Docker compares the instruction string (e.g., 'RUN npm ci --only=production') and the checksum of the build context files referenced by preceding COPY/ADD instructions. If both match a previously built layer, it reuses that layer. The instruction string must be byte-identical — even a trailing space changes the cache key.
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
Why is my Docker build not using cache even though I didn't change any files?
02
What's the difference between COPY and ADD for caching?
03
How do I share Docker cache between GitHub Actions runners?
04
Can Docker layer caching cause security issues?
N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Notes here come from systems that actually shipped.

Follow
Verified
production tested
July 11, 2026
last updated
258
articles · all by Naren
🔥

That's Docker. Mark it forged?

7 min read · try the examples if you haven't

Previous
Dockerfile Best Practices
22 / 32 · Docker
Next
Docker Storage and Volumes Deep Dive