Image Scanning and Vulnerabilities

Image scanning checks a container image for known vulnerable packages before you run or ship it. It matters because an image is not just your application code: it also contains an operating-system filesystem, language packages, native libraries, and metadata inherited from its base image. Scanning gives you a practical way to see what is inside an image, decide what must be fixed, and avoid distributing avoidable risk through a registry.

Overview: how image scanning works

A Docker image is a stack of read-only layers plus a manifest that tells Docker which layers belong together. Each layer may add, change, or delete files, so a scanner analyzes the final filesystem view and the image metadata to identify installed packages. For example, an Alpine-based image may contain apk packages, a Debian-based image may contain deb packages, and a Node.js app may contain npm packages from package-lock.json or node_modules.

Modern Docker workflows commonly use Docker Scout for local and registry-backed analysis. The important commands are docker scout quickview for a summary, docker scout cves for detailed vulnerability findings, and docker scout compare for seeing whether one image is better or worse than another. Other scanners such as Trivy, Grype, and registry-native scanners follow the same basic idea: build a software bill of materials, match package names and versions against vulnerability databases, then report CVEs with severity, affected versions, and fixed versions where known.

A vulnerability report is not the same thing as a proof that your container is exploitable. A CVE may affect a package that is installed but never used, or it may require a code path, configuration, architecture, or network exposure your container does not have. Still, scanning is valuable because it turns hidden dependency risk into visible work. The usual goal is to remove unnecessary packages, update affected packages, move to a patched base image, and document the rare finding that is truly not applicable.

Scanning also connects directly to registries. When you push an image, the registry stores a manifest, config object, and compressed layer blobs. Registry-side scanners can analyze that pushed artifact and keep reporting newly disclosed CVEs later, even when the image digest has not changed. That is why production should pin and deploy immutable digests where possible: the exact bytes stay the same, but the security knowledge about those bytes changes over time.

Syntax

docker scout quickview [IMAGE]
docker scout cves [OPTIONS] IMAGE
docker scout compare IMAGE --to IMAGE
docker buildx build --sbom=true --provenance=true -t NAME:TAG .
Command or option Meaning
docker scout quickview IMAGE Shows a compact vulnerability and policy summary for an image.
docker scout cves IMAGE Lists CVEs found in packages detected inside the image.
--only-severity critical,high Filters the CVE list to selected severities.
--only-fixed Shows only vulnerabilities that have a known fixed package version.
--format sarif Writes results in SARIF JSON, commonly consumed by code scanning tools.
--output FILE Saves the report to a file instead of only printing it.
docker scout compare A --to B Compares two images so you can see whether an update improves the result.
--sbom=true Asks BuildKit/buildx to attach SBOM information when building, improving downstream inspection.
--provenance=true Attaches build provenance metadata so consumers can trace how the image was built.

Examples

Scan a public image summary

docker pull nginx:1.27.3-alpine
docker scout quickview nginx:1.27.3-alpine

Output:

1.27.3-alpine: Pulling from library/nginx
Status: Downloaded newer image for nginx:1.27.3-alpine
    i New version 1.27.4-alpine available
  Image stored for indexing
  Policy status  PASSED
  Vulnerabilities  0C  0H  2M  1L

This pulls a specific tag and asks Scout for a quick summary. The exact counts will change as vulnerability databases are updated, but the shape of the result is stable: you get a short view of whether the image has critical, high, medium, or low findings. Notice that the tag is specific rather than latest. A moving tag makes reports hard to reproduce because the same command can refer to different image bytes later.

List fixable high-impact CVEs

docker scout cves --only-severity critical,high --only-fixed nginx:1.27.3-alpine

Output:

Analyzing image nginx:1.27.3-alpine
Detected 1 vulnerable package with 1 vulnerability

Package  Version   Fixed version  Severity  Vulnerability
openssl  3.3.2-r0  3.3.2-r1       HIGH      CVE-2024-example

Filtering is useful in CI because not every finding needs the same response. --only-severity critical,high narrows the report to the issues most likely to block a release, and --only-fixed focuses on findings you can usually remediate by updating a package or base image. In real output, use the listed fixed version and package location to decide whether the fix belongs in your Dockerfile, your language lockfile, or the base image tag.

Build an image with better metadata

FROM node:20.18.1-alpine3.20
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
USER node
EXPOSE 3000
CMD ["node", "server.js"]

Output:

Dockerfile saved. The image will run as the non-root node user and documents port 3000. EXPOSE is metadata only; it does not publish the port to the host.

This Dockerfile starts from a pinned Node.js Alpine tag, installs production dependencies before copying the rest of the source, and switches away from root. The EXPOSE instruction is only documentation and image metadata; publishing still requires docker run -p or Compose ports:. The dependency-copy order also helps build caching: changing application source does not automatically invalidate the npm ci layer unless the package files changed.

docker buildx build --sbom=true --provenance=true -t registry.example.com/team/web:1.4.0 .
docker scout cves --format sarif --output web-1.4.0.sarif.json registry.example.com/team/web:1.4.0

Output:

[+] Building 18.4s (10/10) FINISHED
=> naming to registry.example.com/team/web:1.4.0
Analyzing image registry.example.com/team/web:1.4.0
Report written to web-1.4.0.sarif.json

The build command asks BuildKit to include SBOM and provenance attestations. The scan command then exports a SARIF report, which many CI systems can archive or display. If you push this image, scan the same pushed reference or digest that you deploy so the report matches the artifact in the registry.

Compare before and after an update

docker scout compare registry.example.com/team/web:1.3.0 --to registry.example.com/team/web:1.4.0

Output:

Comparing registry.example.com/team/web:1.3.0 to registry.example.com/team/web:1.4.0
Vulnerabilities: 2C 7H 18M 5L -> 0C 1H 12M 4L
Base image: node:20.17.0-alpine3.19 -> node:20.18.1-alpine3.20
Packages: 14 removed, 22 updated, 3 added

Comparison is often more useful than a raw scan. It answers the release question: did this change reduce risk, increase risk, or simply move findings around? Use it after base image updates, dependency upgrades, or Dockerfile simplification.

How it works step by step

  1. Docker resolves the image reference. The scanner needs exact image bytes. For a local image it can read the local image store; for a registry image it resolves the tag to a manifest and layer digests.
  2. The image filesystem is reconstructed. Docker images are layered. The scanner analyzes the effective filesystem after applying the read-only layers, not a running container writable layer.
  3. Packages are detected. The scanner looks for operating-system package databases, language lockfiles, installed modules, binaries, and image metadata. This becomes an SBOM-like inventory.
  4. Packages are matched to vulnerability data. Package names, ecosystems, versions, and sometimes distro versions are compared with CVE databases and vendor advisories.
  5. Findings are prioritized. Severity, fixed-version availability, exploitability signals, and policy rules help decide what blocks a release.
  6. You remediate and rebuild. Updating the base image, rebuilding with newer dependencies, removing unused packages, and running as a non-root user reduce practical risk. The rebuilt image receives a new digest and must be scanned again.

Common Mistakes

Scanning only once

docker scout cves registry.example.com/team/web:1.4.0
# Then never scanning this deployed digest again

A clean report today can become noisy tomorrow because new CVEs are disclosed for old packages. Fix this by scanning in CI before release and also scanning pushed images or deployed digests on a schedule.

Using latest in production scans

docker scout cves mycompany/web:latest

This is ambiguous because latest is just a tag, not a guarantee of freshness or immutability. A better release process scans mycompany/web:1.4.0 and records the digest that was deployed. Tags are useful labels; digests identify exact content.

Trying to delete secrets after copying them

FROM alpine:3.20
COPY production.env /tmp/production.env
RUN rm /tmp/production.env

This is unsafe because Docker layers are read-only history. Removing the file in a later layer does not erase it from the earlier layer. Secrets should be provided at runtime through your orchestrator, Docker secrets, mounted files, or environment injection outside the image build.

Ignoring the base image

Many findings come from the base image, not your application code. If the report shows a vulnerable OS package, changing JavaScript or Python dependencies will not fix it. Update the base image tag, choose a slimmer base, or rebuild after the base image publisher releases patched layers.

Best Practices

  • Scan the exact image reference or digest you plan to push or deploy.
  • Use pinned image tags in Dockerfiles and record deployed digests for auditability.
  • Prefer small production images that omit compilers, package managers, test tools, and build caches when they are not needed at runtime.
  • Use multi-stage builds so build tooling stays in the builder stage and only runtime artifacts reach the final image.
  • Run containers as a non-root user where the application supports it.
  • Keep .dockerignore tight so secrets, local caches, and unnecessary files do not enter the build context.
  • Export machine-readable reports such as SARIF in CI, but keep a human-readable summary for release decisions.
  • Treat severity as a starting point, then consider fixed versions, package reachability, internet exposure, and business impact.
  • Rebuild and rescan regularly, even when your application source has not changed, because base images and vulnerability data move.

Practice Exercises

  1. You maintain registry.example.com/team/api:2.2.0. Run a scan that shows only critical and high vulnerabilities with known fixes. Hint: combine severity and fixed-version filters.
  2. A teammate updated a Dockerfile from node:20.17.0-alpine3.19 to node:20.18.1-alpine3.20. Compare the old and new image tags and decide whether the update reduced vulnerability counts.
  3. Review a Dockerfile that copies .env during build and removes it later. Explain why the secret can still exist in image history and choose a runtime-only alternative.

Summary

  • Image scanning inventories packages inside image layers and matches them to known vulnerability data.
  • Docker Scout commands such as quickview, cves, and compare support local, CI, and registry workflows.
  • A CVE report is a decision tool, not automatic proof of exploitability, but high and fixable findings deserve fast attention.
  • Base image updates, dependency upgrades, smaller final images, non-root users, and clean build contexts all reduce risk.
  • Scan exact tags or digests repeatedly because vulnerability knowledge changes after an image is built.