Skip to main content

How to build distroless container images for minimal attack surface

Distroless images ship only your app and runtime, with no shell or package manager, minimizing size and CVEs. Build in an earlier stage, copy artifacts in, run as nonroot, and use the debug tag only when needed.

Difficulty
Intermediate
Duration
30 minutes
Steps
6

Distroless container images

A distroless image contains only your application and its runtime dependencies, no package manager, no shell, and no general-purpose utilities. The result is a tiny image with very few known vulnerabilities, because there is almost nothing to be vulnerable. The trade-off is that you cannot exec a shell into it, so debugging needs a different approach.

Prerequisites

  • Docker installed.
  • An application that produces a static binary, or runs on a supported runtime such as Java, Python, or Node.

Steps

1. Understand distroless images

Distroless images are published for common runtimes. Because they ship no shell, they remove a class of attacks that rely on running commands inside a compromised container.

2. Pick a distroless base

Match the base to your runtime, for example gcr.io/distroless/static, gcr.io/distroless/base, gcr.io/distroless/java21, or gcr.io/distroless/nodejs20. Use static for fully static binaries and base when you need glibc.

3. Build artifacts in an earlier stage

Distroless has no compiler, so build in a normal image first:

FROM golang:1.22 AS build
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /app ./cmd/server

4. Copy into the distroless stage

FROM gcr.io/distroless/static:nonroot
COPY --from=build /app /app
ENTRYPOINT ["/app"]

5. Run as the nonroot user

The :nonroot tag runs as an unprivileged user by default. Make sure file paths your app writes to are writable, or mount a volume. Avoid hardcoding root-owned paths.

6. Debug with a debug variant

When you must inspect the container, use the :debug tag, which adds a BusyBox shell:

docker run --rm -it gcr.io/distroless/static:debug sh

Keep :debug out of production images.

Verification

Build and run the image: docker build -t myapp:distroless . && docker run --rm myapp:distroless. Confirm there is no shell: docker run --rm myapp:distroless sh should fail. Scan the image and observe far fewer CVEs than a full base image.

Next Steps

Add image scanning to confirm the reduced CVE count, sign images for supply-chain integrity, and enforce non-root execution in Kubernetes with a security context and pod security standards.

Prerequisites

  • Comfortable with multi stage builds
  • An app that compiles to a binary or runs on a known runtime

Steps

  • 1
    Understand distroless images
  • 2
    Pick a distroless base
  • 3
    Build artifacts in an earlier stage
  • 4
    Copy into the distroless stage
  • 5
    Run as the nonroot user
  • 6
    Debug with a debug variant

Category

Containers