Cloud and Containers

How to Scan Docker Images for Vulnerabilities in CI

22 September 2026 8 min read

Adding a container scan to a CI pipeline is usually a task that lands on one engineer’s plate after an audit finding, a customer security questionnaire, or an incident traced back to an unpatched base image. The theory is well known: pull an image, scan it, fail the build if something bad shows up. What actually takes time is everything around that one command: where the step sits, what threshold decides pass or fail, what to do with the hundreds of findings that show up on day one, and where the results go once the job finishes. This walks through a working setup rather than the general concept.

Where in the pipeline the scan belongs: build, push, or pre-deploy

There are three points where an image scan can run, and each answers a different question. A post-build scan runs immediately after docker build, before the image is pushed to any registry: nothing has left the runner yet, no registry storage is wasted on an image you are about to delete, and the feedback lands in the same pull request that introduced the change. A registry-side scan runs against images already pushed, either on a schedule or triggered by the registry itself (Amazon ECR, Google Artifact Registry, and Harbor all support this natively). This second layer matters because vulnerability databases change daily: an image that passed its build-time scan in January can have a CRITICAL CVE disclosed against one of its packages in March, and nothing re-checks it unless something scans the registry independently. A pre-deploy or admission-time check is the third layer, enforced by something like Kyverno or OPA Gatekeeper in the cluster, blocking a pod from scheduling unless its image carries a passing scan attestation.

For a first implementation, build the post-build gate only. It is the one developers see directly, it teaches the team what a normal finding count looks like, and it needs no cluster-level policy work you are not ready to maintain yet. Add the registry-side re-scan once the build-time gate is stable, since that is what protects against newly disclosed CVEs in images nobody is rebuilding this week. Treat admission control as a later hardening step, not a prerequisite.

A working example: scan step, severity threshold, and a readable report

Trivy, from Aqua Security, is a reasonable default for a first scanner: no server component, reads a local image directly off the Docker daemon or an OCI tarball, and covers OS packages plus language dependencies (npm, pip, Maven, Go modules) in one pass. Grype, from Anchore, covers similar ground and pairs with Syft for SBOM generation. Docker Scout ships inside Docker Desktop and Hub and is worth a look if that is already your registry. None of these needs to be the final answer; day one is about getting a scan running and a report you can read, not picking the perfect tool.

Tool Maintainer License Notable trait
Trivy Aqua Security Apache 2.0 Single binary, scans images, filesystems, and IaC
Grype Anchore Apache 2.0 Pairs with Syft for SBOM generation
Docker Scout Docker, Inc. Free tier + paid org policy Built into Docker Desktop and Hub
Snyk Container Snyk Commercial, free tier Remediation advice tied to Snyk’s own vulnerability DB

A minimal CI job scans the image right after the build step, before the push step: build the image and tag it with the commit SHA, run trivy image –severity HIGH,CRITICAL –exit-code 1 –format table myapp:$SHA against that tag, and only run docker push if the scan step exits zero. The –format table output is readable directly in the CI log for a first rollout: package name, installed version, fixed version if one exists, and severity, sorted with CRITICAL findings on top. Once the team is comfortable with that, switch to SARIF for the reporting step described later, and keep the table output as a secondary artifact.

Choosing a failure policy that does not block every pull request on day one

The first real scan of a production image almost always returns more findings than expected, often in the hundreds, and most sit in the base OS image rather than in code the app team wrote. If the very first version of the pipeline fails the build on any HIGH or CRITICAL finding, one of two things happens: the pipeline gets disabled within a week, or an ignore file gets populated with every CVE ID at once just to make the red X go away, which defeats the purpose. A phased gate avoids both outcomes.

Phase Duration Gate condition Build result on match
1: Baseline 2 to 4 weeks None, report only Always passes, report posted as PR comment
2: Critical with fix 4 to 8 weeks CRITICAL and a fixed version exists Fails
3: High with fix Ongoing HIGH or CRITICAL and a fixed version exists Fails
4: No fix available Case by case Any severity with no fixed version Report only, tracked separately

The condition that makes phase 2 and 3 workable is the –ignore-unfixed flag (Trivy) or the equivalent –only-fixed filter in Grype. Gating on findings with no available patch just punishes the team for something they cannot act on this sprint, and that is the fastest way to get the scan step muted. Findings with no fix yet belong in the reporting stream, tracked with an expected review date, not in the merge-blocking path.

Even with fixed-only gating, findings in phase 2 and 3 tend to repeat the same handful of base-image CVEs across every service that shares a parent image. Systems that add an AI validation and triage step, such as SITEY’s, cluster a shared libssl or zlib CVE across forty images into one decision instead of forty separate pull request comments, which matches how the finding is actually worked rather than how the scanner reported it forty separate times.

Handling findings in the base image that the app team cannot fix

A finding in python:3.11-slim or node:20-bookworm that traces to glibc, openssl, or a similar OS package is not something the application team can patch by editing its own code. The fix, if one exists, ships from the base image maintainer, and until then the finding sits in a state that is neither open and actionable nor resolved. Two changes make this manageable instead of a permanent backlog.

  • Separate ownership by layer. Trivy’s –list-all-pkgs flag and Grype’s SBOM output both show which layer introduced a package, so a base OS layer finding can be routed to whoever owns the base image, often a platform team, instead of the service team that only added application dependencies on top.
  • Reduce the base image’s package count rather than waiting for upstream patches. Moving from a general-purpose image like debian:bookworm to a slim or distroless variant, or to Alpine where the runtime supports it, cuts the number of OS packages that can carry a CVE. It is a one-time migration cost that pays off on every subsequent scan.

When the base image maintainer ships a fix, closing the finding correctly means pulling the new digest and re-running the same scanner against it, not marking a ticket done because a Dockerfile line changed. Pipelines that automate this step, such as SITEY’s automated retest and closure phase, re-run the original scanner against the new image hash before a finding is allowed to close, instead of trusting that a version bump did the job.

Caching and runtime: keeping the scan under a minute

The slowest part of most container scans is not the scan itself, it is downloading the vulnerability database. Trivy’s database runs to a couple hundred megabytes, and both Trivy and Grype update theirs multiple times a day upstream, so a cold run that fetches the full database can take longer than the rest of the pipeline combined. Three changes address most of this.

  1. Cache the database directory across runs. On GitHub Actions, cache ~/.cache/trivy (or ~/.cache/grype) keyed on the current date, so the cache is reused within a day and refreshed once it rolls over. On GitLab CI, declare the same directory under the job’s cache:paths.
  2. Scan the image the build step just produced directly from the local Docker daemon rather than pushing it and pulling it back down. Both Trivy and Grype accept a local image reference without a registry round trip.
  3. Scope the scan to the image you built, not the entire registry. A single-image scan with a warm database cache typically finishes in well under a minute for images in the few-hundred-megabyte range; scanning every tag on every commit is a different job that belongs on a schedule.

If the database cache goes stale, most of these scanners can skip the update and use whatever is cached, which keeps the pipeline from turning a scanner outage into a build outage. That is a reasonable default for the build-gate job; the registry-side background scan should still insist on a fresh database.

Exporting results so they persist beyond the CI job log

A CI job log is not a system of record. GitHub Actions and GitLab CI both purge job logs on a retention schedule that is fine for debugging a failed build last Tuesday but useless for showing an auditor, six months from now, what happened to a specific CRITICAL finding under ISO 27001’s vulnerability management clause or PCI DSS’s scanning requirements. The fix is to export the scan output in a structured format and send it somewhere with its own retention, instead of treating the console output as the deliverable.

SARIF is the practical export format if you are already on GitHub: Trivy and Grype both support a –format sarif option, and uploading that file with github/codeql-action/upload-sarif puts every finding into the repository’s Security tab with line-level context, surviving independently of the job log and showing up as a PR annotation. For teams that need the data outside GitHub’s own view, JSON export forwarded to object storage or a central system is the more durable path. A CI job that fires once and exits is a poor place to track a finding’s state over multiple weeks; that requires something that keeps a finding open, marks it triaged, and later marks it retested and closed, which is the kind of lifecycle tracking platforms such as SITEY apply to every finding, not a build log.

This also matters once more than one scanner is in play, which most teams eventually run: a container image scanner in CI, a separate SAST tool for source code, and often a host-based agent scanning the same base image from a different angle. Without deduplication, the same glibc CVE shows up three times under three different names in three dashboards. Consolidating scanner output into one place, the kind of work covered under scanner integrations when a platform ingests output from many tools at once, is what keeps a single vulnerability from generating three unrelated tickets for three different people to close independently.

About SITEY

SITEY is an autonomous vulnerability management platform. It discovers, validates, prioritizes, remediates and re-tests vulnerabilities through an eight-phase automated pipeline, unifying output from 17 integrated scanners. SITEY is self-hosted: it runs in your own infrastructure and your findings are stored there. Outbound connections are limited to licence activation and the optional services you enable, such as an AI provider, CVE enrichment and patch catalogues. Pricing is 599 USD per month or 5,999 USD for a perpetual lifetime license. See pricing or how the platform works.

SITEY closes the loop, not just the report.Discover, validate, fix and verify in your own infrastructure.

See pricing