Topic 7.1
Shrinking Your Images
In one line
A smaller image pulls faster, deploys faster, costs less to store, and has fewer places for a vulnerability to hide — size reduction is one of the highest-leverage things you can do to a Dockerfile.
Think of it like this
Packing for a trip with only a carry-on versus checking three oversized suitcases. The carry-on gets through the airport faster, costs nothing extra, and there's simply less that could get lost or damaged — a smaller image is the exact same trade, applied to shipping software instead of luggage.
Key ideas
- 01
Choose the smallest base image variant that genuinely satisfies your needs:
-alpinevariants are often 5-10x smaller than full Debian-based images (Alpine Linux's entire base is only a few MB), and 'distroless' images (like Google'sgcr.io/distroless/java) go even further, containing ONLY your app's runtime dependencies and nothing else — no shell, no package manager, nothing extraneous at all. - 02
Multi-stage builds (Phase 6.1) are themselves the single biggest size lever for compiled languages like Java — never ship a build toolchain (Maven, the JDK compiler) in your final image if a JRE-only final stage can do the job.
- 03
Combine RUN instructions and clean up in the SAME layer (Phase 2.4's lesson) — package manager cache files and temporary downloads that aren't cleaned up WITHIN the same RUN instruction that created them get permanently baked into the image regardless of later cleanup attempts.
- 04
Order your COPY instructions to copy the SMALLEST, most-stable things first and your actual application code LAST (Phase 2.2's caching lesson) — this is a build-speed optimization more than a final-size optimization, but the two often go hand in hand in a well-structured Dockerfile.
- 05
docker image lsshows sizes directly.docker history <image>shows the size contributed by EACH individual layer — genuinely useful for hunting down which specific instruction is responsible for unexpected bloat. Third-party tools likedivegive an even more detailed, interactive layer-by-layer filesystem browser. - 06
Watch for accidentally including build artifacts, test files, or documentation that got swept in by an overly broad
COPY . .— a solid.dockerignore(Phase 2.3) prevents most of this category of bloat before it ever happens.
In your stack
- →
A realistic size comparison worth internalizing: a naive single-stage Dockerfile using a full JDK base and copying a whole Maven project might produce a 700MB+ image. The SAME app, built with a proper multi-stage Dockerfile onto an
eclipse-temurin:17-jre-alpinefinal stage, often lands around 150-200MB — sometimes less with careful layering — for functionally identical running behavior.
Code & diagrams
Measure this yourself on any real project — the numbers are genuinely convincing.
# See every image's size at a glance
docker images
# Break down exactly which layer contributed how much size
docker history myapp:v1
# A JRE-alpine final stage instead of a plain JRE base often saves another 30-60MB
FROM eclipse-temurin:17-jre-alpine
# versus
FROM eclipse-temurin:17-jre
# Confirm the difference directly
docker pull eclipse-temurin:17-jre
docker pull eclipse-temurin:17-jre-alpine
docker images | grep eclipse-temurinExplain it without notes
Why does switching a final-stage base image from a full JRE to a JRE-alpine variant sometimes introduce subtle runtime bugs that a full Debian-based JRE image didn't have?
You're asked to reduce a Java Docker image's size. In priority order, what are the first two things you'd check, and why those two first?
Practice
Take any Dockerfile you've written and run docker history on the resulting image — identify the single largest layer and explain why it's that large.
Try switching a Dockerfile's final stage from eclipse-temurin:17-jre to eclipse-temurin:17-jre-alpine, rebuild, and confirm the app still works correctly end to end (not just that it builds).
Trade-offs
- ↔
Chasing the absolute smallest possible image (distroless, scratch-based builds) can mean losing debugging conveniences (no shell to
docker execinto, no package manager to install a diagnostic tool on the fly) — a real trade between production leanness/security and day-to-day debuggability that's worth deciding deliberately per project, not by default.
Done when you can
I can identify the largest layer in any of my images using docker history.
I know the size trade-off between full, -alpine, and distroless base image variants.
I've personally measured a real before/after size reduction on a project.