Comparison & limitations¶
Why not just static-link + FROM scratch?¶
If you can rebuild your service as a static binary (CGO_ENABLED=0, musl), do that. You
need no packer. Scratchsmith is built for the binaries that static linking cannot help:
- Closed-source or vendor binaries you cannot recompile.
- glibc binaries that rely on NSS,
dlopen, or locale behavior. Static glibc breaks these quietly, often only in production. - Anything where "just rebuild it static" is not on the table.
That is the wedge, and it is not a fence. Scratchsmith already packs a static binary too, because the resolver detects it and stages the single file. When you want an SBOM, a hardening report, a non-root image, and no Dockerfile, it is equally handy. You hand-write no multi-stage build. It is purpose-built for the hard case and useful for the easy one.
How Scratchsmith compares¶
Every tool here builds smaller and safer images. They differ mainly in what you feed them.
| Input | No Dockerfile | Prebuilt dynamic glibc binaries | Built-in SBOM + hardening | Daemonless build | |
|---|---|---|---|---|---|
| Scratchsmith | a prebuilt dynamic ELF binary | ✅ | ✅ (the whole point) | ✅ SBOM + ELF hardening lint | ✅ --push / --oci-archive (the default sink uses docker load) |
Static + FROM scratch |
source you can static-link | ✅ (COPY) |
❌ must be static | ❌ | ✅ |
| Docker multi-stage | a Dockerfile + source | ❌ | ✅ (you hand-craft it) | ❌ | ❌ needs daemon / BuildKit |
| apko | apk packages | ✅ | ❌ needs apk packaging | ✅ | ✅ |
| ko | Go source | ✅ | ❌ Go only | partial | ✅ |
| slim | an already-built image | ✅ | ✅ (minifies) | security profiles | ❌ needs a built image first |
Scratchsmith owns the one input the others do not serve: an arbitrary prebuilt dynamic binary.
Snapshot as of 2026-08. The other tools evolve, so read their own docs for current behavior. Only Scratchsmith's row is kept current here.
Limitations¶
- glibc / host-arch only (for now). Dynamic musl/Alpine binaries are detected by their interpreter and rejected with a clear error before anything is staged. You never get a silently broken image. This is a v1 scope choice, not a permanent boundary: musl uses a different loader model and has no NSS, so it needs its own resolver backend (a possible future addition). (A static musl binary is a single self-contained file and packs fine today.) No cross-arch resolution yet.
dlopenis best-effort. Libraries loaded at runtime viadlopenare invisible to static analysis. Scratchsmith warns about adlopenreference in the binary's dynamic symbol table, and it lets you force-stage those libraries with--include <lib>. It is not a blanket "any binary just works" guarantee.- Loading into a container engine is the default sink. The default hands the image to a
local engine for convenience:
dockerby default, or--runtime podman|nerdctlfor those. You can also go daemon-free entirely with--push <ref>(straight to a registry),--oci-archive <file>(an OCI archive), or--no-build --output(a rootfs). - Signing the image
packproduces needs--push.pack --push --signcosign-signs the pushed image by digest (keyless).--sbom --signattaches the SBOM as a signed attestation. Both apply only to the registry-push sink, because cosign signs a registry image. Release artifacts are separately cosign-signed with SLSA provenance (Verifying releases).