Dash0 Raises $110M Series B at $1B Valuation

  • 13 min read

How to Scan Docker Images for Vulnerabilities

Every Docker image you build stacks an OS base layer, system libraries, a language runtime, and your application dependencies — and any of them can carry known CVEs (Common Vulnerabilities and Exposures). A scanner works by extracting a software inventory from every image layer, then cross-referencing each package against vulnerability databases like NVD, OSV, and distro-specific advisories. This article walks through the three tools that cover most teams (Trivy, Grype, and Docker Scout) and shows how to wire them into a CI pipeline so vulnerable images never reach production.

Prerequisites

Before following along, you'll need:

  • Docker Engine 24 or later (Docker Scout is built into the CLI from v24 onward)
  • A Docker image to scan (the examples use myapp:latest)
  • macOS or a Debian/Ubuntu Linux host for the install commands shown

Trivy and Grype install steps are covered inline below. No prior scanner experience is assumed.

What a scanner actually checks

Before picking a tool, it helps to know what they look at. Most scanners inspect two categories of packages.

OS packages are the apt/dpkg packages on Debian/Ubuntu images, apk packages on Alpine, or rpm packages on RHEL/Amazon Linux. These live in the base layer and are a common source of stale CVEs.

Application dependencies are npm packages, PyPI packages, Gems, Maven artifacts, Go modules. Scanners detect these from lockfiles, node_modules/, or site-packages/ directories baked into the image.

Neither category alone is sufficient. An image with a pristine Alpine base can still carry a critical CVE in a transitive npm dependency, and vice versa.

Scanning with Trivy

Trivy (by Aqua Security) is one of the most widely adopted open-source container scanners, with over 24,000 GitHub stars and broad adoption in the CNCF ecosystem. It runs as a single binary with no daemon required, and covers OS packages, language dependencies, misconfigurations, and secrets in one tool.

Install it on macOS or Linux:

bash
123456789101112
# macOS
brew install trivy
# Linux (Debian/Ubuntu)
sudo apt-get install -y wget apt-transport-https gnupg lsb-release
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key \
| gpg --dearmor \
| sudo tee /usr/share/keyrings/trivy.gpg > /dev/null
echo "deb [signed-by=/usr/share/keyrings/trivy.gpg] https://aquasecurity.github.io/trivy-repo/deb \
$(lsb_release -sc) main" \
| sudo tee /etc/apt/sources.list.d/trivy.list
sudo apt-get update && sudo apt-get install -y trivy

Run your first scan against a local image:

bash
1
trivy image myapp:latest

The output lists every detected package alongside its CVE ID, severity, installed version, and fixed version:

12345678910
myapp:latest (debian 12.5)
Total: 12 (UNKNOWN: 0, LOW: 6, MEDIUM: 3, HIGH: 2, CRITICAL: 1)
┌───────────────┬────────────────┬──────────┬───────────────────┬───────────────┬──────────────────────────────────────┐
│ Library │ Vulnerability │ Severity │ Installed Version │ Fixed Version │ Title │
├───────────────┼────────────────┼──────────┼───────────────────┼───────────────┼──────────────────────────────────────┤
│ libssl3 │ CVE-2024-5535 │ CRITICAL │ 3.0.11-1~deb12u2 │ 3.0.13-1 │ OpenSSL: SSL_select_next_proto buf...│
│ ... │ ... │ ... │ ... │ ... │ ... │
└───────────────┴────────────────┴──────────┴───────────────────┴───────────────┴──────────────────────────────────────┘

For CI usage, filter to what actually matters and fail the build on critical findings. The first command exits with a non-zero code if any CRITICAL CVE is found; the second outputs JSON for downstream processing:

bash
12345
# Fail if any CRITICAL vulnerability is found
trivy image --exit-code 1 --severity CRITICAL myapp:latest
# Filter to HIGH and CRITICAL, output JSON for downstream processing
trivy image --severity HIGH,CRITICAL --format json --output results.json myapp:latest

Trivy can also catch problems before you build at all. This command checks your Dockerfile for misconfigurations like running as root or missing COPY --chown:

bash
1
trivy config ./Dockerfile

Scanning with Grype

Grype (by Anchore) is another strong open-source option, particularly popular in teams that also generate SBOMs (Software Bill of Materials). The two tools use different vulnerability databases and detection heuristics, so running both on critical images catches more than either would alone. If you're only going to run one scanner, Trivy's broader misconfiguration coverage gives it an edge for most teams. If compliance or SBOM generation matters, Grype is worth adding.

Install Grype:

bash
12345
# macOS
brew install grype
# Linux
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin

Run a basic scan:

bash
1
grype myapp:latest

For CI pipelines, the --fail-on flag is useful. The command below fails the build only when there are vulnerabilities with available fixes, which avoids blocking on CVEs your base image vendor has already assessed and won't patch:

bash
1
grype myapp:latest --only-fixed --fail-on high

Grype pairs well with Syft, Anchore's SBOM generator. The two-step approach below lets you store the SBOM as a build artifact and re-scan it later against updated databases without rebuilding the image. This is worth doing for any compliance workflow where you need an auditable record of exactly what shipped, because "we scanned it when we built it" isn't enough when new CVEs are disclosed weeks later.

bash
123
# Generate an SBOM in CycloneDX format, then scan it
syft myapp:latest -o cyclonedx-json > sbom.cdx.json
grype sbom:./sbom.cdx.json --fail-on high

The CycloneDX format produced here is an open standard maintained by OWASP and is widely supported by compliance tooling.

Scanning with Docker Scout

Docker Scout is Docker's native scanner, integrated into Docker Desktop and the Docker CLI since Docker 24. If your team is already on Docker Desktop, it requires no additional installation and zero configuration.

bash
1234567891011
# Quick vulnerability overview
docker scout quickview myapp:latest
# Full CVE list
docker scout cves myapp:latest
# Filter to fixable critical/high CVEs only
docker scout cves --only-fixed --only-severity critical,high myapp:latest
# Compare two versions to see what changed
docker scout compare myapp:v2 --to myapp:v1

Docker Scout's strength is the developer experience. It surfaces fix recommendations alongside CVE data and integrates directly into Docker Hub. The free tier covers unlimited public images and a limited number of private repositories, which is enough for individual developers or small teams. For broader private repo coverage, it becomes a paid product.

CI pipeline integration

Automating scans in CI is where the real value is. Running a scan locally before you push is useful, but it's easy to skip. A CI gate is not.

GitHub Actions with Trivy

The workflow below builds your image, scans it with Trivy, and uploads results to GitHub's Security tab in SARIF format. The if: always() on the upload step is important: it ensures scan results appear even when the build fails, so you can see what was found rather than just that something was found.

12345678910111213141516171819202122232425262728293031323334
name: Container scan
on:
push:
branches: [main]
pull_request:
jobs:
scan:
runs-on: ubuntu-latest
permissions:
security-events: write
contents: read
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build image
run: docker build -t myapp:${{ github.sha }} .
- name: Scan with Trivy
uses: aquasecurity/trivy-action@0.36.0
with:
image-ref: myapp:${{ github.sha }}
exit-code: "1"
severity: "CRITICAL,HIGH"
format: "sarif"
output: "trivy-results.sarif"
- name: Upload results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: trivy-results.sarif

GitHub Actions with Grype

For teams using Grype, the anchore/scan-action step below produces a SARIF report and fails the build at the severity threshold you choose:

12345678910111213
- name: Scan image with Grype
uses: anchore/scan-action@v7
id: scan
with:
image: myapp:${{ github.sha }}
fail-build: true
severity-cutoff: high
- name: Upload SARIF report
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: ${{ steps.scan.outputs.sarif }}

Multi-stage build tip

If you're using multi-stage builds (and you should be), scan only the final stage that actually ships. Build stages often contain compilers, test frameworks, and build tools that carry their own CVEs but never reach production. Target the production stage explicitly before scanning:

bash
12
docker build --target production -t myapp:prod .
trivy image myapp:prod

Common pitfalls

"Won't fix" entries. Many OS vendors triage CVEs and mark some as will not fix because the vulnerability isn't exploitable in their configuration, or because a backport isn't feasible. If you don't filter these out, your CI reports fill with noise and engineers start ignoring everything, including the CVEs that matter. Use --ignore-unfixed in Trivy or --only-fixed in Grype when running CI gates.

Scanning :latest in CI. The same :latest tag can pull a different image digest on Tuesday than it did on Monday, which means your scan results don't correspond to a specific build. Pin your base images to specific digests in your Dockerfile (FROM node:20-alpine@sha256:...) so the image you scan is the image you ship.

Assuming a clean scan stays clean. Vulnerability databases update constantly. An image you scanned two weeks ago may now have critical CVEs from newly disclosed vulnerabilities. Set up scheduled re-scans against your registry (Trivy supports scanning images directly from registries) to catch new disclosures between builds. The worst place to learn about a critical CVE is from someone else's security report.

False positives vary by scanner. Trivy and Grype use different vulnerability databases and match packages differently. On some images, one will report a vulnerability the other doesn't. For high-stakes images, run both and reconcile the differences rather than picking one and assuming it catches everything.

Final thoughts

You now have three scanners to choose from, a CI workflow for each, and the context to avoid the most common mistakes. The practical starting point for most teams is Trivy in CI with --ignore-unfixed and a CRITICAL/HIGH threshold, then add Grype if you need SBOM generation or compliance coverage.

The next gap to close is connecting scan results to what's actually running. A critical CVE in an image deployed across a hundred pods is a different problem than the same CVE in an internal tool that gets one request per day. Dash0's Kubernetes monitoring gives you a live view of which containers are running which image versions. You can split by container.image.tags to see exactly which image versions are active across your workloads, alongside resource metrics, logs, and distributed traces. Cross-referencing that with your scanner output tells you where a newly disclosed CVE is actually deployed, rather than reasoning from a static image inventory. More detail on what Dash0 collects from Kubernetes workloads is in the integration overview.

Start a free trial to see your container fleet in one view. No credit card required.