Container Security

Scratch vs Distroless: Choosing the Right Minimal Container Image

Both scratch and distroless promise minimal attack surface. The right choice depends on your runtime, your debugging needs, and your tolerance for complexity.

Bob
Cloud Security Architect
6 min read

The push toward minimal container images has produced two dominant options at the extreme end: scratch and distroless. Both aim to reduce attack surface to near zero. Both eliminate shells, package managers, and most system utilities. But they serve different use cases, and picking the wrong one creates friction that teams often resolve by abandoning minimalism entirely.

What scratch Actually Is

The scratch image is Docker's way of saying "start with nothing." It is not an image at all. It is a completely empty filesystem. When you build FROM scratch, your container contains only the files you explicitly add.

This means no libc, no certificate authority bundles, no timezone data, no user accounts, no /etc/passwd, nothing. Your binary must be entirely self-contained.

FROM scratch
COPY --from=builder /app/myapp /myapp
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
ENTRYPOINT ["/myapp"]

If your binary tries to dynamically link against a library, it fails. If it tries to resolve DNS using the system resolver, it fails. If it tries to read CA certificates from the default system path, it fails unless you copied them explicitly.

What Distroless Actually Is

Google's distroless images take a different approach. They include the minimal set of files needed to run a specific language runtime: libc, libssl, ca-certificates, timezone data, and a non-root user. But they exclude shells, package managers, and debugging utilities.

Distroless images are available for specific runtimes: Java, Python, Node.js, Go, .NET, and a base image for statically compiled binaries. Each image is built from Debian packages, providing glibc compatibility without the full Debian userland.

FROM gcr.io/distroless/base-debian12
COPY --from=builder /app/myapp /myapp
ENTRYPOINT ["/myapp"]

The distroless base image provides what scratch does not: working DNS resolution, TLS certificate verification, and timezone support out of the box.

Security Comparison

Attack Surface

Scratch wins on raw numbers. Zero files means zero vulnerabilities from the base image. The only CVEs that apply are in your application binary and whatever libraries it statically links.

Distroless is close but not zero. The base-debian12 image contains roughly 20 packages. These packages occasionally have CVEs, though Google's automated patching keeps them relatively current.

In practice, the difference is small. Both approaches eliminate the vast majority of attack surface compared to a full distribution image.

Shell Access

Neither scratch nor distroless includes a shell. This is a security feature because it prevents an attacker who gains code execution from easily pivoting to run arbitrary commands. Without /bin/sh, most post-exploitation toolkits fail.

However, a determined attacker can still achieve arbitrary code execution without a shell. They can use statically compiled tools, memory corruption, or binary exploitation techniques. The lack of a shell raises the bar but does not make exploitation impossible.

Runtime Security

Distroless images include a non-root user, making it straightforward to run your application without root privileges. Scratch images require you to create user entries manually by copying /etc/passwd and /etc/group from the build stage.

Both support read-only root filesystems, seccomp profiles, and AppArmor or SELinux policies. The choice between them does not significantly affect your ability to apply runtime security controls.

Practical Tradeoffs

Debugging

This is where the rubber meets the road. When something goes wrong in production, you need to investigate. Neither image gives you standard debugging tools.

For scratch, your options are limited to kubectl exec with an ephemeral container (if your cluster supports it), sidecar containers with debugging tools, or remote debugging over network connections.

For distroless, Google provides debug variants that include BusyBox. These are intended for development and troubleshooting, not production. You can keep debug images available in your registry and deploy them when needed.

The practical solution for both: define a debugging strategy before you go to production. Teams that adopt minimal images without a debugging plan invariably end up either adding a shell back to the image or building custom debug tooling.

Dependency Management

With scratch, you are responsible for every file in the image. If your application needs CA certificates, you copy them. If it needs timezone data, you copy it. This is explicit and auditable, but it also means you need to keep those files updated.

Distroless handles this for you. Google rebuilds distroless images regularly, updating CA certificates and timezone data. You get dependency management for the runtime layer without maintaining it yourself.

Build Complexity

Scratch images require more Dockerfile engineering. You need multi-stage builds, static compilation flags, and explicit file copying for every runtime dependency. This is more complex but also more transparent. You know exactly what is in the image because you put it there.

Distroless images simplify the Dockerfile but add a dependency on Google's build infrastructure. If Google changes the contents of a distroless image, your application might be affected. This is a supply chain consideration worth evaluating.

Language-Specific Guidance

Go

Go is the poster child for scratch images. With CGO_ENABLED=0, Go produces fully static binaries that run perfectly in scratch. Add CA certificates and timezone data if needed, and you are done.

If your Go application uses CGO (for SQLite, for example), you need libc. Distroless or Alpine become better choices.

Rust

Rust with the musl target produces static binaries suitable for scratch. With the default glibc target, use distroless or a minimal distribution image.

Java

Java requires a JVM, which requires libc and many supporting libraries. Distroless Java images provide a pre-configured JRE in a minimal environment. Running Java from scratch is not practical.

Python and Node.js

Interpreted languages need their runtime, which needs libc and supporting libraries. Distroless provides purpose-built images for these runtimes. Scratch is not an option.

.NET

.NET provides both self-contained and framework-dependent deployment models. Self-contained deployments on Linux still require libc, making distroless the practical minimum.

When to Choose Scratch

Choose scratch when your application compiles to a fully static binary, you want absolute control over the image contents, your team has the expertise to manage minimal images, and you have a debugging strategy that does not depend on in-container tools.

When to Choose Distroless

Choose distroless when your application needs a language runtime like Java, Python, or Node.js, you want minimal images without managing every runtime file, your team benefits from Google's automated patching of the base layer, or you want a simpler Dockerfile without sacrificing security posture.

How Safeguard.sh Helps

Safeguard.sh provides accurate vulnerability scanning for both scratch and distroless images. For scratch images, it analyzes the statically linked libraries embedded in your binary. For distroless images, it tracks the Debian packages included in the base layer and maps them to known CVEs. Safeguard.sh generates SBOMs that capture every component regardless of how minimal the image is, ensuring you have complete visibility into your container contents.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.