What does statically linking a Spring Boot native image achieve, and why would you pair it with a minimal container base image like distroless or scratch?
answer
- libc baked into binary
- no external .so needed
- distroless = no shell; scratch = empty
- smaller image + attack surface
- musl→scratch, mostly-static→distroless
basics
~20 sStatic linking bundles the C libraries the executable needs into the binary itself, so it doesn't depend on libraries installed in the container. That lets it run on a tiny or empty base image, giving a smaller, more secure container.
solid answer
~40 sWhen GraalVM compiles a Spring Boot app ahead-of-time into a native executable, that binary normally still depends on shared system libraries (chiefly the C library, libc) present in the container. Static linking copies those libraries' code into the executable so it has no external runtime dependencies. A self-contained binary can then run on a stripped-down base image: distroless (OS libs but no shell or package manager) or even scratch (completely empty). The payoff is a much smaller image, faster pulls/starts, and a drastically reduced attack surface — no shell, no apt, fewer CVEs to patch. You typically choose fully static + musl for scratch, or mostly-static (glibc left dynamic) for distroless.
code
kotlin · 15 lines// build.gradle.kts — GraalVM Native Build Tools plugin
plugins {
id("org.springframework.boot") version "3.3.0"
id("org.graalvm.buildtools.native") version "0.10.2"
}
graalvmNative {
binaries {
named("main") {
// Fully static + musl -> can run on 'scratch'
buildArgs.add("--static")
buildArgs.add("--libc=musl")
}
}
}go deeper
Know that static linking = no external library dependencies, enabling tiny base images (distroless/scratch) that are smaller and more secure.
Should map fully-static+musl→scratch and mostly-static→distroless, and know it's configured via buildArgs in Native Build Tools.
Should explain glibc-vs-musl tradeoffs and why glibc static linking is discouraged (NSS/DNS).
Should weigh operability (no shell for debugging), toolchain cost (musl toolchain), and CVE/compliance posture across a fleet.
## Background Spring Boot native images are produced by **GraalVM `native-image`**, driven from the build via **GraalVM Native Build Tools** — the Gradle plugin `org.graalvm.buildtools.native` (`graalvmNative { }` block) or the Maven `native-maven-plugin`. The output is a platform-specific executable, not a JAR, so there is no JVM at runtime. ## Linking basics Every Linux executable needs a **libc** (the C standard library — implementations are **glibc**, the default on most distros, or **musl**, a smaller alternative). By default the native image is **dynamically linked**: at startup the OS loader resolves shared libraries (`.so` files) such as `libc.so`, `libz`, etc., from the container's filesystem. If those files aren't in the image, the binary won't start. **Static linking** instead copies the needed library code *into* the executable at build time, so it carries its own dependencies and needs nothing from the base image. ## Why it matters for containers A statically linked binary can run on a **minimal base image**: - **distroless** (e.g. `gcr.io/distroless/base`) — has core OS libraries (including glibc) but **no shell, no package manager, no coreutils**. - **scratch** — the empty image: literally nothing but your binary. Benefits: 1. **Smaller images** — tens of MB instead of hundreds. 2. **Security / smaller attack surface** — no shell to exploit, no OS packages generating CVEs, nothing to patch. 3. **Faster** pulls and cold starts. ## The two common recipes - **Fully static + musl** → runs on `scratch`. Nothing external at all. - **Mostly-static** (everything static except glibc, which stays dynamic) → runs on **distroless**, which supplies glibc. ## When NOT to bother If you're already shipping on a normal `debian`/`ubuntu`/`eclipse-temurin` base that includes the needed libraries, dynamic linking works fine and is simpler. Static linking is an optimization for minimal-image deployments. Also note `--static` is **Linux-only** — you can't produce a fully static binary targeting macOS/Windows. ## Gotcha Static linking is about the *native binary's* system-library dependencies, not about your Java/Kotlin code. Your application classes are already AOT-compiled into the binary regardless.
- What's the difference between distroless and scratch here?distroless still ships core OS libraries (like glibc) but no shell or package manager, so it suits a mostly-static binary that leaves glibc dynamic. scratch is completely empty, so it needs a fully static binary (typically musl-based).
saying these in an interview costs you the question
- Thinking static linking is about bundling Java classes — those are AOT-compiled in regardless; it's about system libraries (libc).
- Believing you can build a fully static (`--static`) binary on macOS/Windows — it's Linux-only.