Dockerfile Guidelines¶
General Rule¶
Images are self-contained. Containers do not load libraries, tools, or plug-ins etc. at runtime. Of cause, containers may operate on files mountend into them. For example, a container from an build image operates on source code mounted into the container.
Multi-Stage Builds¶
Always use multi-stage builds — separate build and runtime concerns:
- Build stage: full SDK image (language-specific builder or
debian:bookworm-slimwith tools installed). - Runtime stage: minimal or distroless image. Copy only the compiled artifact and required runtime files.
Preferred base images for the release (production) runtime stage:
| Language | Runtime image |
|---|---|
| C++ | gcr.io/distroless/cc-debian12 |
| Go | gcr.io/distroless/static-debian12 |
| Java | gcr.io/distroless/java25-debian13 |
| TypeScript (frontend-react) | nginx:alpine (pinned) |
Static Frontend Runtime Stage — the One Documented Distroless Exception¶
The frontend-react profile's release stage is the one documented exception to the
distroless-only rule above: a pinned nginx:alpine serves the static assets produced by the
builder stage (vite build's dist/ output). There is no application process running — no
interpreted or compiled program to keep off a general-purpose base image — only a static file
server, so the attack-surface argument for distroless does not apply the same way it does to a
service with a running application binary. No debug stage variant is needed here either: nginx
already ships a shell, and there is no compiled binary whose debug symbols could be stripped.
Debug Runtime Stage¶
Service Dockerfiles add a third stage, debug, alongside builder and release — for manual,
time-boxed incident investigation only. It is never deployed automatically (see
specs/architecture/ADRs/ADR-004-ci_pipeline_design.md, "Production debugging").
Unlike release, the debug stage is not distroless — it needs a package manager to hold
investigation tooling, which a minimal image cannot provide by design.
- Runtime artifact: wherever the
releasebuild already retains debug symbols (e.g. C++ without an explicitstrip, or a Java JAR compiled without-g:none), thedebugstage reuses that exact same binary/JAR — no separate build. Where thereleasebuild strips them (e.g. Go's-ldflags="-s -w"), thedebugstage uses a second build with the same flags minus the stripping — same optimization level and behavior, just with debug info retained. This is not the sanitizer-enabled Debug build configuration (seeADR-004-ci_pipeline_design.md, "Build Configurations") — it must not change runtime behavior or timing. - Extra tooling: the
debugstage's base image provides a real shell (bash) and basic investigation tools (curl,make, etc.) via its package manager — e.g.debian:bookworm-slimwithapt-get installfor glibc-linked binaries (C++, Go), or the same Alpine-based JRE already used for thereleasestage (Java) withapk add.
Base Images¶
- Pin base images to a specific version tag — never use
latest. - Keep base images up to date; automate with Dependabot or Renovate.
Layer Caching¶
- Copy dependency manifests first, install dependencies, then copy source — maximize cache reuse.
- Each
RUNinstruction should do one logical thing.
Security¶
- Run as a non-root user: create a dedicated user in the build stage; apply it in the runtime stage with
USER. - No secrets in any image layer; use
--mount=type=secretfor build-time secrets, environment variables at runtime. - Scan images for vulnerabilities in CI (e.g., Trivy).
Labels¶
Apply standard OCI labels:
LABEL org.opencontainers.image.version="<version>"
LABEL org.opencontainers.image.revision="<git-sha>"
LABEL org.opencontainers.image.source="<repo-url>"
Entrypoint¶
- Use
ENTRYPOINTfor the main binary;CMDfor default arguments. - Use exec form
["binary", "arg"], not shell form — avoids signal propagation issues. - Entrypoint scripts (if needed) reside in
image/.
Directives¶
- Always use multi-stage builds: build stage (SDK) + runtime stage (distroless/minimal)
- Runtime images: C++ →
gcr.io/distroless/cc-debian12, Go →gcr.io/distroless/static-debian12, Java →gcr.io/distroless/java25-debian13 - Pin all base image versions — never use
latest - Copy dependency manifests first, then source (maximize cache reuse); each
RUNdoes one logical thing - Run as non-root user; no secrets in any layer; scan with Trivy in CI
- Use
ENTRYPOINT ["binary", "arg"]exec form — not shell form - The
debugruntime stage is not distroless: it may add a shell and investigation tools (bash, curl, make) via a package manager, and — only where the release build strips debug symbols — a debug-symbol-retaining build of the same binary; never deployed automatically - The frontend-react profile's static-asset runtime is the one documented exception to distroless: a pinned
nginx:alpineserves the built assets, since no application process runs