RousselTMLEARNING

Observabilité

Hardened images: reducing your containers’ attack surface

Choosing a hardened base image: slim, distroless, Docker Hardened Images, Chainguard; non-root, pinned versions and regular rebuilds.

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:

ImageHIGH and CRITICAL vulnerabilitiesWith a fix available
PHP image on Alpine 3.18, built in 2023, never rebuilt23 (3 CRITICAL)23: all fixed since
Official nginx 1.30.6 (Debian), November 202671 (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.

From full to hardened: full image, slim, distroless, hardened image

2. Image families #

From simplest to most hardened.

Comparison #

FamilyExampleStrengthsLimits
-slimnode:22.11-bookworm-slimCompatible, fewer packagesShell and apt still present
Alpinepython:3.13-alpineVery smallmusl: possible incompatibilities with some libraries
Distroless (Google)gcr.io/distroless/nodejs22-debian12:nonrootNo shell or package manager, nonroot variantLess direct debugging
Docker Hardened Imagesdhi.io/…Over 1,000 Debian and Alpine images, free (Apache 2.0) since December 2025, SBOM and attestationsDocker Hub login required; FIPS and extended support paid
Chainguardcgr.dev/chainguard/…Minimal images rebuilt continuouslyOlder 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 build stage.
  • Numeric user: USER 65532:65532, which Kubernetes can check with runAsNonRoot.

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=app
Rebuild regularly. A hardened image ages like any other: rebuild it at least weekly, and automate base image updates (Renovate, Dependabot).

Sources #

Related articles

Linting Dockerfiles with Hadolint and docker build --check

Spot Dockerfile errors and bad practices before building: Hadolint, its rules, thresholds, and BuildKit built…

Scanning Docker images: Trivy, Grype and Docker Scout

Detect image vulnerabilities and misconfigurations with Trivy, read results, filter what is fixable and block…

SBOM: the component inventory of your images

Generate a CycloneDX or SPDX SBOM, rescan it without rebuilding and know in seconds which images a new flaw a…

Keep learning

Support us

♥ SUPPORT US

Support RousselTM Learning

Courses, articles, labs and certificates stay free with no intrusive ads. A voluntary contribution, of any amount you choose, pays for hosting, lab environments and new content.

Contribute with PayPal Any amount · card or PayPal accountLearn more
QR code PayPal
Or scan this QR code with your phone

Thank you, every contribution counts!