Docker

Docker - Advanced

Page 1 of 2

Minimizing Production Image Size, For Real

Multi-stage builds (hard tier) get you most of the way to a small image, but "small" matters for more than just disk space and download time. A smaller image has a smaller attack surface - fewer installed packages means fewer potential vulnerabilities to scan, patch, and worry about - and a smaller image also means faster cold starts, which matters a lot in any environment that scales containers up and down dynamically (a slow-to-pull image directly costs you response time during a scale-up event).

The production-grade version of a multi-stage build takes the runtime stage further than just "smaller base image" - it removes anything the running app doesn't strictly need, including package managers, shells, and compilers that have no job to do once the app is actually running.


Distroless and Scratch Images

Distroless images (Google's gcr.io/distroless/* family is the best-known) contain your application and its runtime dependencies - and essentially nothing else. No shell, no package manager, no text editor, none of the normal Linux userland tools you'd expect to find.

FROM node:20 AS build
WORKDIR /app
COPY . .
RUN npm ci && npm run build

FROM gcr.io/distroless/nodejs20-debian12
COPY --from=build /app/dist /app
CMD ["/app/server.js"]

The trade-off is real and worth stating plainly: no shell means docker exec -it ... /bin/sh simply doesn't work anymore - there's nothing there to open. That's inconvenient for debugging, but it's also exactly the point from a security standpoint: an attacker who compromises the running app also finds no shell to pivot into, no package manager to pull in additional tools, nothing to work with beyond the application itself.

scratch goes further still - a genuinely empty base image, useful for statically-linked binaries (a common target for Go builds) that need absolutely nothing else from the OS to run at all.

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

FROM scratch
COPY --from=build /app/server /server
ENTRYPOINT ["/server"]

Supply Chain: Scanning and SBOMs

A production image is built from layers of other people's work - a base image, packages installed on top of it, dependencies your own code pulls in. Any one of those can carry a known vulnerability, and you generally have no way to know just by looking.

Image scanning tools (Trivy, Grype, and the scanning built into most registries) check every package in an image against databases of known vulnerabilities (CVEs) and tell you what's actually present, not just what you deliberately installed.

trivy image my-app:1.0

An SBOM (Software Bill of Materials) is a complete, machine-readable inventory of exactly what's inside an image - every package, every version. Increasingly, this isn't optional for anyone working with regulated industries or larger customers - it's a real, contractual requirement, and it's also just genuinely useful during an incident: "is the vulnerability that was just announced actually present in anything we're running" is a question an SBOM answers in seconds instead of hours of manual auditing.

docker sbom my-app:1.0

Image Signing and Provenance

Scanning tells you what's in an image. Signing tells you the image actually came from where you think it did, and hasn't been tampered with since. Without it, nothing stops an attacker who gains write access to a registry from silently replacing a trusted image with a malicious one carrying the exact same tag.

Cosign (part of the Sigstore project) is the tool most of the industry has converged on for this:

cosign sign my-registry.example.com/my-app:1.0
cosign verify my-registry.example.com/my-app:1.0

A deployment pipeline that verifies signatures before deploying anything closes off an entire class of supply-chain attack - a compromised registry, or a compromised CI credential, can't silently substitute a different image without the signature check failing.


Rootless Docker

Running your container's process as a non-root user (hard tier) is one layer of defense. A deeper one: running the Docker daemon itself without root privileges at all, so that even a complete container escape doesn't hand an attacker root on the host, because the daemon managing everything never had root to begin with.

dockerd-rootless-setuptool.sh install

Rootless mode has real limitations (some networking configurations and low-numbered ports need workarounds), which is why it isn't the universal default - but for security-sensitive environments, it closes off a meaningful escalation path that non-root-inside-the-container alone doesn't.


    Welcome to OpsQuiz!

    Real scenario-based DevOps questions, hands-on practice, and clear explanations for every answer.