status: accepted date: 2026-03-12

ADR-0021: Distroless Container Images

Context and Problem Statement

The reference server is distributed as a container image. The base image choice affects image size, attack surface, build time, and debugging capabilities. We need a base image strategy that minimizes security risk and image size while still supporting the operational needs of a production service (health checks, graceful shutdown, signal handling).

Decision Drivers

  • Minimal attack surface – fewer packages mean fewer CVE exposure points
  • Small image size for fast pulls and reduced storage costs
  • The container must support health check endpoints and signal handling
  • Multi-stage builds should produce a clean separation between build and runtime artifacts

Considered Options

  • Alpine Linux base image
  • Debian slim base image
  • Google distroless base image

Decision Outcome

Chosen option: “Distroless base image with multi-stage Dockerfile”, because it provides the smallest possible attack surface by containing only the application binary, its runtime dependencies, and CA certificates – no shell, no package manager, no utilities that an attacker could exploit. The Dockerfile uses a multi-stage build: the first stage uses a full Rust toolchain image for compilation, and the final stage copies only the compiled binary into gcr.io/distroless/cc-debian12. The application exposes /healthz (liveness – returns 200 if the process is running) and /readyz (readiness – returns 200 if the database connection pool is healthy) health endpoints. The application handles SIGTERM for graceful shutdown, draining in-flight requests before exiting. Alpine was rejected because musl libc can cause subtle compatibility issues with some Rust crates. Debian slim was rejected because it includes a shell and package manager that increase attack surface unnecessarily.

Consequences

  • Good, because the runtime image contains no shell, package manager, or unnecessary utilities
  • Good, because image size is minimal (typically under 30MB for a Rust binary)
  • Good, because health endpoints enable Kubernetes liveness and readiness probes
  • Bad, because distroless images cannot be exec’d into for debugging (mitigated by using a debug variant in staging)
  • Bad, because the lack of a shell means troubleshooting must be done via application logs and external tooling