Image Optimization & Layer Caching

Docker Intermediate/Advanced
Chapter 2 ยท Image Optimization & Layer Caching

๐Ÿ“ฆ Image Optimization & Layer Caching

Chapter 1 reduced image size between stages. This chapter optimizes within a stage โ€” fewer, smaller layers, a smart base image choice, and a build order that keeps rebuilds fast instead of reinstalling every dependency on every source change.

How Docker Layers Work

Every instruction in a Dockerfile โ€” RUN, COPY, ADD โ€” creates a new layer. Docker caches each layer, reusing it on the next build as long as nothing about that instruction (or anything before it) has changed. Understanding this caching behavior is the foundation for everything else in this chapter.

Minimizing Layers

Combining related commands into a single RUN instruction โ€” using && and line continuations โ€” reduces the total layer count, and matters for more than just tidiness.

# Three separate layers โ€” the apt cache still exists in an EARLIER layer # even after this later RUN "deletes" it RUN apt-get update RUN apt-get install -y curl RUN rm -rf /var/lib/apt/lists/* # One layer โ€” the cache never exists in the image's history at all RUN apt-get update && \ apt-get install -y curl && \ rm -rf /var/lib/apt/lists/*

.dockerignore

A .dockerignore file excludes files from the build context sent to the Docker daemon โ€” the same idea as .gitignore, applied to what a build is even allowed to see.

# .dockerignore node_modules .git .env *.log

This speeds up builds (a smaller context transfers faster) and, just as importantly, prevents accidentally COPYing sensitive files โ€” a local .env with real credentials, for instance โ€” into an image, the same "never let a secret leak into a built artifact" discipline this site has stressed repeatedly for source control and CI.

Choosing a Base Image: Alpine vs. Slim vs. Full

Base Image TypeSizeC LibraryCompatibility Risk
Full (e.g. node:18)LargestglibcLowest โ€” includes broad tooling, "just works"
Slim (e.g. node:18-slim)SmallerglibcLow โ€” drops non-essential packages, keeps glibc compatibility
Alpine (e.g. node:18-alpine)SmallestmuslReal โ€” native modules compiled against glibc can fail or behave subtly differently

Alpine's small size comes from using musl libc instead of the more common glibc โ€” a genuinely different C library, not just a stripped-down version of the same one. Some native Node.js addons or Python packages compiled against glibc can fail to run, or behave subtly differently, on Alpine โ€” a well-known, real compatibility gotcha, not a theoretical edge case.

Build Cache Invalidation Order

The moment anything about an instruction changes, that layer's cache is invalidated โ€” and so is every layer after it, even if those later instructions themselves didn't change at all. Instruction order in the Dockerfile determines how much gets rebuilt on a typical change.

Bad Ordering vs. Good Ordering

Bad: COPY . . First
COPY . . RUN npm install

Any source file change invalidates the COPY layer โ€” and therefore forces npm install to re-run on every single build, even when dependencies never changed.

Good: Dependencies First
COPY package*.json ./ RUN npm install COPY . .

npm install's cache stays valid across source-only changes โ€” it only re-runs when package.json/package-lock.json actually change.

The general rule: order instructions from least likely to change to most likely to change โ€” dependency manifests before source code, since dependencies change far less often than application code does during normal development.

๐Ÿ’ป Coding Challenges

Challenge 1: Combine Layers With Cleanup

Rewrite this three-layer sequence into a single RUN instruction that never leaves the cleaned-up files in the image's history: RUN apt-get update, RUN apt-get install -y git, RUN rm -rf /var/lib/apt/lists/*.

Goal: Practice the combine-and-cleanup-in-one-layer pattern this chapter introduced.

โ†’ Solution

Challenge 2: Reorder for Better Caching

A Python Dockerfile has COPY . . immediately followed by RUN pip install -r requirements.txt. Reorder it so source-code-only changes don't force a dependency reinstall.

Goal: Practice applying the dependency-manifest-first ordering rule to a Python project.

โ†’ Solution

Challenge 3: Pick a Base Image

A team's Node.js app depends on a native addon compiled against glibc, and image size is a secondary concern to reliability. Which base image type should they choose, and why not the smallest option?

Goal: Practice weighing size against compatibility risk using this chapter's alpine/slim/full comparison.

โ†’ Solution

โš ๏ธ Gotcha: Deleting a File in a Later Layer Doesn't Shrink the Image

Each layer is a diff against the previous one โ€” when a later RUN rm ... instruction deletes a file, Docker records that deletion as its own layer, but the file still physically exists in the earlier layer's diff, and the image still has to ship it. This is exactly why the three-separate-RUN example above doesn't actually save any space, despite the final command "removing" the apt cache โ€” the cache is still sitting in the layer created by apt-get update. The fix is always the same: cleanup has to happen in the same RUN instruction that created the mess, never a separate, later one.

๐ŸŽฏ What's Next

The next chapter is Docker Compose in Depth โ€” going beyond docker1's intro: override files, profiles, healthchecks, depends_on with conditions, and explicitly named networks/volumes.