DevOps Peach

Security (DevSecOps) · Software supply chain security · Lesson 2 of 4

Base image hygiene

The base image drives most of your CVE count and attack surface; choose small official images, pin by digest, use multi-stage builds, run as a non-root user and rebuild regularly.

Practice interactively →

Choosing a base image

Kind Example When
Full distro ubuntu:24.04, debian:12 Many system tools needed
Slim python:3.12-slim, node:22-slim A good default for most apps
Alpine alpine:3.20 Very small, uses musl (may behave differently from glibc)
Distroless gcr.io/distroless/static No shell, no package manager
Scratch scratch Static binaries such as Go

Use images only from trusted sources: Docker Official Images, Verified Publishers or a vetted internal registry. A stranger's image on Docker Hub may hide a miner or a backdoor.

Pinning for reproducible builds

Tags such as node:22 or latest can point at different content every week. A digest is a content hash and never changes:

FROM node:22-slim@sha256:EXAMPLE   # the tag for humans, the digest for machines

Combine this with Renovate or Dependabot so digests are updated through checked pull requests instead of drifting silently.

Multi-stage builds

FROM golang:1.23 AS build
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /app ./cmd/server

FROM gcr.io/distroless/static:nonroot
COPY --from=build /app /app
USER nonroot
ENTRYPOINT ["/app"]

The build stage has the compiler and tools; the final stage has only the binary. The image is smaller with fewer CVEs, and an attacker who gets into the container finds no shell or curl to use.

flowchart LR
  A["golang:1.23<br/>~800 MB, many packages"] -- "COPY --from=build" --> B["distroless/static<br/>a few MB, binary only"]

Other habits

Traps: using latest in production; never rebuilding after the first build; copying the whole root folder, secrets included, into the image; assuming Alpine always has fewer issues without scanning.

Practice interactively →