Why your Docker image is way bigger than it needs to be, and how to check
A 1.2GB image for an app whose actual code is a few megabytes is one of the most common Docker complaints, and the cause is almost always one of a small number of well-known habits — not something wrong with Docker itself. Here's how to see where the size is actually going, and the fixes in order of impact.
Check where the size actually is, don't guess
docker image ls # total size per image
docker history <image> # size added by each layer, in build order
docker history is the important one — it shows exactly which instruction in the
Dockerfile added how many megabytes, which turns "the image is big" into "this specific
RUN line added 400MB." For a deeper look at what's inside a layer rather than just its
total size, the third-party tool dive (wagoodman/dive) opens an interactive layer-by-
layer file browser — not part of Docker itself, but the standard tool people reach for when
docker history's per-layer totals aren't enough detail.
Cause 1: a full OS base image instead of a minimal one
FROM ubuntu:24.04 or FROM debian:12 starts you well over 100MB before a single line of
application code is added. FROM python:3.12-slim or FROM node:22-alpine (Alpine-based
images use musl instead of glibc and a minimal package set, often under 50MB base)
cover the same runtime with a fraction of the base size. This is the single biggest lever
available, and it's a one-line change.
Cause 2: package manager cache left in the image (the classic gotcha)
This is the one that catches people even after they think they've cleaned up, because it misunderstands how image layers work:
# Wrong — looks like it cleans up, but doesn't shrink the image
RUN apt-get update && apt-get install -y build-essential
RUN rm -rf /var/lib/apt/lists/*
Each RUN instruction creates a new, immutable layer. Deleting a file in a later layer
doesn't remove it from the earlier layer where it was created — it just hides it in the
final merged view. The bytes are still physically present in the image, in the layer that
added them; the image is exactly as large as if the deletion never happened. The fix is to
create and clean up within the same RUN instruction, so the cache never exists in any
layer that ships:
# Right — cache is created and removed within one layer, never shipped
RUN apt-get update && apt-get install -y build-essential \
&& rm -rf /var/lib/apt/lists/*
Cause 3: build tools shipping in the final image
A typical build needs a compiler, dev headers, and build-time dependencies that the running application never touches again. Without a multi-stage build, all of that ships in the final image anyway, because there's only one image and it accumulated everything used at any point.
# Stage 1: has the compiler, dev headers, everything needed to build
FROM golang:1.23 AS build
WORKDIR /src
COPY . .
RUN go build -o /app
# Stage 2: final image only gets the compiled binary, not the Go toolchain
FROM gcr.io/distroless/base-debian12
COPY --from=build /app /app
ENTRYPOINT ["/app"]
The final image contains the compiled binary and nothing else that produced it — no compiler, no source tree, no package manager. This is usually the single largest reduction available for compiled languages, often cutting an image by 90%+.
Cause 4: no .dockerignore, so COPY . . pulls in everything
Without a .dockerignore, COPY . . includes .git (which can be enormous on an old
repo), node_modules, local .env files, test fixtures, and build artifacts from your
host machine — none of which the running container needs, and some of which (.env) it
actively shouldn't have.
# .dockerignore
.git
node_modules
*.md
.env
tests/
Checking the win
docker image ls # compare the before/after size directly
docker history <image> # confirm the big layers are actually gone, not just hidden
Sources: Docker's own documentation on multi-stage builds and image layer caching; Alpine
and Debian-slim official image size comparisons; dive project documentation for layer
inspection; the immutable-layer/union-filesystem behavior described (deletions in a later
layer not reclaiming space from an earlier one) is fundamental to how OCI image layers
work, documented in the OCI Image Format Specification.