How to Shrink a Docker Image
Introduction¶
Smaller images pull faster, start faster, cost less to store, and have fewer CVEs. Shrinking one is a repeatable process — measure, cut the biggest thing, repeat.
Measure first¶
docker image ls myapp
docker history myapp:latest --no-trunc --format '{{.Size}}\t{{.CreatedBy}}'
dive myapp:latest # interactive: per-layer contents, "wasted space" score
dive shows you exactly which layer added the 400 MB and whether a later layer
deleted files (which doesn't shrink the image — the bytes stay in the earlier
layer).
The levers, biggest first¶
1. Base image¶
| Base | Approx size |
|---|---|
python:3.12 |
~1 GB |
python:3.12-slim |
~150 MB |
python:3.12-alpine |
~50 MB (musl — test carefully) |
gcr.io/distroless/python3 |
~50 MB, no shell |
scratch (static binaries) |
0 |
-slim is the safe default. Alpine saves more but musl libc breaks some wheels
and DNS edge cases. Distroless removes the shell and package manager entirely —
great for security, harder to debug.
2. Multi-stage¶
Build with the full toolchain, copy only the artifact into a slim/distroless runtime. This is usually the single biggest win. See Multi-stage Docker builds explained.
3. One RUN, cleaned up¶
Each RUN is a layer. apt-get install then rm in a later RUN keeps the
package bytes forever.
RUN apt-get update && apt-get install -y --no-install-recommends \
curl ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# pip
RUN pip install --no-cache-dir -r requirements.txt
# apk
RUN apk add --no-cache curl
--no-install-recommends, --no-cache-dir, apk --no-cache skip the caches in
the first place.
4. .dockerignore¶
Stop sending junk into the build context (and into COPY . .):
.git
node_modules
*.log
dist
coverage
.env*
**/__pycache__
5. Build-only deps in a build stage¶
gcc, -dev headers, build-essential, make — needed to compile wheels or
native modules, never at runtime. Multi-stage handles this; or apt-get purge
them in the same RUN after building.
After the pass¶
FROM python:3.12-slim AS build
RUN python -m venv /venv
ENV PATH=/venv/bin:$PATH
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt
FROM python:3.12-slim
COPY --from=build /venv /venv
ENV PATH=/venv/bin:$PATH
WORKDIR /app
COPY . .
USER 1000
CMD ["python", "-m", "app"]
Verification and troubleshooting¶
docker build -t myapp:slim .
docker image ls myapp
dive myapp:slim
docker run --rm myapp:slim python -c "import app; print('ok')"
trivy image myapp:slim # did the CVE count drop too?
- Image didn't shrink after adding
rm -rf— the delete is in a separate layer. Combine into the sameRUNwith&&. - Alpine build fails compiling a Python/Node native dep — musl vs glibc. Add
build-base/gcc musl-devfor the build, or switch to-slim. - distroless image won't start / can't debug — no shell. Use a
:debugvariant (has busybox) locally, ordocker run --entrypoint shwon't work — usekubectl debug/ an ephemeral container. COPY . .still huge —.dockerignoremissing or not at the build context root.- Layer cache busts every build —
COPY . .before dependency install. Reorder. - Squashing (
--squash) hides the problem, loses cache sharing, and is experimental. Fix the layers instead.
Related tools and reading¶
- On-site: Web server config generator.
- Related posts: Multi-stage Docker builds explained, Dockerfile best practices.
Stuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.More in Docker & Kubernetes
StatefulSets Explained
StatefulSets give pods sticky names, sticky storage, and ordered operations — what databases and clustered apps need. Here's how each guarantee works.
October 24, 2026Persistent Volumes and Claims, Explained
PVC is a request, PV is the fulfilled storage, StorageClass is the factory. Most 'Pending PVC' problems are an access-mode or provisioner mismatch.
October 24, 2026Kubernetes Jobs and CronJobs
A Job runs pods until they succeed; a CronJob creates Jobs on a schedule. Here's parallelism, retry limits, the missed-schedule trap, and history cleanup.
October 24, 2026