Stop Shipping the Kitchen Sink
We've all been there: you build a Docker image for a simple Python script, and it's 800MB. You pull it on a slow connection and watch the progress bar crawl. The image contains compilers, headers, and a full OS package manager you'll never use. It's time to trim the fat.
Start with the Right Base
Your base image sets the ceiling. ubuntu:latest is a convenience, not a goal. For most apps, you can go much smaller.
-
alpineis tiny (around 5MB) and uses musl, but sometimes native modules need extra work. -
debian:bookworm-slimis a middle ground: Glibc, but without the bloat. - For Go or Rust, consider
scratchordistroless.
Here's a quick comparison for a simple Python app:
# python:3.12-slim
FROM python:3.12-slim
# ~120MB
# python:3.12-alpine
FROM python:3.12-alpine
# ~50MB
# python:3.12 (full)
FROM python:3.12
# ~1GB
Always check the official images for slim or alpine variants. They exist for a reason.
Multi-Stage Builds: The Killer Feature
If you need compilers to build, don't ship them. Use a multi-stage build: compile in one stage, copy only the artifacts to a clean final stage.
# Build stage
FROM golang:1.22 AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o myapp .
# Final stage
FROM alpine:latest
RUN apk add --no-cache ca-certificates
COPY --from=builder /app/myapp /usr/local/bin/myapp
CMD ["myapp"]
The Go binary is static, so we copy it into a minimal Alpine. Final image size? Around 10MB. Compare that to shipping the Go toolchain.
Clean Up Within a Layer
If you're stuck with a single-stage build (legacy, or just quick), at least clean up in the same RUN command. Each RUN creates a layer, and files deleted in a later layer still exist in the previous one.
RUN apt-get update && apt-get install -y \
build-essential \
&& pip install --no-cache-dir -r requirements.txt \
&& apt-get purge -y build-essential \
&& apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/*
Note the && chain: this prevents intermediate layers from holding onto temporary files.
Use .dockerignore
Your build context can be huge if you're not careful. A stray node_modules or .git folder gets sent to the daemon, slowing builds and bloating cache. Add a .dockerignore file:
.git
node_modules
__pycache__
*.md
.env
Copy Specific Files
Instead of COPY . ., copy only what you need. This also helps with layer caching: if only your code changes, you don't invalidate the layer with dependencies.
COPY package.json package-lock.json ./
RUN npm ci --only=production
COPY src ./src
Don't Install Unnecessary Packages
That's obvious, but also think about package manager caches. pip install leaves .pyc files and caches. Use:
pip install --no-cache-dir -r requirements.txt
For npm, npm ci --only=production skips devDependencies and uses the lockfile.
Distroless and Scratch
For production, consider gcr.io/distroless images. They have no shell, no package manager, just your app and runtime. This reduces attack surface and size.
If your app is fully static (Go, Rust), you can use scratch:
FROM scratch
COPY --from=builder /app/myapp /myapp
ENTRYPOINT ["/myapp"]
That's as small as it gets: just the binary.
Check Your Layers
Use docker history to see what's taking space:
docker history myimage
You'll spot layers that are unexpectedly large. Also, docker images shows the total size, but remember that shared layers between images don't count twice.
Example: Slimming Down a Python API
Here's a before and after for a FastAPI app.
Before:
FROM python:3.12
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0"]
Size: ~1GB
After:
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin
COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0"]
Size: ~150MB. The trick is copying only the installed packages, not the entire builder image.
Final Thoughts
Bloat isn't just about download speed. Smaller images mean faster startups, less disk usage, and fewer security vulnerabilities. Start with a slim base, use multi-stage builds, and always clean up. Your future self (and your CI pipeline) will thank you.
Happy trimming!
Top comments (0)