跪拜 Guibai
← All articles
Docker · Backend · Operations

How a 1.2GB Docker Image Shrank to 128MB: The Pitfalls and a Repeatable Playbook

By 张龙687 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Oversized images are a tax on every part of the delivery pipeline: slow CI, sluggish autoscaling, ballooning registry costs, and a flood of irrelevant CVEs. The techniques here are language-agnostic and the highest-impact steps—`.dockerignore` and multi-stage builds—can be applied in an afternoon with near-zero risk, making this a high-leverage practice for any team running containers.

Summary

A production Node.js order service ballooned to a 1.2GB Docker image, dragging down CI pipelines, Kubernetes scaling, and security posture. A systematic two-day effort squeezed it to 128MB while dropping the build time from over 6 minutes to 52 seconds. The process starts with measurement tools like `docker history` and `dive` to pinpoint the fat, then applies five cuts: a `.dockerignore` file, multi-stage builds, a switch to `-slim` base images, BuildKit cache mounts, and finally Google's distroless runtime images.

The biggest wins come from the first two steps, which carry almost no risk. `.dockerignore` alone removed 118MB by preventing `.git` and `node_modules` from entering the build context. Multi-stage builds then shed over 700MB by discarding devDependencies and build toolchains from the final image. The article also catalogs real-world traps: Alpine's musl libc breaks native modules and causes sporadic DNS failures in Kubernetes, `rm`-ing files in a later layer doesn't reclaim space due to union filesystem whiteouts, and distroless images lack a shell, forcing a rethink of debugging workflows.

A companion Go example shows the extreme end of the spectrum: a statically compiled binary on a `distroless/static` base yields a 14.3MB image. The piece closes with a 13-item checklist that any team can follow to audit and shrink their own images, emphasizing that measurement must precede optimization.

Takeaways
`docker history` and the `dive` tool pinpoint exactly which layers and files bloat an image; never optimize by guesswork.
A `.dockerignore` file is the single highest-ROI change, often cutting hundreds of megabytes by excluding `.git`, `node_modules`, and secrets from the build context.
Multi-stage builds separate the build environment from the runtime, discarding compilers, devDependencies, and source code that are never needed in production.
Default to `-slim` base images; Alpine saves only ~65MB more but introduces musl libc incompatibilities and DNS resolution quirks that are hard to debug in Kubernetes.
BuildKit cache mounts keep package manager caches across builds without adding bytes to the image, dropping dependency install times from 95 seconds to 12 seconds on cache hits.
Google's distroless images contain no shell, package manager, or libc—just the runtime and essentials—yielding a 14.3MB Go service or a 128MB Node.js service.
Deleting a file in a later Dockerfile layer does not shrink the image; the data remains in a lower layer with only a whiteout marker added on top.
Pinning base images to a digest and combining with Renovate/Dependabot balances reproducibility with automated security updates.
Security CVEs drop dramatically as a side effect of slimming: the removed gigabytes contain unused system packages like perl, git, and python2 that carry most of the vulnerabilities.
A 13-step checklist covers the full optimization sequence, from initial measurement through CI cache configuration and final vulnerability scanning.
Conclusions

Image slimming follows a Pareto distribution: 80% of the size reduction comes from `.dockerignore` and multi-stage builds, both of which are low-risk and quick to implement. The remaining 20%—Alpine, distroless, cache mounts—carry disproportionate complexity and should be evaluated against concrete deployment patterns rather than applied dogmatically.

The Alpine vs. slim debate is often framed as a size question, but the real cost is operational: musl's different DNS resolver behavior causes intermittent failures in Kubernetes that can take days to diagnose, a risk that dwarfs the 65MB saved.

Distroless images force a healthy constraint: without a shell, teams must build proper observability—health endpoints, structured logging, metrics—rather than relying on `docker exec` as a crutch for debugging.

The union filesystem's whiteout mechanism is a persistent source of confusion; developers routinely `rm` files in Dockerfiles expecting size savings that never materialize, because the data remains in a lower layer and still ships in the image.

BuildKit's parallel stage execution is an underappreciated benefit of splitting `deps` and `builder` stages: when the two stages have no dependency on each other, they run simultaneously, cutting wall-clock build time beyond what layer caching alone achieves.

Security scanning is an accidental beneficiary of image slimming: the packages that bloat images—gcc, perl, python2, git—are also the ones carrying the bulk of CVEs, so removing them is effectively a security hardening measure disguised as an optimization.

Concepts & terms
Union filesystem whiteout
Docker images are composed of read-only layers stacked via a union filesystem. Deleting a file in a later layer does not remove it from the image; instead, a whiteout marker is placed in the new layer to hide the file from view. The original data remains in the lower layer and still contributes to the total image size.
Multi-stage build
A Dockerfile technique using multiple FROM statements to define separate build stages. Intermediate stages can contain compilers and devDependencies, while the final stage copies only the runtime artifacts, producing a minimal production image without the build toolchain.
BuildKit cache mount
A BuildKit feature (`RUN --mount=type=cache,target=...`) that mounts a persistent cache directory into a build step. The cached data survives across builds but is not included in the final image layer, enabling fast dependency installs without bloating the image.
distroless
Google's family of minimal container images that contain only the application runtime and essential files (ca-certificates, timezone data, /etc/passwd). They have no shell, package manager, or libc, reducing attack surface and image size to as little as 2MB for static binaries.
musl libc
A lightweight C standard library used by Alpine Linux as an alternative to glibc. While smaller, it can cause compatibility issues with precompiled native Node.js modules built against glibc, and its DNS resolver behaves differently in Kubernetes environments, sometimes causing intermittent resolution failures.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗