Safeguard
Containers

Docker Scratch Image: The Security Case for Empty Bases

A Docker scratch image starts from nothing, and that emptiness is the point: no shell, no package manager, and almost no CVEs for a scanner to find.

Marcus Chen
DevSecOps Engineer
5 min read

A Docker scratch image is an explicitly empty base — no operating system, no shell, no package manager — and its security value is exactly that absence: there is almost nothing for a scanner to flag and almost nothing for an attacker to exploit inside the container. FROM scratch gives you a container that contains only the files you copy into it, which for a statically linked binary can mean an image with a single executable and nothing else.

The tradeoff is that everything a normal base provided — CA certificates, timezone data, a shell for debugging — you now provide deliberately or do without. That discipline is the whole security argument.

Why an empty base shrinks your attack surface

Most container CVEs come from the OS layer: an outdated libssl, a vulnerable bash, a package you never invoke but that ships in the base anyway. A debian or ubuntu base image can report dozens of findings on day one, none of which are your code. A scratch image reports essentially none, because those packages are not present.

Beyond the scan count, absence is a runtime defense. If an attacker achieves code execution inside a scratch container, there is:

  • No shell to spawn (/bin/sh does not exist), which breaks a large class of exploit payloads and reverse shells.
  • No package manager to pull in tools.
  • No standard utilities like curl or wget for exfiltration or lateral movement.

You cannot exploit what is not installed. That is a stronger guarantee than patching a base image you keep fully stocked.

Where scratch fits: static binaries

Scratch works cleanly for languages that compile to a static, dependency-free binary — Go and Rust are the common cases. A multi-stage build compiles in a full toolchain image, then copies just the binary into scratch:

# Build stage with the full toolchain
FROM golang:1.23 AS build
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /app ./cmd/server

# Final stage: nothing but the binary
FROM scratch
COPY --from=build /app /app
ENTRYPOINT ["/app"]

CGO_ENABLED=0 is the load-bearing flag: it forces a fully static binary with no dynamic links to the C library, which does not exist in scratch. Forget it and the container starts and immediately fails to find libc.

The things scratch does not give you, and how to add them back

An empty base means common runtime needs are missing. Add only what you actually require:

  • TLS root certificates. Without them, any outbound HTTPS call fails certificate verification. Copy them from the build stage:
COPY --from=build /etc/ssl/certs/ca-certificates.crt \
     /etc/ssl/certs/ca-certificates.crt
  • Timezone data, if your app formats times in non-UTC zones.
  • A non-root user. Scratch has no /etc/passwd, so create the user in the build stage and copy the file, then set a numeric UID:
USER 65534:65534

Running as a numeric non-root UID means that even the minimal container does not run its process as root, which limits what a compromise can touch on a shared host or mounted volume.

Scratch versus distroless

Scratch is the most extreme option. Google's distroless images sit one step up: they include CA certificates, timezone data, and a non-root user, but still no shell or package manager. Choose based on your binary:

  • Fully static (Go with CGO off, Rust with musl): scratch is viable and gives the smallest, quietest image.
  • Needs a few runtime files or dynamic linking: distroless is far less fiddly and still cuts most of the OS attack surface.

If you find yourself copying half a base image into scratch to make an app work, use distroless instead — you are recreating a minimal base by hand and will do it worse.

Scanning a scratch image still matters

An empty base does not mean an empty scan. Your compiled binary still embeds application dependencies — Go modules, Rust crates — and those have CVEs of their own. A container scan of a scratch image should focus on those application components:

# Even a scratch image's binary carries dependency CVEs
trivy image --scanners vuln myorg/app:scratch

The value of scratch is that the scan output is now almost entirely about your code and its libraries, not noise from an OS you never chose to run. Continuous scanning across a broader supply chain view — the kind an SCA platform such as Safeguard provides — keeps those application-layer findings deduplicated across every image you build. Our software composition analysis product covers the module and crate layer that a scratch image cannot hide.

FAQ

What is a Docker scratch image?

FROM scratch is an explicitly empty base image with no files at all. You build on it by copying in exactly what your application needs, typically a single statically linked binary, which produces a very small container with minimal attack surface.

Is a scratch image more secure than a regular base image?

Generally yes, for its attack surface. It has no shell, package manager, or standard utilities, which removes most OS-layer CVEs and breaks many exploit payloads. The tradeoff is that you must add runtime essentials like CA certificates yourself and cannot easily debug inside the container.

When should I use distroless instead of scratch?

Use distroless when your app needs a few runtime files, dynamic linking, or a pre-configured non-root user. It keeps most of scratch's security benefit while sparing you from hand-assembling a minimal base. Reserve scratch for fully static binaries.

Do I still need to scan a scratch-based image?

Yes. The binary you copy in embeds application dependencies (Go modules, Rust crates) that carry their own CVEs. A scan of a scratch image is mostly signal about your own code and libraries rather than OS noise.

Never miss an update

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