Why Image Size Matters
Large Docker images slow down builds, CI/CD pipelines, and deployments because every layer must be pulled and pushed across the network. Smaller images also reduce the attack surface by shipping fewer binaries and libraries, and they lower storage costs for registries. Optimizing image size is one of the highest-leverage habits a container author can build.
Cricket analogy: A touring squad carrying excess baggage through every airport slows down travel and increases the risk of something going missing or being flagged at customs; keeping the kit bag lean is one of the highest-leverage habits a professional player builds, just like image size optimization.
Choosing a Minimal Base Image
The base image you choose has the single biggest impact on final image size. A full 'ubuntu' or 'debian' image can be several hundred megabytes, while 'alpine' variants are often under 10MB, and 'distroless' images ship only the application and its runtime dependencies with no shell or package manager at all.
Cricket analogy: Choosing a full first-class touring party of thirty support staff has a far bigger impact on travel costs than any other decision, while a lean core squad of essential players like a T20 franchise roster keeps logistics minimal, mirroring the base image's outsized impact on final size.
# Heavy base (avoid for production)
FROM python:3.12
# Much smaller alternative
FROM python:3.12-slim
# Smallest, no package manager, no shell
FROM gcr.io/distroless/python3-debian12Multi-Stage Builds
Multi-stage builds let you compile or install dependencies in a 'builder' stage that includes compilers and dev tools, then copy only the final artifacts into a clean, minimal runtime stage. The builder stage's layers are discarded entirely from the final image.
Cricket analogy: A team's extended preseason training camp with extra coaches and equipment (builder stage) is where the real conditioning work happens, but only the final fit, match-ready squad (runtime stage) actually travels to the tournament, leaving the training camp's extra baggage behind entirely.
# Stage 1: build
FROM golang:1.22 AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /out/app ./cmd/app
# Stage 2: minimal runtime
FROM gcr.io/distroless/static-debian12
COPY --from=builder /out/app /app
ENTRYPOINT ["/app"]Minimizing Layers and Cleaning Up in the Same RUN
Each RUN, COPY, and ADD instruction creates a new layer. Package manager caches and temporary files added in one RUN layer are NOT removed by a cleanup command in a later layer, because earlier layers are already committed. Combine install and cleanup into a single RUN statement.
Cricket analogy: If a groundskeeper piles extra soil onto the pitch on Monday and only rakes it flat on Wednesday, the pile was already 'committed' into Monday's ground state; the fix is piling and leveling in the same session, just as combining install and cleanup into a single RUN avoids stale layers.
RUN apt-get update && \
apt-get install -y --no-install-recommends curl && \
rm -rf /var/lib/apt/lists/*Running 'apt-get install' in one RUN layer and 'rm -rf /var/lib/apt/lists/*' in a separate later RUN layer does NOT shrink the image — the apt cache is still preserved in the earlier layer's diff. Always chain install and cleanup in the same RUN instruction.
Using .dockerignore and Ordering Layers for Cache Efficiency
A '.dockerignore' file prevents build context bloat (node_modules, .git, local build artifacts) from being sent to the Docker daemon, which speeds up builds. Ordering instructions from least-to-most frequently changing (dependency manifests before source code) maximizes layer cache reuse across builds.
Cricket analogy: A touring party leaves personal luggage like street clothes and gadgets out of the official team kit bag sent ahead, keeping it light, and packs in order of what's needed least first (formal wear) to what's needed most (match gear), mirroring '.dockerignore' and layer ordering.
.git
node_modules
*.log
dist/
.envTools like 'docker image ls', 'docker history <image>', and 'dive' let you inspect per-layer size contributions so you can find and eliminate the largest offenders in an existing Dockerfile.
- Prefer slim, alpine, or distroless base images over full OS images
- Multi-stage builds discard build-time tools from the final image
- Combine package install and cache cleanup into a single RUN instruction
- Use .dockerignore to keep the build context small
- Order Dockerfile instructions to maximize layer cache hits
- Inspect image layers with 'docker history' or 'dive' to find bloat
Practice what you learned
1. Why does running 'rm -rf /var/lib/apt/lists/*' in a separate RUN instruction fail to reduce image size?
2. What is the primary benefit of a multi-stage Dockerfile build?
3. Which base image type ships without a shell or package manager?
4. What does a .dockerignore file primarily accomplish?
5. Which command helps inspect the size contribution of each layer in an image?
Was this page helpful?
You May Also Like
Multi-Stage Builds
How to use multiple FROM stages in a single Dockerfile to separate build-time tooling from the final runtime image, producing smaller, more secure images.
Writing a Dockerfile
A practical guide to Dockerfile syntax and the core instructions used to build a custom image from scratch.
Image Layers and Build Caching
How Docker's layered filesystem and build cache work, and how to order Dockerfile instructions to maximize cache hits and minimize image size.
Docker Security Basics
Defensive practices for hardening container images and runtime, including non-root users, minimal base images, vulnerability scanning, and dropped capabilities.
Related Reading
Related Study Notes in DevOps
Browse all study notesNginx Study Notes
DevOps · 30 topics
DevOpsAnsible Study Notes
DevOps · 30 topics
DevOpsAdvanced Kubernetes Study Notes
Kubernetes · 30 topics
DevOpsAdvanced Bash Scripting Study Notes
Bash · 30 topics
DevOpsApache Kafka Study Notes
Kafka · 30 topics
DevOpsCI/CD Tools & Pipelines Study Notes
YAML · 37 topics