Skip to content
rusthacks.dev
DevOps

Distroless Alpine Scratch and Debian Slim for Rust Images

Four runtime base images for Rust containers, compared on size, libc, debugging, TLS certificates and security. A decision table and the failure each choice invites.

3 min read
Summarize in AI
Flat illustration for the post “Distroless Alpine Scratch and Debian Slim for Rust Images”, composed around security.

The base image decides three things at once: how big the image is, how hard it is to debug and what C library the binary runs on. The last one causes outages when it is wrong. This post compares four common choices for a Rust service and says when each is right. It extends a small Docker image for Rust in the packaging steps of shipping Rust to production.

Rust 1.90, Debian 12.

The comparison

debian:bookworm-slimdistroless/cc-debian12alpinescratch
libcglibcglibcmuslnone
Shell and toolsyes (sh, apt)noyes (sh, apk)no
Runtime size (base)around 75 MBaround 25 MBaround 8 MB0
Build targetx86_64-unknown-linux-gnugnu...-musl...-musl static
CA certificatesadd packageincludedadd packagecopy in
Debug in containerdocker exec shneeds debug variantdocker exec shneeds a sidecar or copy

Sizes are approximate, vary by release and architecture, and are worth checking with docker images when you choose.

Debian slim

The unsurprising default. Build in rust:1.90-bookworm, run in debian:bookworm-slim. The same glibc on both sides eliminates a class of failures. You get a shell for docker exec, apt for a debugging tool and long security support.

Distroless cc

Dockerfile
FROM gcr.io/distroless/cc-debian12:nonroot
COPY --from=builder /app/target/release/myapp /myapp
ENTRYPOINT ["/myapp"]

It contains glibc, libgcc, certificates and tzdata, nothing else, and a nonroot user. A shell-less image cannot be exploited into spawning /bin/sh, and docker exec has nothing to run; use the :debug tag, which adds BusyBox, when you need to look inside. Health checks must be done by the orchestrator over HTTP, or by a subcommand of your own binary.

Alpine

Small and familiar, with apk. The catch is musl. DNS resolution has historically behaved differently, the default allocator is slower under multithreaded allocation, and crates that link C libraries may fail to build. If you pick it, build for x86_64-unknown-linux-musl in an Alpine builder stage. The trade-offs with static binaries are in static linking with musl.

scratch

Dockerfile
FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/myapp /myapp
USER 10001
ENTRYPOINT ["/myapp"]

The smallest image: the binary plus certificates. No shell, no /tmp, no /etc/passwd, so USER 10001 works by number only. If the program writes temporary files, mount a volume at /tmp. Everything about timezones, locale and user names must be embedded or avoided.

Decision

  • Default, or if unsure: Debian slim.
  • Security-minded and comfortable without a shell: distroless cc.
  • Static binary, minimal everything, you have tested musl behavior: scratch.
  • Alpine only if you already depend on it elsewhere.

Failure modes

A glibc binary on Alpine: exec /myapp: no such file or directory, because the dynamic loader is missing. A binary built on a newer distribution than the runtime: GLIBC_2.38 not found. A scratch image calling an HTTPS API: certificate verify failed, with no roots. Test the image with a real outbound call, not only the health check. The images are documented in the distroless repository.

Share

Keep reading

Get new posts by email

We send one email when a new post is published. No spam.

Unsubscribe in one click. No spam, ever.