Skip to content

ADR-003: Use minimal, multi-stage Docker images for the service runtime container

Field Value
Date 2026-03-20
Status Accepted
Deciders
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:

  1. builder — full toolchain, compiles the artifact.
  2. release — minimal runtime, no shell, no package manager. Used in production.
  3. debug — minimal runtime with a shell (busybox or Alpine). Used for incident investigation.

Decision Drivers

  • No shell or package manager in the production image — reduced attack surface.
  • No CVEs from unused OS packages.
  • Smallest possible image size.
  • The debug stage must allow shell access for troubleshooting without shipping shell tooling to production.

Considered Options

  1. Alpine Linux (*-alpine) for all runtime stages.
  2. Debian slim for all runtime stages.
  3. Distroless or language-minimal JRE images (language-dependent).
  4. 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 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 /busybox/sh

Positive Consequences

  • Minimal attack surface in production: no shell, no curl, no apt/apk.
  • Smallest viable image size for the language.
  • CI builds the image with Kaniko — no privileged containers required.

Negative Consequences / Risks

  • Teams must use the debug image 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.

Option 3 — Distroless / JRE-Alpine (chosen)

  • Pro: Minimal attack surface; smallest image; no unnecessary tools in production.
  • Con: No shell in the release image (by design). Use the debug stage for troubleshooting.

Option 4 — Scratch

  • Pro: Absolute minimum size; no OS layer.
  • Con: Requires fully static binary including TLS certificates; adds build complexity.
  • image/Dockerfile — three-stage Dockerfile for this project
  • specs/guidelines/containerization.md — Containerization Guidelines
  • specs/ADRs/ADR-002-hermetic-ci-build-images.md — decision for CI build images (separate from runtime)