Docker Image Layers and Build Cache
Why instruction order decides whether your build takes one second or ninety. Watch the cache report CACHED, then break it with a one-byte change and watch which layers rebuild.
Dockerfiles and Builds Guide 17 of 46 Intermediate
- OSUbuntu 26.04 LTS (resolute)
- Docker Engine29.7.2
- Shellbash
- Architectureamd64
- TimeAbout 13 min
- Reviewed21 August 2026
Tested on the versions above. Layer sizes, digests and timings vary per run and per host. Match the shape of the output, not the exact strings.
| Server Name | IP Address | OS | Roles | CPU | RAM | HDD |
|---|---|---|---|---|---|---|
| DOCKER01 | 192.168.0.21 | Ubuntu 26.04 LTS | Docker Host | 2 Core | 4 GB | 50 GB |
Before you start
-
Every instruction is a layer
An image is a stack of read-only diffs.
docker historyshows them newest first, with the instruction that produced each and what it added. Metadata instructions such as CMD cost nothing; instructions that touch the filesystem cost real bytes.bash Example session docker history cg-hello:1.0 --format "table {{.CreatedBy}}\t{{.Size}}"CREATED BY SIZECMD ["./hello.sh"] 0BRUN /bin/sh -c chmod +x hello.sh # buildkit 12.3kBCOPY hello.sh . # buildkit 12.3kBWORKDIR /app 8.19kBRUN /bin/sh -c apk add --no-cache curl # bui… 5.27MBADD alpine-minirootfs-3.22.5-x86_64.tar.gz /… 8.96MBExpected resultOne row per layer, with sizes attributable to specific instructions.
Success conditionYou can see which instruction is expensive. Note
chmodcost a whole 12.3kB layer just to flip a permission bit. -
A rebuild with nothing changed reuses everything
BuildKit marks reused layers CACHED. Counting them is the quickest way to see how much work a build actually did.
bash docker build -t cg-ctx:demo . 2>&1 | grep -c CACHED2Expected resultA non-zero count - every eligible layer was reused.
Success conditionThe count matches the number of cacheable instructions. A count of zero means something invalidated the very first layer.
-
Break the cache with one byte
Change a single file that a COPY instruction reads. That layer's inputs differ, so it rebuilds - and crucially, so does every layer after it. Layers below the change are still reused, which is the entire basis of instruction ordering. That cascade is the whole reason instruction order matters. Put what changes least at the top - base image, system packages, dependency manifests, dependency install - and your source last. Copying source before installing dependencies discards the dependency cache on every commit:
COPY . .thenRUN npm cirebuilds the install every time, whileCOPY package.json package-lock.json ./thenRUN npm cithenCOPY . .keeps it cached.bash echo change > src/app.pydocker build -t cg-ctx:demo . 2>&1 | grep -E "CACHED"#6 CACHEDExpected resultFar fewer CACHED lines than before - only the layers preceding the change survive.
Success conditionThe count dropped. The surviving CACHED layer is everything below the file you touched - which is why the cheapest build optimisation available is reordering instructions.
-
Force a clean build when you need to prove something
--no-cacheignores every cached layer. It is slow on purpose. Use it to confirm a build is genuinely reproducible from scratch, or when you suspect a stale layer is hiding a problem - not as a habit.bash # slow by design - rebuilds every layer including package installsdocker build --no-cache -t cg-ctx:demo .# no CACHED lines will appearExpected resultA build with no CACHED lines at all.
Success conditionEvery step runs. If the build fails only with --no-cache, a cached layer was masking a broken instruction.
Troubleshooting
The cache is never used, even when nothing changed
Why: Something early invalidates: a COPY of a file whose contents or metadata changed, an ADD of a remote URL, or a base image tag that was repointed upstream.
Fix:Find the first non-CACHED step - everything below it is collateral. Pin the base image by digest if upstream churn is the cause.
bash docker build --progress=plain -t tmp . 2>&1 | grep -E "CACHED|^#[0-9]+ \[" | head -12Deleting files in a later RUN did not shrink the image
Why: Layers are additive. A file added in one layer and deleted in a later one is still present in the earlier layer and still ships.
Fix:Create and clean up within the same RUN instruction, or move the work into a builder stage - see the multi-stage guide.
bash # same layer, so the cache never ships:RUN apk add --no-cache curlBuild cache is consuming a lot of disk
Why: Cache accumulates across builds and is not bounded by default.
Fix:Check the size first with
docker system df, which reports build cache separately and how much is reclaimable. Only then decide what to remove.bash Example session docker system dfTYPE TOTAL ACTIVE SIZE RECLAIMABLEBuild Cache 8 0 20.52MB 7.772MB