SBOM
Docker SBOM: how to generate one, and what it misses
You can get an SBOM for a Docker image in one command, at build time with BuildKit or afterwards with Docker Scout or Syft. Each one reads the package metadata it finds in the image. That is the right record of what the image contains, as far as the metadata goes. It is not a record of how the image was built.
Updated
How to generate an SBOM for a Docker image
There are three common ways. BuildKit can write the SBOM while it builds the image and attach it to the image as an attestation. Docker Scout and Syft generate one from an image that already exists, locally or in a registry.
At build time, with BuildKit
Add --sbom=true, or its long form --attest type=sbom, to docker buildx build.
BuildKit runs its Syft scanner plugin on the result and attaches the SBOM to the image as an SPDX JSON document.
Build with the local exporter to look at the SBOM before you push anything:
# Build the image, attach an SBOM attestation and push it
docker buildx build --sbom=true -t registry.example.com/app:1.0 --push .
# The long form of the same option
docker buildx build --attest type=sbom -t registry.example.com/app:1.0 --push .
# Check the SBOM before pushing: write the build result to ./out
docker buildx build --sbom=true --output type=local,dest=out .
# out/sbom.spdx.json is the SBOM Read the SBOM attached to an image
docker buildx imagetools inspect reads the attestation back from the registry. Everything SBOM-related
is under .SBOM in its Go template:
# Print the SPDX document attached to the image
docker buildx imagetools inspect registry.example.com/app:1.0 \
--format "{{ json .SBOM.SPDX }}"
# List each package and its version
docker buildx imagetools inspect registry.example.com/app:1.0 \
--format "{{ range .SBOM.SPDX.packages }}{{ .name }}@{{ .versionInfo }}{{ println }}{{ end }}" From an existing image, with Docker Scout
docker scout sbom uses the image's SBOM attestation when there is one. When there is none, it indexes
the image contents itself. It prints JSON by default and writes to a file with --output:
# List the packages in an image
docker scout sbom --format list registry.example.com/app:1.0
# Write an SPDX or CycloneDX SBOM to a file
docker scout sbom --format spdx --output app.spdx.json registry.example.com/app:1.0
docker scout sbom --format cyclonedx --output app.cdx.json registry.example.com/app:1.0 From an existing image, with Syft
Syft is the open source scanner that BuildKit's plugin is built on. Point it at an image and choose the format with
-o. Add =file to write each format to its own file:
# CycloneDX JSON to the terminal
syft registry.example.com/app:1.0 -o cyclonedx-json
# Both formats, each to its own file
syft registry.example.com/app:1.0 -o spdx-json=app.spdx.json -o cyclonedx-json=app.cdx.json SPDX or CycloneDX?
BuildKit attestations are always SPDX. Docker Scout writes SPDX or CycloneDX, and Syft writes both, among other formats. Both standards list packages, versions, licenses and package URLs, so the choice usually comes down to the tool that reads the SBOM next. See CycloneDX vs SPDX for the differences.
How an image scanner builds an SBOM
All of these tools work the same way. They unpack the image layers and read the package metadata they find: the operating system's package database and the metadata that language package managers leave behind. Every package they can identify becomes an SBOM entry, including the packages that came from the base image.
Include build stages and the build context
By default, BuildKit scans only the final stage. In a multi-stage build, the compilers, build tools and packages
used in earlier stages are not in the final image, so they are not in its SBOM either. Docker's
SBOM attestation docs
describe two build arguments that widen the scan. BUILDKIT_SBOM_SCAN_STAGE adds a stage, and
BUILDKIT_SBOM_SCAN_CONTEXT adds the build context:
# syntax=docker/dockerfile:1
# Scan the build context as well as the image
ARG BUILDKIT_SBOM_SCAN_CONTEXT=true
FROM golang:1.25 AS build
# Include this build stage in the SBOM
ARG BUILDKIT_SBOM_SCAN_STAGE=true
WORKDIR /src
COPY . .
RUN go build -o /out/app .
FROM scratch
COPY --from=build /out/app /app
ENTRYPOINT ["/app"]
Both must be declared with ARG in the Dockerfile. Setting them only with --build-arg has
no effect, though --build-arg can override a value the Dockerfile declares. With the local
exporter, each scanned stage gets its own file, such as sbom-build.spdx.json next to
sbom.spdx.json.
What a scan still misses
Stage scanning closes one gap. The scanner still reads files, though, and some of what a build uses leaves no file behind, or none it can identify:
- Files without package metadata. A binary downloaded with
curl, a tarball unpacked in aRUNstep, or vendored code copied in. Some binaries carry build information a scanner can read, such as Go binaries; many do not. - What the build downloaded and deleted. A package or installer a
RUNstep fetches, uses and removes in the same layer leaves nothing on the filesystem for any scanner to read, final stage or not. - How things got in. An SBOM from a scan lists what is there. It does not say which registry or host a package came from, or which step fetched it.
- Stages you did not mark. Stage scanning is opt in, per stage or globally. A stage without the argument is still left out.
- Scan settings. What appears depends on how the scanner is run. On one project, a single Syft flag decides whether npm packages appear in the result at all.
Why it matters
A compromised build tool or base image affects what you ship even if it never appears in the final image. And a binary without metadata can carry a vulnerability that no SBOM entry points to. When an advisory lands, you need to know what went into building the image as well as what is in it.
Record the build as well
The image SBOM answers what is inside the image. A record of the build answers what the build pulled in: the base images it pulled and the packages each step fetched, including the hidden dependencies that no lockfile lists. CRACI records that while your GitHub Actions job runs, through a package-aware proxy on the runner, and the API returns SBOMs, network traces and provenance for the job. Use the image SBOM for what you ship and the build record for how it was made.
See SBOM tools compared, build-time SBOMs and how to generate an SBOM in GitHub Actions.
Docker SBOMs: frequently asked questions
How do I generate an SBOM for a Docker image?
Three common ways. Add --sbom=true to docker buildx build and BuildKit attaches an SPDX SBOM to the image. Run docker scout sbom on an existing image. Or run Syft, for example syft IMAGE -o cyclonedx-json. All three read the packages they find in the image.
What is an SBOM scanner?
A tool that reads files, such as a source directory, a lockfile or a container image, and writes an SBOM of the packages it can identify. Syft, Trivy, Docker Scout and BuildKit's Syft plugin are SBOM scanners. They describe the files in front of them, not the build that produced them.
Which SBOM format does Docker produce?
BuildKit attestations are SPDX JSON documents attached to the image. Docker Scout can print a package list or write SPDX or CycloneDX. Syft writes both, and more. Pick the format the tool that consumes the SBOM expects.
How does a scanner build an SBOM from a Docker image?
It unpacks the image layers and reads the package metadata it finds: the operating system's package database, and language package files such as installed npm or Python package metadata. Each package it can identify becomes an entry in the SBOM.
Why would an image SBOM miss a component?
Because it can only list what it can identify in the files it reads. A binary downloaded with curl carries no package metadata, a package a build step downloaded and deleted leaves nothing to read, and earlier build stages are left out unless you turn on stage scanning.
Is an image SBOM still useful?
Yes. It is the right record of what is inside the image you ship, including packages from the base image. It just does not describe how the image was built, so pair it with a record of the build when that matters.
See what your image build pulled in
Run one image build on CRACI and set its record of fetched base images and packages next to your scanner's image SBOM.
Book a demo