skip to content

Static Linking & Minimal Base Images

Static or mostly-static linking against musl produces a self-contained binary you can drop into a distroless or scratch image. The natural end point of the small-container argument for going native.

part ofSpring Frameworkoverview, primer and where to startread it →
on this pageshow

explore

questions

4

Compare fully static linking (`--static --libc=musl`) with mostly-static linking (`StaticExecutableWithDynamicLibC`) for a Spring Boot native image. When would you pick each?

level: middleimportance: must knowfreq 45%

answer

  1. fully static = musl + everything → scratch
  2. mostly-static = glibc dynamic → distroless
  3. glibc static breaks NSS/getaddrinfo
  4. musl needs extra toolchain
  5. StaticExecutableWithDynamicLibC = -H: flag

basics

~20 s

Fully static (--static --libc=musl) links everything including libc, so the binary runs on an empty scratch image. Mostly-static links everything except glibc, which stays dynamic, so it runs on distroless (which provides glibc). Use musl-static for scratch, mostly-static for distroless.

solid answer

~40 s

Both produce self-contained-ish Spring Boot native binaries but differ in how they treat libc. **Fully static** (`--static` combined with `--libc=musl`) statically links *all* libraries, including the musl C library, yielding a binary with zero external dependencies that runs on `scratch`. It needs the **musl toolchain** installed on the build machine. **Mostly-static** (`-H:+StaticExecutableWithDynamicLibC`) links every library statically *except* the standard C library (glibc), which remains dynamically loaded — so it requires a base image that ships glibc, like **distroless**. Mostly-static exists because statically linking **glibc** is problematic (its NSS/`getaddrinfo` machinery misbehaves when static), so you keep glibc dynamic and static-link everything else. Pick musl-static when you want the absolute smallest scratch image; pick mostly-static when you're comfortable on distroless and want to avoid the musl toolchain and its edge cases.

code

kotlin · 12 lines
kotlin
graalvmNative {
    binaries {
        named("main") {
            // Option A: fully static, runs on scratch (needs musl toolchain)
            // buildArgs.add("--static")
            // buildArgs.add("--libc=musl")

            // Option B: mostly-static, runs on distroless (glibc stays dynamic)
            buildArgs.add("-H:+StaticExecutableWithDynamicLibC")
        }
    }
}

go deeper

for a junior

Know the two modes exist and their target images (scratch vs distroless).

for a middle

Should articulate the glibc-vs-musl split and match each mode to its base image and toolchain.

for a senior

Should explain the NSS/getaddrinfo root cause and the mutual exclusivity of the flags.

for a principal

Should decide fleet-wide policy weighing toolchain maintenance, DNS reliability, and image-size targets.

## The three linking modes GraalVM offers 1. **Dynamic (default)** — the binary loads shared libraries (glibc, libz, …) from the container at runtime. Needs a base image containing them (e.g. `debian`, `ubuntu`). 2. **Fully static** — `--static`. Links *everything* statically, including libc. On Linux this only works with a **static libc**; in practice that means **musl**, selected with `--libc=musl`. Result: no external dependencies → runs on **`scratch`**. 3. **Mostly-static** — `-H:+StaticExecutableWithDynamicLibC`. Statically links all libraries **except the standard C library (glibc)**, which stays dynamic. Result: needs only glibc at runtime → runs on **distroless** images that ship glibc. ## Why musl for fully static? **glibc is not designed to be statically linked.** Its **NSS (Name Service Switch)** subsystem — used by `getaddrinfo`/`gethostbyname` for DNS and hostname resolution — dynamically `dlopen`s plugin modules at runtime. A statically linked glibc binary can't load those, so hostname/DNS lookups may fail or warn. That's why fully static uses **musl** (which has no such dynamic-plugin requirement), and why the mostly-static mode deliberately leaves **glibc dynamic**. ## Toolchain requirements - **musl-static** requires a **musl toolchain** on the builder: `musl-gcc`, and typically `zlib` compiled against musl. This is extra setup (or a prebuilt builder image / buildpack). - **mostly-static** uses the normal glibc toolchain already present — less setup. ## Configuring in Spring Boot Native Build Tools Gradle (`org.graalvm.buildtools.native`) or Maven `native-maven-plugin` both accept `buildArgs` that pass straight through to `native-image`: - Fully static: `--static`, `--libc=musl` - Mostly-static: `-H:+StaticExecutableWithDynamicLibC` (an `-H:` host option; some GraalVM versions expose `--static-nolibc`) ## Decision guide | Want | Base image | Args | Toolchain | |------|-----------|------|-----------| | Smallest possible | `scratch` | `--static --libc=musl` | musl (extra) | | Small + safe DNS, easy build | `distroless` (glibc) | `-H:+StaticExecutableWithDynamicLibC` | glibc (default) | | Simplicity, don't care about size | `debian`/`ubuntu`/temurin | none (dynamic) | none | ## Gotchas - `--static` is **Linux-only**. - Don't combine `--static` with `-H:+StaticExecutableWithDynamicLibC` — they're mutually exclusive linking strategies. - Fully static + glibc (i.e. `--static` without musl) is **discouraged**: it links a static glibc and can break DNS via NSS.

  • Why is statically linking glibc discouraged?
    glibc's NSS (Name Service Switch) dlopens resolver modules at runtime for hostname/DNS lookups (getaddrinfo). A static glibc binary can't load them, so DNS resolution warns or fails. musl has no such dynamic-plugin need, which is why fully static uses musl and mostly-static keeps glibc dynamic.
  • What extra build-machine setup does `--libc=musl` require?
    A musl toolchain: musl-gcc plus supporting libraries such as zlib built against musl. Without it, the native-image build fails. Buildpacks or a prebuilt builder image can supply this.

saying these in an interview costs you the question

  • Claiming `--static` alone gives a safe fully-static binary on any Linux — without musl it statically links glibc and can break DNS.
  • Saying mostly-static runs on scratch — it needs glibc, so it needs distroless (or another glibc-bearing image).
  • Thinking musl static needs no special toolchain.

context

open as a page

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?

level: juniorimportance: should knowfreq 35%

basics

~20 s

Static 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.

open as a page

How do you actually configure static/mostly-static linking through Spring Boot's native build tooling (Gradle/Maven plugins and buildpacks), and what pitfalls hit teams?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Pass GraalVM flags via the Native Build Tools plugin's buildArgs — Gradle graalvmNative { binaries { named("main") { buildArgs.add("--static"); buildArgs.add("--libc=musl") } } } or Maven <buildArgs>. With Paketo buildpacks, use BP_NATIVE_IMAGE plus build-argument env vars. Ensure a musl toolchain exists on the builder.

open as a page

As an architect, weigh the tradeoffs of shipping Spring Boot native images as fully-static-on-scratch vs mostly-static-on-distroless vs dynamic-on-debian. What drives the decision?

level: principalimportance: nice to knowfreq 18%

basics

~20 s

Scratch+musl gives the smallest, hardest-to-attack image but adds toolchain complexity and near-zero debuggability. Distroless+mostly-static balances small size and reliable glibc DNS with easier builds. Debian+dynamic is simplest and most debuggable but largest. Choose by security posture, operability, and team maturity.

open as a page