ADR-004: Use minimal, multi-stage Docker images for service runtime containers¶
| Field | Value |
|---|---|
| Date | 2026-03-20 |
| Status | Accepted |
| Deciders | — |
| Supersedes | — |
| Superseded by | — |
Context and Problem Statement¶
Service projects require a runtime Docker image. The choice of base image for the runtime stage determines the attack surface, image size, the availability of debugging tools, and the number of CVEs introduced by the OS layer.
The containerization.md guideline mandates distroless or equivalently minimal runtime
images. The question is: which base image is appropriate per language, and how should
multi-stage builds be structured?
Decision Drivers¶
- Minimal attack surface: no shell, no package manager, no unnecessary OS utilities in production.
- No CVEs from unused system packages — the runtime image should contain only what is required to run the binary.
- Smallest possible image size.
- Three-stage build:
builder(full toolchain) →release(minimal runtime) →debug(minimal runtime with a shell, for troubleshooting only). - Must be buildable with Kaniko (no Docker daemon in CI).
Considered Options¶
- Alpine Linux (
alpine:3or language-specific*-alpine) for all runtime stages. - Ubuntu/Debian slim (
debian:bookworm-slim) for all runtime stages. - Distroless (
gcr.io/distroless/*) or language-minimal JRE images per language. - Scratch (empty image) for statically-linked binaries.
Decision Outcome¶
Chosen option: Option 3 — language-appropriate minimal images, because it provides the smallest viable runtime per language without requiring complex cross-compilation or musl libc changes:
| Language | Release stage base | Rationale |
|---|---|---|
| Go | gcr.io/distroless/static-debian12:nonroot |
CGO_ENABLED=0 produces a statically-linked binary with zero native deps; distroless/static has no libc at all. |
| C++ | gcr.io/distroless/cc-debian12:nonroot |
Conan deps linked statically; binary needs glibc+libstdc++ (present in distroless/cc) but no shell. |
| Java | eclipse-temurin:25-jre-alpine |
JVM cannot run on distroless without a complete JRE layer; Alpine JRE is the next-smallest option. |
The debug stage uses the :debug variant of the distroless image (which contains a busybox
shell) for Go and C++, and the standard JRE-Alpine (which already has a shell) for Java.
Positive Consequences¶
- Release images contain no shell, no package manager, no curl — dramatically reduced attack surface.
- Image sizes are minimal: Go distroless images are typically < 10 MB.
- Kaniko builds the image without a Docker daemon — compatible with GitLab CI without privileged containers.
- The
debugstage allowsdocker execand shell access for incident investigation without shipping shell tooling to production.
Negative Consequences / Risks¶
- No shell in the release image means
docker execinto a running container requires thedebugtag. Teams must be aware of this. - Distroless images do not receive OS package updates via
apt/apk— the image must be rebuilt to pick up OS-level security fixes. Mitigated by the hermetic CI build pipeline. - Java services use Alpine JRE rather than a fully distroless image; Alpine includes a shell
in the runtime image. Fully distroless Java images exist (
gcr.io/distroless/java21) but require additional Maven packaging configuration that is out of scope for the scaffold.
Pros and Cons of the Options¶
Option 1 — Alpine Linux for all stages¶
- Pro: Familiar;
apkavailable for adding tools; excellent community support. - Con: Includes shell, package manager, and many utilities not needed at runtime — larger attack surface.
- Con: Alpine uses musl libc; C++ binaries compiled against glibc (the default for most distros) require recompilation.
Option 2 — Debian slim for all stages¶
- Pro: glibc-compatible; familiar package management.
- Con: Significantly larger than Alpine or distroless; includes apt, bash, many utilities.
- Con: Higher CVE exposure from pre-installed packages.
Option 3 — Distroless / JRE-Alpine (chosen)¶
- Pro: Minimal attack surface; smallest image size; no unnecessary tools in production.
- Pro: Distroless images are maintained by Google with frequent security updates.
- Con: No shell in production (intended); requires debug variant or sidecar for troubleshooting.
- Con: Java requires Alpine JRE instead of true distroless — slight compromise for Java.
Option 4 — Scratch¶
- Pro: Absolute minimum size; no OS layer at all.
- Con: Requires fully static binary with no external dependencies, including TLS root
certificates. Certificates must be manually
COPY-ed into the image. Adds build complexity for little practical gain over distroless/static.
Links¶
internal/templating/go/files/service/image/Dockerfile.tmpl— Go service Dockerfileinternal/templating/cpp/files/service/image/Dockerfile.tmpl— C++ service Dockerfileinternal/templating/java/files/service/image/Dockerfile.tmpl— Java service Dockerfilespecs/guidelines/containerization.md— Containerization Guidelines (distroless mandate)specs/ADRs/ADR-002-arch-test-strategy-by-artifact-type.md— ADR-002 (build images)