ADR-004: Use minimal, multi-stage Docker images for the service runtime container¶
| Field | Value |
|---|---|
| Date | 2026-03-20 |
| Status | Accepted |
| Deciders | TODO |
| Supersedes | — |
| Superseded by | — |
Context and Problem Statement¶
The service requires a runtime Docker image. The choice of base image for the runtime stage determines the attack surface, image size, CVE exposure, and the availability of debugging tools in production.
The containerization.md guideline mandates distroless or equivalently minimal runtime images.
The generated image/Dockerfile implements a three-stage build:
- builder — full toolchain, compiles the artifact.
- release — minimal runtime, no shell, no package manager. Used in production.
- debug — a package-manager-having base (e.g.
debian:bookworm-slim, or the JRE-Alpine image already used forrelease) with a shell and basic investigation tools (bash,curl,make) installed on top. Used for manual, time-boxed incident investigation only — never deployed automatically (seeADR-006-ci_pipeline_design.md, "Production debugging").
Decision Drivers¶
- No shell or package manager in the production image — reduced attack surface.
- No CVEs from unused OS packages.
- Smallest possible image size for the
releasestage. - The
debugstage must allow shell access and basic investigation tooling (curl,make, etc.) for troubleshooting, without shipping any of that to production. - Where the
releasebuild strips debug symbols, thedebugstage must still be able to symbolicate stack traces — without resorting to a differently-behaving (e.g. sanitizer-enabled) build (seeADR-006-ci_pipeline_design.md, "Build Configurations").
Considered Options¶
- Alpine Linux (
*-alpine) for all runtime stages. - Debian slim for all runtime stages.
- Distroless or language-minimal JRE images (language-dependent).
- Scratch (fully empty image).
Decision Outcome¶
Chosen option: Option 3 — language-appropriate minimal images. See image/Dockerfile
for the concrete base images used by this project.
Key constraint: no shell in the release image¶
The release stage does not contain a shell. docker exec <container> sh will fail
against a running production container.
To get a shell (and curl/make) for debugging:
# Build the debug variant locally:
docker build --target debug -t <name>:debug .
# Or pull the debug tag published by CI:
docker run --rm -it <registry>/<name>:<tag>-debug bash
Debug stage: tooling and debug symbols¶
The debug stage trades the release stage's minimalism for investigation capability — see
specs/guidelines/containerization.md ("Debug Runtime Stage") for the full rationale:
- Base image: not distroless — a real package manager (
apt/apk) is required to installbash,curl,make, etc. on top. - Binary/JAR: reused as-is from the
releasebuild wherever it already retains debug symbols (e.g. C++, Java); rebuilt with the same flags minus any stripping where thereleasebuild explicitly strips them (e.g. Go's-ldflags="-s -w"). Either way, this is the same optimization level and runtime behavior asrelease— not a different (e.g. sanitizer-enabled) build configuration.
Positive Consequences¶
- Minimal attack surface in production:
releasehas no shell, no curl, no apt/apk. - Smallest viable image size for the language's
releasestage. - CI builds the image with Kaniko — no privileged containers required.
Negative Consequences / Risks¶
- Teams must use the
debugimage tag (or a sidecar) for interactive debugging. - OS-level security fixes require a full image rebuild — not resolved by
apt-get upgrade. Schedule regular pipeline runs or image rebuilds to pick up base image updates.
Pros and Cons of the Options¶
Option 1 — Alpine Linux¶
- Pro: Shell available; familiar; easy to add tools with
apk. - Con: Shell and package manager present at runtime — unnecessary attack surface.
Option 2 — Debian slim¶
- Pro: glibc-compatible; familiar.
- Con: Larger than Alpine or distroless; includes apt, bash, many utilities — precisely why
it is used for the
debugstage instead, rather than forrelease.
Option 3 — Distroless / JRE-Alpine (chosen)¶
- Pro: Minimal attack surface; smallest image; no unnecessary tools in production.
- Con: No shell or tooling in the
releaseimage (by design). Use thedebugstage (a non-distroless base, see above) for troubleshooting.
Option 4 — Scratch¶
- Pro: Absolute minimum size; no OS layer.
- Con: Requires fully static binary including TLS certificates; adds build complexity.
Links¶
image/Dockerfile— three-stage Dockerfile for this projectspecs/guidelines/containerization.md— Containerization Guidelines ("Debug Runtime Stage")specs/guidelines/tool_chain.md— hermetic CI build requirements for CI build images (separate from runtime)specs/architecture/ADRs/ADR-006-ci_pipeline_design.md— "Build Configurations" and "Production debugging" sections this decision'sdebugstage design relies on