1. Why harden the base image? #
What an image really contains.
The finding #
An image inherits everything its base image contains: shell, package manager, libraries. Each package is a potential vulnerability and one more tool for an attacker who got into the container. We scanned two images with Trivy:
| Image | HIGH and CRITICAL vulnerabilities | With a fix available |
|---|---|---|
| PHP image on Alpine 3.18, built in 2023, never rebuilt | 23 (3 CRITICAL) | 23: all fixed since |
| Official nginx 1.30.6 (Debian), November 2026 | 71 (1 CRITICAL) | only 2 |
Two lessons: an image that is not rebuilt accumulates flaws already fixed upstream, and a full image carries dozens of unfixed vulnerabilities that a scanner will report without you being able to act. The answer: start from a minimal base and rebuild often.
2. Image families #
From simplest to most hardened.
Comparison #
| Family | Example | Strengths | Limits |
|---|---|---|---|
| -slim | node:22.11-bookworm-slim | Compatible, fewer packages | Shell and apt still present |
| Alpine | python:3.13-alpine | Very small | musl: possible incompatibilities with some libraries |
| Distroless (Google) | gcr.io/distroless/nodejs22-debian12:nonroot | No shell or package manager, nonroot variant | Less direct debugging |
| Docker Hardened Images | dhi.io/… | Over 1,000 Debian and Alpine images, free (Apache 2.0) since December 2025, SBOM and attestations | Docker Hub login required; FIPS and extended support paid |
| Chainguard | cgr.dev/chainguard/… | Minimal images rebuilt continuously | Older versions reserved for the paid offer |
The hardened Dockerfile #
The principle: build in a full image, then copy only the result into a minimal image, run as non-root.
# syntax=docker/dockerfile:1
FROM node:22.11-bookworm-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY . .
FROM gcr.io/distroless/nodejs22-debian12:nonroot
WORKDIR /app
COPY --from=build /app /app
USER 65532:65532
EXPOSE 3000
CMD ["server.js"]- Pinned versions: never
latest, for reproducible builds. - Multi-stage: build tools stay in the
buildstage. - Numeric user:
USER 65532:65532, which Kubernetes can check withrunAsNonRoot.
3. Running a shell-less image #
Debugging and maintaining.
Debugging #
Without a shell, docker exec -it … sh no longer works. Use a debug container sharing namespaces:
docker run --rm -it --pid=container:app --network=container:app busybox:1.37 sh
# Kubernetes
kubectl debug -it pod/app --image=busybox:1.37 --target=appRebuild regularly. A hardened image ages like any other: rebuild it at least weekly, and automate base image updates (Renovate, Dependabot).
