Learn how the Docker build cache works, why one changed line can rebuild everything after it, and how to order a Dockerfile so rebuilds take seconds.

Bishal Biswas
Self Learner
Why your second build should be fast
The first time you build an image, Docker runs every instruction in your Dockerfile. Installing packages can take a minute or more. That is fine once. But you build many times a day, and if every small code change means waiting for npm ci again, you will lose a lot of time.
Docker has a solution called the build cache. A cache is a store of saved results that you can reuse instead of doing the same work again. In this lesson you will learn how the build cache decides what to reuse, why the order of your instructions matters so much, and how to write a Dockerfile for the notes-api app that rebuilds in a few seconds.
You learned in Images and Layers Explained that an image is a stack of layers. Each instruction such as RUN or COPY adds a layer. The build cache is simply a collection of layers that Docker built earlier and kept.
How the cache works
When Docker builds an image, it goes through the Dockerfile from top to bottom. For each instruction it asks one question:
“Have I already built this exact step, on top of this exact parent layer?”
If the answer is yes, Docker reuses the saved layer. This is called a cache hit, and it takes almost no time. If the answer is no, Docker runs the instruction for real. This is a cache miss.
How Docker answers that question depends on the instruction:
| Instruction | Docker reuses the cached layer when… |
|---|---|
FROM |
The same base image is already on your machine |
RUN |
The command text is exactly the same as last time |
COPY and ADD |
The files being copied have exactly the same content as last time |
ENV, WORKDIR, EXPOSE, USER |
The instruction text is the same |
Two details are worth remembering:
- For
RUN, Docker only compares the text of the command. It does not know what the command does.RUN apt-get updatewill be reused even if the package lists on the internet changed last week. - For
COPY, Docker looks at the content of the files (a checksum, which is like a fingerprint of the file). Changing one character in one copied file is enough to cause a miss.
The chain rule: one miss breaks everything below it
This is the most important idea in the lesson. Each layer sits on top of the one before it. If a layer changes, every layer above it is built on a different parent, so Docker cannot trust the old ones. After the first cache miss, every instruction after it runs again.
FROM node:24-alpine cache hit
WORKDIR /app cache hit
COPY . . MISS (you edited src/server.js)
RUN npm ci runs again, even though dependencies did not change
CMD ["node", "src/server.js"] runs again
Think of it like a stack of plates. If you need to replace the third plate, you have to lift off every plate above it first.
So the goal is simple: put the things that rarely change near the top, and the things that change often near the bottom.
A slow Dockerfile
Here is a first version of a Dockerfile for notes-api. It works, but it is slow to rebuild.
# syntax=docker/dockerfile:1
FROM node:24-alpine
WORKDIR /app
# Copies EVERYTHING, including source code
COPY . .
# Depends on the line above, so it reruns on every code change
RUN npm ci
CMD ["node", "src/server.js"]
The problem is COPY . . before npm ci. Your source code changes all the time. Each change causes a miss on the COPY line, and the chain rule then forces npm ci to run again, downloading every package from scratch.
A fast Dockerfile
The fix is to split the copy into two steps. First copy only the files that describe your dependencies, install them, and only then copy the rest of the code.
# syntax=docker/dockerfile:1
FROM node:24-alpine
WORKDIR /app
# 1. Dependency files change rarely
COPY package.json package-lock.json ./
# 2. So this slow step is cached until package files change
RUN npm ci --omit=dev
# 3. Source code changes often, so it comes last
COPY . .
CMD ["node", "src/server.js"]
Now when you edit src/server.js, the COPY package.json ... step is still a hit (those files did not change), so npm ci is also a hit. Only the final COPY . . runs again, which takes a fraction of a second.
The same pattern works for every language. In Python you copy requirements.txt first, then run pip install, then copy the code. You will do that in Project: Dockerizing a Python App.
Seeing the cache in action
Build the image once:
docker build -t notes-api:dev .
Now change a line in src/server.js and build again. Look for the word CACHED in the output:
=> [1/5] FROM docker.io/library/node:24-alpine 0.0s
=> CACHED [2/5] WORKDIR /app 0.0s
=> CACHED [3/5] COPY package.json package-lock.json ./ 0.0s
=> CACHED [4/5] RUN npm ci --omit=dev 0.0s
=> [5/5] COPY . . 0.1s
=> exporting to image 0.2s
Steps 2 to 4 say CACHED. Only step 5 ran. The whole build took well under a second.
Tip: If the build output moves too fast to read, add
--progress=plaintodocker build. It prints every step as plain text, which is also useful when you need to see the output of aRUNcommand.
You can also see the layers of a finished image, and how big each one is, with docker history:
docker history notes-api:dev
More ordering tips
Order from least to most frequently changed
A good general order for most Dockerfiles is:
FROMthe base image- System packages (
apt-get install,apk add) WORKDIR, users and other setup- Dependency files, then the install step
- Your source code
CMDorENTRYPOINT
Keep apt-get update and apt-get install together
This looks harmless but causes a real bug:
RUN apt-get update
RUN apt-get install -y curl
The first line is cached forever because its text never changes. Weeks later you add a new package to the second line. Docker reruns only the install step, using the old, stale package list, and the install can fail. Put both in one RUN so they are always cached (or rebuilt) together:
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
Keep the build context small
COPY . . checks every file it copies. If your folder contains node_modules, .git or log files, any change to them causes a cache miss for no good reason. A .dockerignore file stops these files from being sent to the build at all. You learned how in The Build Context and .dockerignore.
node_modules
.git
*.log
.env
Put build arguments as late as possible
A changed ARG value can cause misses from the point where it is first used. If you pass something like a version number or build date with --build-arg, declare and use it near the end of the Dockerfile, so it does not break the cache for the slow steps above. See Build Arguments and Environment Variables: ARG vs ENV for more about ARG.
Forcing a fresh build
Sometimes you want Docker to ignore the cache. For example, a RUN apt-get upgrade step may be cached for months, so it never picks up security fixes.
# Ignore the cache for every step
docker build --no-cache -t notes-api:dev .
# Also download a newer version of the base image if one exists
docker build --pull -t notes-api:dev .
# Do both, for example in a weekly scheduled build
docker build --pull --no-cache -t notes-api:dev .
| Flag | What it does |
|---|---|
--no-cache |
Runs every instruction again, ignoring saved layers |
--pull |
Checks the registry for a newer base image before building |
Note: BuildKit, Docker’s default builder, can also keep a cache of downloaded packages that survives even a cache miss, using
RUN --mount=type=cache. You will learn this in BuildKit Cache Mounts and Build Secrets.
The cache in CI
On your laptop the cache lives on your disk and is there the next time you build. A CI runner, such as a GitHub Actions machine, often starts empty every time, so it has no cache at all. Good ordering still helps, but you need a little extra setup to save and restore the cache between CI runs. You will see that in Building and Pushing Images in GitHub Actions.
Common mistakes
Copying everything before installing dependencies
COPY . . before npm ci or pip install is the number one cause of slow builds. Always copy the dependency files first.
Expecting RUN to notice changes on the internet
RUN curl https://example.com/script.sh | sh is cached by its text. If the script on the website changes, Docker will not notice. Use --no-cache, or download a specific, versioned file.
Forgetting package-lock.json
If you copy only package.json, npm ci fails because it needs the lock file. Copy both: COPY package.json package-lock.json ./.
Thinking the cache makes images smaller
The cache makes builds faster, not images smaller. To make images smaller you need multi-stage builds, which come next.
Quick recap
- Each Dockerfile instruction makes a layer, and Docker keeps built layers in the build cache.
RUNis reused when its text is the same;COPYis reused when the file content is the same.- After the first cache miss, every later instruction runs again.
- Put rarely changing steps first and often changing steps (your source code) last.
- Copy dependency files, install, then copy the code.
- Keep
apt-get updateandinstallin oneRUN, and use.dockerignore. - Use
--no-cacheand--pullwhen you need a truly fresh build.
Next, you will use several FROM lines in one Dockerfile to keep build tools out of your final image in Multi-Stage Builds for Small Images.
# syntax=docker/dockerfile:1
# Cache-friendly Dockerfile for notes-api.
# Rule: things that change rarely go at the top, things that change often go at the bottom.
# Build: docker build -t notes-api:dev .
# Fresh: docker build --pull --no-cache -t notes-api:dev .
#
# 1. Base image: changes only when you choose a new version
FROM node:24-alpine
# 2. Setup steps: almost never change
ENV NODE_ENV=production
WORKDIR /app
# 3. Copy ONLY the dependency files first
COPY package.json package-lock.json ./
# 4. Slow install step: stays CACHED until the package files change
RUN npm ci --omit=dev && npm cache clean --force
# 5. Source code: changes all the time, so it is copied last
# (.dockerignore keeps node_modules, .git and .env out of this copy)
COPY . .
# 6. Metadata and start command
EXPOSE 3000
USER node
CMD ["node", "src/server.js"]
Was this explanation helpful?
Your feedback helps us improve the content for everyone.
Thank you for your feedback!
We appreciate you helping us improve.
What could we improve? (Optional)
Read aloud
Ready