Distroless Images: Minimal Attack Surface

A standard ubuntu image contains ~25,000 files. An alpine image contains ~500 files. A Distroless image contains… your app and its dependencies. Nothing else.

Distroless images (maintained by Google) contain NO shell (/bin/sh), NO package manager (apt, apk), and NO useful tools (curl, wget, ls).

Why? Security. If an attacker breaks into your container, they can’t run wget http://malware.com, they can’t run apt-get install nmap, and they can’t even list files. They are trapped in a featureless void.


1. Interactive: Attack Surface Comparator

See what tools differ across image types. Fewer tools = Smaller Attack Surface.


2. Implementation: Multi-Stage Builds

Since Distroless has no package manager, you cannot install dependencies in it. You must build your app in a “Builder” stage (full OS) and copy the binary to the “Runner” stage (Distroless).

Go Example

# STAGE 1: Build
FROM golang:1.21 as builder

WORKDIR /app
COPY . .
# Statically compile the binary
RUN CGO_ENABLED=0 go build -o myapp main.go

# STAGE 2: Run
# gcr.io/distroless/static-debian11 is for static binaries (Go, Rust)
FROM gcr.io/distroless/static-debian11

COPY --from=builder /app/myapp /
CMD ["/myapp"]

Java Example

# STAGE 1: Build
FROM maven:3.8-openjdk-17 as builder
WORKDIR /app
COPY . .
RUN mvn package

# STAGE 2: Run
# gcr.io/distroless/java17-debian11 includes the JVM!
FROM gcr.io/distroless/java17-debian11

COPY --from=builder /app/target/app.jar /app.jar
CMD ["/app.jar"]

3. How to Debug?

“But wait, if there is no shell, how do I docker exec in to debug?”

You don’t.

  1. Logs: Rely on docker logs.
  2. Ephemeral Containers: Since Kubernetes v1.23, you can attach a “debug container” that shares the process namespace.
    kubectl debug -it my-pod --image=busybox --target=my-container
    
  3. Use :debug tag: Google provides debug versions of distroless (e.g., base:debug) that include a busybox shell. Use them only for local debugging.

4. Distroless Flavors

Image Use Case
gcr.io/distroless/static Go, Rust, C++ (Statically linked)
gcr.io/distroless/base C/C++ (Dynamically linked, includes glibc)
gcr.io/distroless/java17 Java applications (includes JVM)
gcr.io/distroless/nodejs20 Node.js applications
gcr.io/distroless/python3 Python applications (Experimental)

[!IMPORTANT] Why not Alpine? Alpine uses musl libc, while most apps are compiled against glibc. Distroless is based on Debian (glibc), offering better compatibility without the bloat.