Dockerfile best practices covered running as non-root and never baking in secrets — real, important, and still not enough, because most of a real image's actual code isn't yours at all. It's a base image and hundreds of transitive OS and language packages, any of which can carry a known vulnerability you never wrote a line of.
3 min read
A typical application image is small on top of a much larger base: the OS
packages inside node:20-slim, the Node runtime itself, and every npm
package your application depends on (plus their dependencies).
Dockerfile best practices' non-root user and secret-handling advice
protects against real risks, but it says nothing about whether any of
that inherited code has a known, publicly documented vulnerability — which
is a separate, equally real risk surface.
An image vulnerability scanner (Trivy, Grype, Docker Scout, Snyk) inspects every package inside a built image — OS packages and language-level dependencies alike — against databases of known CVEs (Common Vulnerabilities and Exposures):
trivy image my-app:latestmy-app:latest (debian 12.5)
==========================
Total: 3 (HIGH: 2, CRITICAL: 1)
┌──────────────┬────────────────┬──────────┬───────────────┐
│ Library │ Vulnerability │ Severity │ Fixed Version │
├──────────────┼────────────────┼──────────┼───────────────┤
│ openssl │ CVE-2024-XXXXX │ CRITICAL │ 3.0.13-1 │
│ curl │ CVE-2024-YYYYY │ HIGH │ 8.5.0-2 │
└──────────────┴────────────────┴──────────┴───────────────┘
A scan run once, manually, at initial development time only catches vulnerabilities known at that moment — a base image with zero known issues today can have a critical CVE disclosed against one of its packages next month, with the image itself never rebuilt or rescanned since. Running the scan automatically in CI on every build (and, ideally, on a recurring schedule against images already deployed) is what actually catches newly-disclosed vulnerabilities in dependencies nobody touched recently — a gate failing the build on new CRITICAL findings is a common, effective policy.
Dockerfile best practices already covered choosing a smaller base image
(slim, alpine, distroless) for size — the same choice directly
reduces the vulnerability surface too, since fewer installed packages
means fewer things that can carry a CVE. A distroless image with no shell,
no package manager, and nothing beyond the language runtime has
dramatically fewer packages to scan (and fewer tools an attacker could use
if they did gain code execution) than a full Debian base — the size
optimization and the security benefit are really the same underlying
mechanism, just described from two different angles.
Scanning answers "does this image contain known-vulnerable code." A separate question: "is this actually the image the build pipeline produced, unmodified, or did something alter it between build and deployment?" Image signing (via Docker Content Trust, or the newer Sigstore/cosign tooling) cryptographically signs an image at build time, letting a deployment step verify the signature before running it — rejecting an image that's been tampered with, or one from an unauthorized source entirely, even if it happens to be otherwise vulnerability-free.
cosign sign my-registry/my-app:v1.2.0
cosign verify my-registry/my-app:v1.2.0The bug-reference lesson later in this domain covers :latest tag drift
as a reproducibility problem. It's also a security problem from this
lesson's angle: an unpinned base image can silently pull in a newly
patched (or newly vulnerable) version between builds, meaning "this image
passed its security scan last week" says nothing reliable about an image
built from the same Dockerfile today. Pinning to a specific digest — not
just a version tag — is the strongest guarantee that a scan result stays
valid until the next deliberate rebuild.
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. Why isn't avoiding baked-in secrets and running as non-root enough to make a container image secure?
2. Why should vulnerability scanning run automatically in CI rather than as a one-time manual check?
3. How does choosing a smaller base image (like distroless) double as a security improvement?
4. What does image signing (like cosign) verify that vulnerability scanning does not?
Docker & Containers