How to Shrink Docker Images by Up to 99% Across Go, Java, Rust, and More
This article explains practical techniques for drastically reducing Docker image sizes—including Go binary static linking, Alpine vs. slim base images, multi‑stage builds for Java, Rust, and interpreted languages, and detailed size comparisons—so developers can choose the optimal base image and build strategy for any language.
1. Go language image slimming
The Go compiler bundles all required dependencies into the binary, but some packages (e.g., DNS) rely on the system's standard library via cgo. Importing net creates a dynamic binary that needs system libraries; you can either copy those libraries into the image or use a busybox:glibc base image.
Disabling cgo (by setting CGO_ENABLED=0) forces Go to use its own implementations, producing a fully static binary that can run in a scratch image:
FROM golang
COPY whatsmyip.go .
ENV CGO_ENABLED=0
RUN go build whatsmyip.go
FROM scratch
COPY --from=0 /go/whatsmyip .
CMD ["./whatsmyip"]You can also keep cgo but specify built‑in libraries with the -tags netgo flag:
$ go build -tags netgo whatsmyip.go2. Alpine image deep dive
Alpine is a tiny Linux distribution using the apk package manager. Compared with Ubuntu, Debian, or Fedora, Alpine provides only about 10 000 packages versus >50 000, resulting in a base image size of ~5 MB.
Installation speed is also fast; a simple benchmark shows installing tcpdump takes 1–2 seconds on Alpine, while the same operation takes 7–60 seconds on larger distributions.
Base image Size Time to install tcpdump
---------------------------------------------------------
alpine:3.11 5.6 MB 1-2s
archlinux:20200106 409 MB 7-9s
centos:8 237 MB 5-6s
debian:10 114 MB 5-7s
fedora:31 194 MB 35-60s
ubuntu:18.04 64 MB 6-8sWhen using Alpine as the second stage ( run) or for all stages, you must consider its use of musl libc instead of glibc. Binaries compiled against musl run fine, but those linked to glibc will fail unless you install the appropriate compatibility libraries.
Run stage using Alpine
FROM gcc AS mybuildstage
COPY hello.c .
RUN gcc -o hello hello.c
FROM alpine
COPY --from=mybuildstage hello .
CMD ["./hello"]The container may fail with
standard_init_linux.go:211: exec user process caused "no such file or directory"because Alpine uses musl libc, which is not binary‑compatible with glibc.
All stages using Alpine
For Go you can use golang:alpine as the build stage and copy the static binary into a final alpine image (≈7.5 MB). For C you need to install build-base in the first stage:
FROM alpine
RUN apk add build-base
COPY hello.c .
RUN gcc -o hello hello.c
FROM alpine
COPY --from=0 hello .
CMD ["./hello"]Install build-base (equivalent to Ubuntu's build-essential ) to get the compiler, standard library, and make.
3. Java image slimming
Java programs run on a JVM, which dynamically links to the standard library (JAR/WAR files). To produce a smaller image you typically use a multi‑stage build: a JDK in the build stage and a JRE in the run stage.
Popular base images include:
openjdk:8-jre-alpine (85 MB)
openjdk:11-jre (267 MB) or openjdk:11-jre-slim (204 MB)
openjdk:14-alpine (338 MB)
Example Java "Hello, world" Dockerfile:
FROM openjdk:11-jdk-alpine
COPY hello.java .
RUN javac hello.java
FROM openjdk:11-jre-alpine
COPY --from=0 hello.class .
CMD ["java", "hello"]4. Interpreted language image slimming
For languages like Node, Python, or Rust, Alpine works well only when the program uses the standard library and does not require external C dependencies. When such dependencies exist, you either need Alpine‑specific packages (rare) or you must compile them yourself, which defeats the size‑saving purpose.
Python data‑science stacks (numpy, pandas, matplotlib) are compiled against glibc, so they fail on Alpine unless you install many build tools, dramatically slowing the build.
When Alpine is unsuitable, a xxx:slim image (based on Debian/glibc) is a good compromise, offering a smaller footprint while still providing the necessary libraries.
Image Size
---------------------------
node 939 MB
node:alpine 113 MB
node:slim 163 MB
python 932 MB
python:alpine 110 MB
python:slim 193 MB
ruby 842 MB
ruby:alpine 54 MB
ruby:slim 149 MB5. Rust image slimming
Rust binaries dynamically link to libdl and need a glibc‑based base image (e.g., Ubuntu, Debian). They cannot run in busybox:glibc because that image lacks libdl. Using rust:alpine works if you compile with musl libc for a fully static binary, which can then be placed in a scratch image.
6. Summary
The optimal base image depends on the language and its dependencies. Alpine yields the smallest images for many compiled languages when static linking is possible, but for interpreted languages with heavy C‑based dependencies, slim or full‑size images may be more practical. Experimentation and experience will guide the right choice.
Footnotes
Two clever tricks to reduce Docker image size by 99%: https://fuckcloudnative.io/posts/docker-images-part1-reducing-image-size/
Natanel Copa's talk: https://dockercon.docker.com/watch/6nK1TVGjuTpFfnZNKEjCEr
musl documentation: https://wiki.musl-libc.org/functional-differences-from-glibc.html
Amazon Corretto: https://hub.docker.com/_/amazoncorretto
Repository with examples: https://github.com/jpetazzo/minimage
Python wheel format: https://pythonwheels.com/
Alpine Python build slowdown article: https://pythonspeed.com/articles/alpine-docker-python/
Rust static linking guide: https://doc.rust-lang.org/1.9.0/book/advanced-linking.html#static-linking
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
