CoursesAdvanced container securityMinimal & secure images

Static binaries & tiny bases

Go, Rust, and C services at a few megabytes.

Advanced14 min · lesson 7 of 25

The smallest, safest image is a single static binary on an empty base. A statically linked binary carries all its dependencies inside itself, so it needs no shared libraries, no loader, no userland at all — which means it can run on scratch, the literally-empty base image. This is the endgame of low-footprint: a service in a few megabytes with zero OS attack surface, because there is no OS.

Dockerfile
# Go: CGO off => a fully static binary; -s -w strips debug info to shrink it
FROM golang:1.23 AS build
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /server ./cmd/server
FROM scratch # nothing. at all.
COPY --from=build /server /server
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
USER 10001:10001
ENTRYPOINT ["/server"]

What scratch forces you to be explicit about

scratch has nothing, so anything the binary assumes must be copied in deliberately: CA certificates for outbound TLS (or HTTPS calls fail with an x509 error), tzdata if it does timezone math, and /etc/passwd if it needs to resolve a username. That explicitness is a feature — you can see every single thing in the image, because you put each one there. Static Rust (musl target) and statically linked C follow the same pattern.

Dockerfile
# Rust: build against musl for a fully static binary, then ship on scratch
FROM rust:1-alpine AS build
RUN apk add --no-cache musl-dev
WORKDIR /src ; COPY . .
RUN cargo build --release --target x86_64-unknown-linux-musl
FROM scratch
COPY --from=build /src/target/x86_64-unknown-linux-musl/release/svc /svc
USER 10001:10001
ENTRYPOINT ["/svc"]

Scratch vs distroless: which minimal

Both are minimal; choose by dependency shape. scratch is right for a truly self-contained static binary — smallest possible, nothing to attack. Distroless is right when you need a little userland: glibc, CA certs and tzdata already present, a non-root user preconfigured, and language variants for Java or Python that cannot be a single static file. For a Go or Rust service that links statically, scratch wins; for most everything else, distroless is the pragmatic floor.

Static images are hard to debug — plan for it
A scratch image has no shell, no ls, no way in — by design. Do not add tools “just in case”; that reintroduces the surface you removed. Instead rely on ephemeral debug containers (docker debug / kubectl debug) that attach tooling at runtime, and keep good logs and health endpoints so you rarely need to open the box at all.