A junior developer reduced a Python Docker image from 1.2 GB to 85 MB, cutting the CI pipeline from 11 minutes to 90 seconds. Smaller images download faster, cost less to store, and expose fewer security flaws.
Why image size matters
Every time a container is pulled, the registry streams the whole image. An 85 MB layer lands in seconds; a 1.2 GB layer can take minutes on a modest network. Build agents must also upload and cache the full image, inflating CI time and cloud-storage bills. Each extra package is a potential vulnerability, so trimming the base shrinks the attack surface.
Where the bloat comes from
- Build tools such as gcc stay in the final image if they are installed in the same stage that runs the app.
- Package-manager caches (e.g.,
aptorpipcaches) sit on disk and are never cleaned up by default. - Every
RUNinstruction creates a new read-only layer; duplicated files across layers add up. - Large base images like
ubuntu:latestship with a full OS, far more than a minimal Python runtime needs.
The three-step shrink
| Step | Base image | Build approach | Resulting size |
|---|---|---|---|
| 1 | Standard Python (full) | Single stage, all tools present | 1.18 GB |
| 2 | python:slim |
Multi-stage: builder with gcc, final stage copies only compiled packages | 210 MB |
| 3 | python:alpine |
Multi-stage on Alpine Linux, which is itself tiny | 85 MB |
Step 1 – the baseline
Starting with the default Python image, the developer saw a 1.18 GB artifact. The image contained the full Debian stack, development headers, and the pip cache.
Step 2 – slim with a builder
Switching to python:slim trimmed the OS footprint, but the build tools remained. Adding a builder stage let gcc, make, and other compile-time dependencies install, compile, and then disappear. The final stage used COPY --from=builder to pull only the compiled wheels and runtime files, dropping the size to 210 MB.
Step 3 – Alpine wins
Alpine Linux builds on musl libc and busybox. Repeating the multi-stage pattern on Alpine produced an 85 MB image—a 93 % reduction from the original. The developer noted that Kubernetes pulled the image in seconds and the CI job finished in 90 seconds.
Real-world impact
- Lower storage costs – Registry storage shrinks.
- Improved security – Fewer packages mean fewer CVEs to track. The Alpine image contains only the Python runtime and the application code.
- Speedier CI – The pipeline’s runtime fell from 11 minutes to 90 seconds.
Practical tips for trimming your Dockerfile
- Avoid
:latesttags; pick:slimor:alpinevariants that match your needs. - Use multi-stage builds: a dedicated builder stage for compilation, a runtime stage that only receives the artefacts you actually need.
- Copy selectively with
COPY --from=builder /path/to/installed /path/in/final. - Order commands so that dependency installation runs before copying source code; this maximises layer caching.
- Clean caches explicitly, e.g.,
rm -rf /var/lib/apt/lists/* ~/.cache/pip.
If you adopt Alpine, add the nss package when your app experiences DNS resolution problems. When installing with pip, prepend /root/.local/bin to PATH so that locally installed scripts are found at runtime.
Caveats
Alpine’s musl libc can clash with binary wheels compiled for glibc, causing runtime errors. In those cases rebuild wheels inside the Alpine builder or fall back to a slim base. The extra nss package is a small price for DNS reliability.
What to watch next
- Scan your existing images for large layers that may be candidates for a multi-stage rewrite.
- Monitor CI logs for upload and download times to detect image bloat.
- Keep an eye on vulnerability reports for the base distribution you choose.
The lesson is clear: a disciplined Dockerfile, a lightweight base, and a builder stage can shrink a Python image by more than an order of magnitude, delivering tangible speed and cost benefits without sacrificing functionality.
