Server

How to Shrink a Docker Image

By Prabath Thalangama· October 24, 2026· 3 min read
#docker#optimization#dockerfile

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 same RUN with &&.
  • Alpine build fails compiling a Python/Node native dep — musl vs glibc. Add build-base/gcc musl-dev for the build, or switch to -slim.
  • distroless image won't start / can't debug — no shell. Use a :debug variant (has busybox) locally, or docker run --entrypoint sh won't work — use kubectl debug / an ephemeral container.
  • COPY . . still huge — .dockerignore missing 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.
PrabathStuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.