Docker image optimization cheatsheet.

Why size matters

  • Faster push/pull (CI, deploys).
  • Smaller attack surface.
  • Cheaper registry storage.
  • Faster cold starts in serverless.

Choose base image

FROM python:3.13              # ~1GB (full debian + dev tools)
FROM python:3.13-slim         # ~150MB (debian-slim, runtime only)
FROM python:3.13-alpine       # ~50MB (musl libc — beware C extensions)
FROM gcr.io/distroless/python3:debug  # ~40MB
FROM scratch                  # empty (static binaries only)

For Python with C extensions: slim is safer than alpine. For Go: scratch or distroless.

Multi-stage

FROM python:3.13 AS builder
WORKDIR /app
RUN apt-get update && apt-get install -y build-essential
COPY requirements.txt .
RUN pip install --user -r requirements.txt

FROM python:3.13-slim
COPY --from=builder /root/.local /root/.local
ENV PATH=/root/.local/bin:$PATH
COPY . .
CMD ["python", "app.py"]

Builder has compilers; final image only runtime deps.

Layer order: stable → volatile

# Bad: code change invalidates all layers below
FROM node:20
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build

# Good
FROM node:20
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

Combine RUN

# BAD: 2 layers, apt cache in image
RUN apt-get update
RUN apt-get install -y curl

# GOOD: 1 layer, clean cache
RUN apt-get update && \
    apt-get install -y --no-install-recommends curl && \
    rm -rf /var/lib/apt/lists/*

–no-install-recommends

RUN apt-get install -y --no-install-recommends curl ca-certificates

Skips suggested packages.

–no-cache-dir for pip

RUN pip install --no-cache-dir -r requirements.txt

BuildKit cache mount

# syntax=docker/dockerfile:1.7
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt

Persists pip cache between builds — but doesn’t bake it into image. Best of both worlds.

Squash with multi-stage

Many layers are fine if needed; final image is unioned. The trick: only keep what you need in final stage.

.dockerignore

.git
.venv
node_modules
__pycache__
*.pyc
.env
*.log
dist
build
.next
coverage
.DS_Store

Reduces build context.

Check image size

docker images myapp
docker history myapp
docker history --no-trunc myapp | head -20
dive myapp                       # https://github.com/wagoodman/dive

dive shows layer-by-layer wasted bytes.

strip / compress

For static binaries:

RUN strip /app/binary

For node:

npm prune --production

Pruning Python pycache

RUN find /usr/lib/python3 -depth -type d -name __pycache__ -exec rm -rf {} +

Or set PYTHONDONTWRITEBYTECODE=1.

Slim Node.js

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
CMD ["node", "server.js"]

For Next.js standalone output:

FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
CMD ["node", "server.js"]

Go static

FROM golang:1.22 AS build
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /out/app .

FROM scratch
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=build /out/app /app
ENTRYPOINT ["/app"]

Image ~10MB.

Rust static

FROM rust:1.78 AS build
WORKDIR /src
COPY . .
RUN cargo build --release --target x86_64-unknown-linux-musl

FROM scratch
COPY --from=build /src/target/x86_64-unknown-linux-musl/release/app /app
ENTRYPOINT ["/app"]

Distroless

FROM gcr.io/distroless/python3-debian12
WORKDIR /app
COPY --from=build /app /app
USER nonroot:nonroot
CMD ["app.py"]

No shell, no package manager, no debug tools. Best for production.

:debug variant has a shell for troubleshooting.

Avoid common bloat

  • Test files / docs / examples in production image.
  • npm install instead of npm ci --omit=dev.
  • .git directory copied in.
  • Logs / cached data baked in.
  • Multiple Python versions.

Image layers tip

docker image inspect myapp --format='{{json .RootFS.Layers}}' | jq length

Fewer is better but not a strict rule. Caching benefits > raw layer count.

Common mistakes

  • apt-get install then apt-get clean in separate RUNs → cache leaked.
  • RUN cd /dir && cmd — cd doesn’t persist; use WORKDIR.
  • COPY a giant context.
  • FROM ubuntu instead of :slim.
  • Not pruning dev dependencies before final stage.

Read this next

If you want my distroless templates (Python/Go/Node), they’re at rajpoot.dev .


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .