A docker scratch image is FROM scratch — Docker's reserved, literally empty base with no filesystem, no shell, no package manager, and no OS layer at all. It's the smallest possible starting point for a Dockerfile image, which makes it the smallest possible attack surface, but that only pays off for statically compiled binaries that don't need anything from an operating system to run. For anything that shells out, reads /etc/resolv.conf in a way that assumes glibc, or expects a package manager to exist at runtime, scratch is the wrong choice.
What actually is a scratch image, and why is it different from a minimal one?
Scratch isn't a minimal image — it's the absence of an image. Alpine and distroless images still contain a real, if small, filesystem: Alpine ships BusyBox and apk; Google's distroless images ship a minimal libc and CA certificates but deliberately omit a shell. Scratch contains none of that. When you write FROM scratch in a Dockerfile, the resulting image format is just your application layers stacked on nothing — no base OS to patch, because there isn't one. That's why scratch images can shrink a container from hundreds of megabytes down to single-digit megabytes: a statically linked Go binary built with CGO_ENABLED=0 can produce a final image that's just that one binary, sometimes under 10MB total.
When does a scratch image actually make sense?
It makes sense when your binary is statically compiled and self-sufficient — Go is the most common fit, since go build with cgo disabled produces a single static executable with no dynamic library dependencies. Rust binaries built against the musl target work the same way. In both cases, you copy the compiled binary into a scratch image, maybe add a ca-certificates.crt file if the app makes outbound HTTPS calls, and you're done. The payoff is real: no OS-level CVEs to patch (there's no OS), a minimal image pull time, and a container that literally cannot spawn a shell if an attacker gains code execution, because there's no shell binary present to spawn.
When should you avoid scratch and use something else instead?
Avoid scratch the moment your app needs anything dynamic linking provides, needs to shell out to another binary, or needs a package manager for runtime dependencies — which rules out most interpreted-language apps (Python, Ruby, Node.js) unless you're willing to vendor a full interpreter and its shared libraries yourself, which is rarely worth the effort. It's also a poor fit for debugging: with no shell, you can't docker exec into a running scratch container to poke around when something breaks in production, which is a real operational cost teams underestimate until they hit an incident with no shell to fall back on. Google's distroless images are the middle ground worth knowing about — no shell either, but they include the language runtime (there are distroless bases for Java, Python, Node, and a static one comparable to scratch) so interpreted languages get most of the same attack-surface reduction without the all-or-nothing constraint scratch imposes.
What should you get right in the Dockerfile itself?
Beyond the base image choice, the Dockerfile image's metadata matters more than teams give it credit for. Use LABEL instructions to record build provenance — org.opencontainers.image.source, org.opencontainers.image.revision, and org.opencontainers.image.created are the standard OCI-recognized docker label keys, and populating them means a docker registry list images view or an SBOM generator can trace an image straight back to the commit and pipeline run that built it, without anyone maintaining that mapping by hand elsewhere. Multi-stage builds are what makes scratch practical in the first place — compile in a full golang:1.22 build stage, then COPY --from=build /app/binary /binary into a final FROM scratch stage, so the compiler toolchain never ships in the image you actually deploy. Pin your build-stage base image by digest, not just tag, since tags are mutable and a rebuild six months later can silently pull a different image than the one you tested against.
How Safeguard Helps
Safeguard scans container images layer by layer regardless of base — scratch, distroless, or Alpine — and flags when a base image tag isn't pinned to a digest, which is one of the most common supply-chain gaps in Dockerfile images that otherwise look hardened. It generates SBOMs automatically as part of the build so the provenance that LABEL instructions capture manually is backed by an actual verifiable component inventory. See the SCA product page for how image and dependency scanning fit together across your build pipeline.
FAQ
Can you run a shell inside a scratch container for debugging?
No — there's no shell binary in the image, so docker exec /bin/sh will fail. Ephemeral debug containers (kubectl debug with a sidecar, or Docker's --target debug stage pattern) are the workaround most teams use instead.
Is scratch more secure than Alpine?
It removes more attack surface since there's no OS at all, but Alpine with unnecessary packages stripped and no shell exposed is close enough in practice for many teams, and it's considerably easier to build and debug against.
Does a scratch image need a docker label to be OCI-compliant?
Labels aren't required for the image format to be valid, but the OCI image spec defines standard label keys for provenance metadata, and omitting them makes tracing an image back to its source significantly harder at scale.
What languages work well with scratch besides Go and Rust?
Any language that compiles to a fully static binary with no dynamic runtime dependencies works — C and C++ compiled with static linking flags are common examples — but most interpreted or JIT-based languages are a poor fit without significant extra work.