Container vulnerability scanning finds known CVEs in the OS packages and language dependencies baked into an image, before that image ever runs in production. The tooling is mature and mostly free — Trivy and Grype are both open source and good enough for the majority of teams — so the real work isn't picking a scanner, it's deciding where in the pipeline to run it and what to actually do when it finds something.
What a scanner actually checks
A scanner unpacks image layers, identifies installed OS packages (via the distro's package database — dpkg, apk, rpm) and language-level dependencies (from lockfiles like package-lock.json, requirements.txt, go.sum that got copied into the image), and cross-references each package-and-version pair against vulnerability databases — the NVD, distro-specific advisories (Debian Security Tracker, Alpine's secdb), and language ecosystem advisories (GitHub Advisory Database, OSV). A hit means a known CVE affects that exact version; it does not automatically mean your application is exploitable, since many CVEs require a specific usage pattern that your code may never trigger.
trivy image \
--severity CRITICAL,HIGH \
--exit-code 1 \
--ignore-unfixed \
registry.example.com/app:${GIT_SHA}
Fail the build selectively, not on everything
--exit-code 1 makes the CI step fail when matching vulnerabilities are found. Doing this unconditionally for every severity is how teams end up disabling the scanner entirely six weeks in — a base Debian image routinely carries dozens of low and medium findings with no available fix. The two flags that make this workable in practice: filter to critical/high severity, and use --ignore-unfixed so you're not blocked on a CVE the upstream maintainer hasn't patched yet. That still leaves you accountable for the CVEs you actually can fix by updating a package.
Scanners only catch known, cataloged vulnerabilities in identifiable packages. They don't catch a hardcoded secret baked into a layer, an application-level injection flaw, or a zero-day. Vulnerability scanning is one control among several, not a substitute for secure coding practices or secrets scanning.
Scan early, and scan again at push
Scanning in CI before push catches problems while they're cheap to fix — usually a base image bump or a dependency update, resolved in the same PR. Scanning again at the registry (most managed registries support this natively) catches drift: a CVE disclosed *after* an image was pushed and is now sitting in a registry, waiting to be deployed. Both matter; CI scanning alone misses vulnerabilities disclosed after the fact, and registry-only scanning means you find out too late in the cycle.
The fix is usually a base image bump
Most findings resolve with a rebuild against an updated base image rather than a code change — distro maintainers backport security patches into the same major version, so bumping from an old `node:20-slim` pull to a fresh one often clears most of the list without touching application code. This is why pinning a base image by digest and never rebuilding is worse for security than it sounds: an unpinned tag that gets rebuilt regularly actually picks up patches; a frozen digest doesn't, until someone deliberately updates it.
Set a weekly CI job that rebuilds your production images from the current Dockerfile with no code changes, purely to pick up base-image security patches, and runs the scanner against the result. This catches the case where your code hasn't changed in months but the base image underneath it has accumulated CVEs.
Handling the findings you can't fix
Some findings genuinely have no fix available yet, or the fix would require a major version bump you're not ready to take. Trivy and Grype both support an ignore-list (`.trivyignore`, or an ignore policy file) to suppress specific CVE IDs with a reason attached — use it, but treat every entry as a tracked exception with an owner and a revisit date, not a permanent silence.
| Where scanned | Catches | Timing |
|---|---|---|
| CI, pre-merge | Vulnerable deps introduced by this change | Cheapest to fix — same PR |
| Registry, on push | Same as CI, plus a second independent check | Still pre-deploy |
| Registry, scheduled rescan | CVEs disclosed after the image was pushed | Post-deploy — needs a redeploy to fix |
| Runtime/admission control | Blocks known-vulnerable images from deploying at all | Last line of defence |
Wrapping up
Vulnerability scanning earns its keep when it's tuned to fail on what's actually fixable and actionable, not on every low-severity finding in a base OS layer nobody's going to patch this sprint. Start with Trivy in CI filtered to critical/high with unfixed CVEs ignored, add a scheduled rebuild-and-rescan for drift, and treat every suppressed finding as a tracked exception rather than a silence.
Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.