How Do I Generate an SBOM During a Build?
Set sbom true on the build step so BuildKit attaches an SPDX document to the image it just produced, then read it back by digest or upload it as an artifact.
Generate the SBOM from the image build itself: set sbom: true on the build step so BuildKit scans the build result and attaches an SPDX document to the image as an attestation (Docker SBOM attestations). The inventory then describes the exact digest you pushed, and a later step can either read it back out of the registry or upload it as a workflow artifact for the release job that needs it.
Answer
A GitHub Actions workflow can produce an SBOM at two moments, and the moment decides how much the document is worth.
The first moment is inside the build. BuildKit 0.11 and later accept an --attest type=sbom request, which runs a scanner over the filesystem the build just produced and writes the result as an attestation manifest referenced from the image index (Docker build attestations). The build action exposes this as a boolean input, so the change to an existing workflow is one line.
The second moment is after the push, when a separate job pulls the image and scans it. That produces a loose file describing a tag, and tags move.
WarpBuild's Warpbuilds/build-push-action@v6 is a drop-in replacement for docker/build-push-action@v6 that points the build at a remote builder profile, so the attestation inputs pass through unchanged (Docker builders documentation).
name: image
on:
push:
branches: [main]
permissions:
contents: read
packages: write
jobs:
build:
runs-on: warp-ubuntu-latest-x64-4x
steps:
- uses: actions/checkout@v5
- name: Log in to the registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build, attest, and push
id: build
uses: Warpbuilds/build-push-action@v6
with:
context: .
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
sbom: true
provenance: mode=max
profile-name: "app-builder"
- name: Read the SBOM out of the pushed image
run: |
docker buildx imagetools inspect \
ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }} \
--format '{{ json .SBOM.SPDX }}' > sbom.spdx.json
- name: Upload the SBOM
uses: actions/upload-artifact@v4
with:
name: sbom-${{ github.sha }}
path: sbom.spdx.json
retention-days: 90Three details in that file carry the weight. sbom: true is the whole generation mechanism. steps.build.outputs.digest addresses the image by content rather than by tag, so the inspect step reads the attestation belonging to this build even if main moves during the run. And the upload step matters because each runner runs in its own virtual machine that is destroyed after the build (WarpBuild security documentation), so anything still on the runner disk at the end of the job is gone.
On a multi-platform build the .SBOM field becomes a map keyed by platform, and the format string changes to {{ json (index .SBOM "linux/amd64").SPDX }} (Docker SBOM attestations). That case is worth planning for early because a builder profile with both architectures enabled produces one attestation per architecture. The Docker builds hub covers the builder profile setup that sits under all of this.
Detail
Build-time attestation versus scanning the pushed image
The two documents differ in three ways that show up during an audit.
Binding. The attestation is a manifest inside the same image index as the runtime layers, addressed by the index digest. A post-push scan writes a file whose only link to the image is a tag string in the filename or the JSON. If the tag is rebuilt, the file describes something that is no longer there.
Coverage of intermediate stages. BuildKit reads two Dockerfile build args, BUILDKIT_SBOM_SCAN_STAGE and BUILDKIT_SBOM_SCAN_CONTEXT, which extend the scan to named build stages and to the build context (Docker SBOM attestations). A compile stage that vendors dependencies and emits a single static binary can therefore be inventoried even though nothing of its package metadata survives into the final layer. A scan of the final image sees only the final filesystem, so those dependencies are invisible to it.
Where the work happens. The scan runs on the builder that already holds the filesystem in its cache. A post-push scan job pulls the image back down over the network before it can start.
Where the SBOM lives and how a later job gets it
| Store | Written by | Read back by | Lifetime |
|---|---|---|---|
| Registry attestation manifest | sbom: true with push: true | docker buildx imagetools inspect <repo>@<digest> --format '{{ json .SBOM.SPDX }}' | As long as the image index exists in the registry |
| Workflow artifact | actions/upload-artifact@v4 | actions/download-artifact@v4 | 90 days by default (GitHub Actions limits, checked on 2026-08-13) |
| Release asset | gh release upload | gh release download | As long as the release exists |
| Local build output | outputs: type=local,dest=out | out/sbom.spdx.json on disk | The job only |
The fourth row is the escape hatch for builds that never push. With the local or tar exporter, BuildKit writes the attestation to a file named sbom.spdx.json at the root of the output directory instead of into an index (Docker SBOM attestations).
A release job in the same workflow takes the artifact route:
release:
needs: build
runs-on: warp-ubuntu-latest-x64-2x
steps:
- name: Fetch the SBOM the build job produced
uses: actions/download-artifact@v4
with:
name: sbom-${{ github.sha }}
- name: Attach it to the release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh release upload ${{ github.ref_name }} sbom.spdx.jsonA release job in a different workflow run cannot use the plain form. Pass run-id and a github-token with actions: read to actions/download-artifact@v4 (actions/download-artifact), or skip the artifact store entirely and run the imagetools inspect command against the digest you are promoting. The registry route is the one that still answers a question asked eleven months later, once the 90 day artifact retention has expired. If the SBOM also needs a Sigstore signature, actions/attest-sbom signs an existing document and records it in the transparency log, which is the same machinery covered in attaching build provenance in GitHub Actions.
What the scan costs
The scan is extra work on the builder, billed like the rest of the session. Builder rates from the Docker builders documentation and the pricing page, checked on 2026-08-13:
| Builder size | Architectures | Price per minute |
|---|---|---|
| 16 vCPU, 32 GB, 100GB disk | amd64, arm64, multi | $0.06 |
| 32 vCPU, 64 GB, 200GB disk | amd64, arm64, multi | $0.12 |
| 64 vCPU, 128 GB, 200GB disk | amd64, arm64, multi | $0.24 |
Substitute your own numbers into the arithmetic. If the SBOM stage adds one minute of builder session time and the repository runs 400 image builds a month, the 16 vCPU profile bills 400 x $0.06 = $24.00 per month for the inventory. The runner side of the same workflow, warp-ubuntu-latest-x64-4x, is $0.008 per minute (pricing page, checked on 2026-08-13). Measuring your own delta means comparing two session durations with the input flipped, which is cheaper than estimating it.
Related Questions
Do I need a separate scanner job to produce an SBOM?
No. BuildKit runs a scanner over the build result when the build step sets sbom: true and writes the SPDX document as an attestation next to the image manifest (Docker SBOM attestations). A separate job that pulls the pushed image and scans it produces a second inventory of the same bytes, one step further from the build. The security scanning guide covers the scanning jobs that remain worth running on their own schedule.
Why does my build fail when I turn the SBOM attestation on?
Attestations ride on an OCI image index, so the build needs an exporter that can write one (Docker build attestations). Pushing to a registry works. Loading into the classic local Docker image store does not, so a build that sets load: true needs the containerd image store or one of the file exporters instead.
How do I read the SBOM back out weeks after the build?
Address the image by digest rather than by tag and run docker buildx imagetools inspect <repo>@<digest> --format '{{ json .SBOM.SPDX }}'. The attestation lives in the registry for as long as the image index does, which outlasts the 90 day default retention on workflow artifacts (GitHub Actions limits, checked on 2026-08-13).
What does SPDX mean here, and is CycloneDX an option?
SPDX is the document format BuildKit emits by default, and SBOM, defined covers what the format records and why regulators ask for it. A different generator image can be selected through the sbom input when a downstream tool needs another format.
Set the flag on one image first, check the output of the inspect step, then roll it across the rest of the matrix from the Docker builds hub. Per-minute rates for every builder size and runner label are on the pricing page.
Start with $10 in free credits
Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.