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:<N>-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-debian<N> |
| Go | gcr.io/distroless/static-debian<N> |
| Java | gcr.io/distroless/java<V>-debian<N> |
| TypeScript (frontend-react) | nginx:alpine (pinned) |
<N> is the Debian release of the build image, <V> the Java version it compiles for. The
runtime's glibc and libstdc++ must be at least as new as the build image's; the same Debian
release guarantees that. A static Go binary links no C library, so for Go <N> is the current
Debian release.
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.
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 — it must not change runtime behavior or timing. - Base image:
debian:<N>-slimwith the Debian release of the build image (C++, Go), or an official JRE image with a package manager, such aseclipse-temurin:<V>-jre(Java). Pin the exact tag. - Extra tooling:
bashand the tools to look around in a running container and to inspect or edit its configuration files:ca-certificates,curl,jq,procps,ripgrep,vim-tiny(forvi), andyq(Mike Farah's, copied from its official image withCOPY --from). Install with--no-install-recommendsand remove the package lists, so the image grows by the tools only. Leave outgit: with Perl it doubles the image, and whoever needs it installs it in the running container. - Updates: Update the packages of the base image first, with its package manager, such as
apt-get upgrade -y(Debian, Ubuntu),apk upgrade(Alpine), ordnf upgrade -y(Fedora). A base image lags behind the bug and security fixes of its distribution, and the image scan counts a fix the distribution has released as fixable. Build images differ: they stay pinned and are kept current by a dependency bot.
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-debian<N>, Go →gcr.io/distroless/static-debian<N>, Java →gcr.io/distroless/java<V>-debian<N>, with<N>the Debian release of the build image (for static Go binaries, the current one) - 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:debian:<N>-slimor an official JRE image, its packages updated first (e.g.apt-get upgrade -y), with bash, ca-certificates, curl, jq, procps, ripgrep, vim-tiny, and yq, 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