- how-to
- Kubernetes
- SBOM
- self-hosting
Dependency-Track on Kubernetes: installing 5.x with the Helm chart, and feeding it from the cluster
Dependency-Track runs on Kubernetes from its official Helm chart, but it only analyses SBOMs you send it. Feeding it from the cluster is the work you build.
StackRadar Team
13 min read
Dependency-Track runs on Kubernetes from its official Helm chart, and the chart gets you to a login screen in one command. It does not get you a single SBOM. Dependency-Track analyses what you upload and never looks at the cluster it runs in, so the real decision is not how to install it but what feeds it. Most of this post is about the second part: the three ways to keep a project tree in Dependency-Track matching what the kubelets are running, with the flags and the API fields each one uses.
The install half is current for chart dependency-track 2.9.0, which ships Dependency-Track 5.2.1, both released on 25 October 2026. The sizing numbers come from the v5 production guide, the commands from the chart README and the v5 docs, and nothing below was typed from memory. StackRadar, our product, is not one of the feeders and is covered in one section at the end, including what it does not do.
What the chart deploys
Dependency-Track 5 is two containers and a database. The chart deploys the first two and expects you to bring the third.
| Component | What it is | Chart default |
|---|---|---|
| API server | dependencytrack/apiserver: the REST API, the background workers and the durable execution engine. One Deployment in the default monolith topology, or separate web and worker Deployments in split. | 2 Gi memory, 500m CPU, one replica; ports 8080 (API) and 9000 (health, metrics) |
| Frontend | dependencytrack/frontend: a static single-page app whose browser calls the API server directly, which is why frontend.apiBaseUrl must be a URL the browser can reach. | 64 to 128 Mi memory, one replica |
| PostgreSQL | Version 14 or newer, the only supported database in v5 (H2, MySQL and SQL Server were dropped). Queues, cache, search and metrics all live in it. | Not in the chart. You set database.jdbcUrl and a Secret. |
| File storage | Uploaded BOMs. A PVC by default; s3 for more than one API-server replica. | local, ReadWriteOnce |
| Key-encryption key | Encrypts the integration secrets (Snyk, Trivy, OSS Index tokens) stored in the database. Every instance must share it. | Not in the chart beyond the quickstart fixture. |
Three things to know before the first helm install. Chart 2.x is for Dependency-Track 5.0.0 and later and requires Kubernetes 1.27; chart 1.x (1.7.0, app 4.14.6) is the 4.x line. The separate hyades chart was the v5 preview and the v5 docs say plainly not to use it. And the chart is published as an OCI artifact and signed with GPG from 2.1.0 on, so helm pull --verify works if you import the project's key.
The quickstart, in one command
The chart README ships a values-quickstart.yaml that bundles an inline PostgreSQL and a fixture key, so you can see the product without provisioning anything:
helm install dt oci://ghcr.io/dependencytrack/helm-charts/dependency-track \
--create-namespace -n dependency-track \
-f https://raw.githubusercontent.com/DependencyTrack/helm-charts/main/charts/dependency-track/values-quickstart.yaml
kubectl -n dependency-track port-forward svc/dt-dependency-track-api-server 8080:8080 &
kubectl -n dependency-track port-forward svc/dt-dependency-track-frontend 8081:8080 &Open http://localhost:8081 and sign in as admin / admin; the first login forces a password change before anything else works. Both port-forwards are needed because the frontend is static and the quickstart sets frontend.apiBaseUrl to http://localhost:8080. The API server waits for PostgreSQL in an init container and then runs the schema migrations on first start, so the pod is not ready the instant it is scheduled. We ran exactly these commands on 4 November 2026 against a two-CPU, 3.5 GB minikube on Kubernetes 1.27.4: Helm returned in 4 seconds and all three pods (dependencytrack/apiserver:5.2.1, dependencytrack/frontend:5.2.1, postgres:18-alpine) were ready 171 seconds after the install command, most of it image pulls and the first migration.
What a production install adds
A real deployment replaces the two fixtures with Secrets you provision and adds the pieces the chart leaves to you. The values below use only keys from the chart's values reference; adjust the names to your cluster.
database:
jdbcUrl: "jdbc:postgresql://pg.example.internal:5432/dtrack?reWriteBatchedInserts=true"
existingSecret:
name: dt-database # keys: username, password
secretManagement:
database:
kek:
existingSecret:
name: dt-kek # openssl rand -base64 32, shared by every instance
apiServer:
topology: split # web and worker scale independently
initializer:
enabled: true # migrations run once, as a Helm hook, not in every pod
web:
replicaCount: 2
resources:
requests: { cpu: "2", memory: 2Gi }
limits: { memory: 2Gi }
worker:
replicaCount: 2
resources:
requests: { cpu: "2", memory: 2Gi }
limits: { memory: 2Gi }
serviceMonitor:
enabled: true # scrapes /metrics on port 9000
fileStorage:
provider: local
local:
accessModes: [ReadWriteMany] # more than one api-server replica needs RWX, or provider: s3
ingress:
enabled: true
ingressClassName: nginx
hostname: dependency-track.example.com
tls:
- secretName: dt-tls
hosts: [dependency-track.example.com]
frontend:
apiBaseUrl: https://dependency-track.example.comWhat each block is for, in the order the production guide raises them:
- PostgreSQL on its own host. The guide starts at 8 GB and 4 cores, never below 4 GB and 2 cores, with
max_connectionscovering 30 per instance plus headroom, and says PostgreSQL becomes the bottleneck before the API server does. A managed offering is fine as long as it allows thepg_trgmextension, which the schema migration creates itself. - Two instances sharing one database and one file store. Instances coordinate only through PostgreSQL; there is no broker. More than one API-server replica needs file storage every replica can reach, which is why the local PVC must be ReadWriteMany or the provider must be
s3. - The initializer Job. Without it every API-server pod runs the migration check on start. With it, a
pre-install/pre-upgradehook runs the tasks once; it requiresdatabase.existingSecretbecause the hook runs before the chart's own Secrets exist. Argo CD runs it as aPreSyncstep. - TLS at the ingress, probes on 9000. The API server does not terminate TLS. It serves
/health/live,/health/readyand/health/startedon the management port, and the chart's startup probe budgets 30 minutes for a long migration, so pass--timeoutto Helm on major upgrades or its five-minute default fires first. - An identity provider. Local users are for evaluation; the guide points at OIDC or LDAP, and the chart sets the environment for either through
apiServer.extraEnv.
Backups are a PostgreSQL backup plus the file store. Upgrades within 5.x roll without downtime when the release notes allow it. The move from 4.x is not an upgrade at all: v5 is migrated into by an offline tool from a 4.14.2-or-later instance, the chart goes from 1.x to a 2.x reinstall, and 4.14 reaches end of life on 9 December 2026. The compare page has the migration in more detail.
Feeding it from the cluster
This is the half the install guides skip. A project in Dependency-Track is whatever something uploads under that name. Nothing in the product discovers running images, reads a registry or notices a rollout, and the official community integrations list has CI plugins, a Jenkins plugin, a Backstage plugin and a Go client, but no Kubernetes feeder. Three exist outside it, and all three end at the same API call.
The call is a multipart POST /api/v1/bom. With autoCreate=true the project is created on first upload, and on creation Dependency-Track also honours parentName and parentVersion (or parentUUID) to place it in a hierarchy, projectTags as a comma-separated list, and isLatest=true. The key needs BOM_UPLOAD and, to create projects, PROJECT_CREATION_UPLOAD. The convention every feeder converges on: one parent project per cluster, one child project per image with the tag or digest as the version, and the namespace and workload carried as tags, because that is the only place the schema has for them.
sbom-operator: the least work
sbom-operator (0.49.0, 3 November 2026) watches pods, runs the Syft bundled in its release on every image it has not seen, and sends the result to one or more targets. With the dtrack target it uploads one project per image, named by the full image name with the tag (or digest) as the version, and it is the only feeder that manages the active and isLatest flags from what is actually running. Its chart turns the args map into --key=value flags:
args:
targets: dtrack
format: cyclonedx
dtrack-base-url: https://dependency-track.example.com
dtrack-api-key: odt_xxxxxxxxxxxxxxxx
kubernetes-cluster-id: prod-fra1
dtrack-kubernetes-cluster-id-mode: tag-namespace # tags each project <cluster>/<namespace>
dtrack-default-parent-project: 4f2a… # UUID of the "prod-fra1" parent project
dtrack-manage-project-active-status: "true" # needs the parent; flips active/isLatest
dtrack-deployment-history: "true" # keeps a deployed-stage tag and timestampshelm repo add ckotzbauer https://ckotzbauer.github.io/helm-charts
helm install sbom-operator ckotzbauer/sbom-operator -n sbom-operator --create-namespace \
-f sbom-operator-values.yamlTwo caveats from its README. isLatest needs Dependency-Track 4.12 or later, which 5.x satisfies. And discovery is either a cron run or a live informer; in live mode the operator also runs once at startup and annotates each pod with the image ids it has processed, so an image is uploaded once rather than once per pod. Narrow the scope with pod-label-selector and namespace-label-selector before pointing it at a cluster with a thousand pods.
Trivy Operator plus a bridge
If Trivy Operator is already in the cluster, it can emit a SbomReport custom resource per container and broadcast each report to a webhook. sbomreport-to-dependencytrack is a small server that receives those webhooks and uploads them, with Go templates for the project name, version and tags, so kube_namespace:[[.sbomReport.metadata.namespace]] lands as a tag. It can also deactivate or delete the project when Trivy Operator reports the SBOM gone. It is the right choice when you want one scanner in the cluster, not two; it is also a 21-star project whose last release, 0.0.14, is from 28 January 2026 and whose last commit is from April, so read the code you are about to depend on.
Your own CronJob, with or without a stored key
The whole feeder is Syft plus one curl, which is why many teams write it themselves. The running-cluster SBOM post covers the Syft half, including how to list what is running by digest. The upload half, with the fields from the v5 docs:
syft registry:ghcr.io/acme/api@sha256:3f1c… -o cyclonedx-json > bom.json
TOKEN=$(curl -sSf -X POST "https://dependency-track.example.com/api/v1/bom" \
-H "X-Api-Key: ${DT_API_KEY}" \
-F "projectName=ghcr.io/acme/api" \
-F "projectVersion=sha256:3f1c…" \
-F "autoCreate=true" \
-F "parentName=prod-fra1" \
-F "projectTags=namespace:payments,workload:deployment/api" \
-F "isLatest=true" \
-F "bom=@bom.json" \
| jq -r .token)
# processing is asynchronous; poll until the analysis has run
curl -sSf -H "X-Api-Key: ${DT_API_KEY}" \
"https://dependency-track.example.com/api/v1/event/token/${TOKEN}"We ran that upload against the quickstart instance on 4 November 2026 with the 68-package nginx:1.27-alpine SBOM from our CycloneDX reference. The response carried a token, the token reported COMPLETED 11 seconds later, and the project appeared under the parent with both tags, isLatest true and 1,047 components, which is the 68 packages plus the file entries Syft records. What it found is in the next section, because it is the part worth knowing before the first real upload.
Since 5.2.0 (8 October 2026) the CronJob does not need a stored key at all. Dependency-Track can exchange a Kubernetes service-account token for a session: you register the cluster's token issuer as a workload identity provider (for a cluster whose issuer only resolves internally, paste the output of kubectl get --raw /openid/v1/jwks as the key set), bind the subject system:serviceaccount:<namespace>:<name> to a service account that has only BOM_UPLOAD and PROJECT_CREATION_UPLOAD, and give the pod a projected token with Dependency-Track's URL as the audience:
spec:
serviceAccountName: bom-uploader
containers:
- name: upload-bom
image: acme-inc/upload-bom:1.0.0
volumeMounts:
- name: dt-token
mountPath: /var/run/secrets/dependency-track
readOnly: true
volumes:
- name: dt-token
projected:
sources:
- serviceAccountToken:
audience: https://dependency-track.example.com
expirationSeconds: 3600
path: tokenACCESS_TOKEN=$(curl -sSf -X POST \
"https://dependency-track.example.com/api/v2/oauth/token" \
-d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
-d "subject_token_type=urn:ietf:params:oauth:token-type:jwt" \
-d "subject_token=$(cat /var/run/secrets/dependency-track/token)" \
-d "workload_identity_provider=k8s-prod" \
-d "service_account=bom-uploader" \
| jq -r .access_token)
curl -sSf -X POST "https://dependency-track.example.com/api/v1/bom" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-F "projectName=ghcr.io/acme/api" -F "projectVersion=sha256:3f1c…" \
-F "autoCreate=true" -F "parentName=prod-fra1" \
-F "bom=@bom.json"The kubelet refreshes the projected token file, so read it right before each exchange, and exchange once per run: sessions last the provider's lifetime (an hour by default) and nothing renews them. The v5 docs note that the exchange endpoint returns the same invalid_request for every failure and that the API server log is where the refused claims show up.
What you get once it is fed, and what you do not
With any of the three feeders running, Dependency-Track does the thing it is good at. Every project is re-analysed as the NVD, GitHub Advisories, OSV and the other configured sources change, without anyone re-uploading. EPSS scores arrive with the findings, the CISA and ENISA KEV catalogues are mirrored since 5.1 and usable in policies, and a policy violation or a new finding can fire a signed webhook. The project tree gives you a per-cluster view and the tags give you per-namespace filters.
What a fresh instance actually reports is worth measuring, so we did. Eleven seconds after the upload the nginx project had 2 findings, both from the NVD, because the instance was three minutes old and its NVD mirror had only finished the 2026 feed. The full mirror, 2002 to 2026, took seven and a half minutes. Findings on an existing project only change when it is analysed again, on the portfolio schedule or when you trigger one, so the count stayed at 2 until we re-ran the analysis, which took 4 seconds and reported 20, all from the NVD and all matched on CPE. That is the ceiling of a default install: the only source it enables is the NVD. GitHub Advisories is off because it needs a token, and OSV is off and, when turned on, defaults to PyPI, NuGet, Maven, Go and npm, none of which matches an apk package.
Enabling OSV with the Alpine ecosystem added (Administration, Vulnerability Sources, OSV, or a PUT to the extension config API) mirrored 287,684 advisories in 133 seconds, and the next analysis, 3 seconds, reported 76 findings: 4 critical, 33 high, 33 medium and 6 low, 56 of them from OSV. Trivy and Grype reported 139 and 198 distinct IDs on the same image five days earlier from their own databases, so the number is not comparable across tools and is not the point. The point is that Dependency-Track reports what its enabled sources know, and a container portfolio needs OSV with the distro ecosystems switched on, and mirrored, before the first upload lands.
Three things it does not give you, because they are not in its model:
- Per-digest history, unless you make it. A re-uploaded BOM for the same project version overwrites the previous one. If the feeder uses the tag as the version, a rebuild on the same tag erases what was there; use the digest as the version if you want to see what was running last month.
- Kubernetes context on findings. The namespace, the workload, the Helm release and the Argo CD application exist only as tags you put there. The affected-projects view for a vulnerability shows project names, and an open request on the tracker (#7830, 30 October 2026) asks for owner and served-URL fields precisely because operators keep that mapping outside the tool.
- A second opinion on the SBOM. Dependency-Track trusts the document it is given. A Syft SBOM of a distroless or scratch image can be nearly empty, and the project will look clean. The feeder's choices decide what the platform can see; the CycloneDX reference shows what a complete one looks like.
Where StackRadar sits
StackRadar is the same idea, store the SBOM and re-match it as advisories land, built from the cluster inward rather than from CI outward. Its scanner discovers the running images itself, generates a CycloneDX SBOM per image digest, and attaches the namespace, workload, Helm release and Argo CD application to every finding, so the feeder problem this post is about does not exist in it. It is open source and self-hosts from its own Helm chart, or runs as a hosted service.
What it does not do, as of 4 November 2026: it does not push its SBOMs into Dependency-Track, so it is not a fourth feeder for the tree above, and it does not accept SBOMs you generated elsewhere, so it is not a home for the firmware, desktop and supplier documents that make Dependency-Track the system of record for many teams. If your SBOMs cover more than containers, or you need its policy engine and VEX workflow, run Dependency-Track and feed it with one of the three options above. If what you need to watch is the images in a few clusters and you would rather not run the feeder, the database and the migrations, the compare page and the self-hosted roundup lay the two side by side.
Frequently asked questions
Can I run Dependency-Track on Kubernetes?
Yes. The project publishes a Helm chart, dependency-track, as an OCI artifact at ghcr.io/dependencytrack/helm-charts. Chart 2.x installs Dependency-Track 5.x and needs Kubernetes 1.27 or newer plus a PostgreSQL 14 or newer database you provide; chart 1.x installs 4.x. The chart deploys the API server and the frontend; the database, the ingress, the key-encryption secret and the file storage are yours to supply for anything beyond an evaluation.
Which Helm chart is the right one for Dependency-Track 5?
The dependency-track chart at version 2.x, from ghcr.io/dependencytrack/helm-charts or the classic repository at dependencytrack.github.io/helm-charts. Do not use the separate hyades chart: it was the preview chart for v5, the maintainers deprecated it, and it receives no updates. Chart 2.x is not a drop-in upgrade from chart 1.x or from hyades; both paths are a reinstall that the chart’s UPGRADING.md walks through.
Does Dependency-Track scan a Kubernetes cluster?
No. It never looks at a cluster, a registry or an image. It analyses the CycloneDX SBOMs that something uploads to it and re-analyses them as advisories change. To cover what runs in a cluster you add a feeder: sbom-operator, Trivy Operator with the sbomreport-to-dependencytrack bridge, or your own CronJob that runs Syft and posts to /api/v1/bom.
How do I get SBOMs from Kubernetes into Dependency-Track automatically?
The least work is sbom-operator with --targets=dtrack: it watches pods, runs Syft on every new image, and uploads one project per image with the tag as the version, the cluster id as a tag and, if you give it a parent project, the active and isLatest flags kept in step with what is running. If you already run Trivy Operator, enable its SBOM reports and point its webhook at sbomreport-to-dependencytrack. If you want full control, a CronJob with syft and curl is about twenty lines.
How much memory and CPU does Dependency-Track need?
The v5 production guide suggests 2 GB of memory and 4 CPU cores per API server instance as a starting point, with floors of 1 GB and 2 cores below which it will not sustain load, and 8 GB with 4 cores for PostgreSQL, never below 4 GB and 2 cores even for evaluation. The chart defaults to 2 Gi and 500m CPU for the API server and 64 to 128 Mi for the frontend. PostgreSQL becomes the bottleneck before the API server does.
Can I helm upgrade from Dependency-Track 4 to 5?
No. Dependency-Track 5 does not upgrade in place from 4: a v4 instance on 4.14.2 or later is taken offline, an offline migrator copies its data into a PostgreSQL cluster dedicated to v5, and the chart move from 1.x to 2.x is a reinstall. Plan a maintenance window. The 4.14 line gets security and high-severity fixes until 9 December 2026.
Why does a fresh Dependency-Track show so few findings for a container image?
Because a default install enables only the NVD, which matches on CPE, and because findings on a project only change when it is analysed again. On 4 November 2026 a 5.2.1 quickstart reported 2 findings for nginx:1.27-alpine eleven seconds after upload, 20 after the NVD mirror finished and the project was re-analysed, and 76 after OSV was enabled with the Alpine ecosystem and mirrored. GitHub Advisories needs a token and OSV defaults to the language ecosystems, so turn on OSV with your distro ecosystems before the first upload.
Can a pod upload SBOMs without storing a Dependency-Track API key?
Since 5.2.0 (8 October 2026), yes. Register the cluster’s service-account token issuer as a workload identity provider, bind the subject system:serviceaccount:<namespace>:<name> to a service account with BOM_UPLOAD and PROJECT_CREATION_UPLOAD, give the pod a projected token with Dependency-Track’s URL as the audience, and exchange it at /api/v2/oauth/token for a session. Earlier versions authenticate automation with team API keys only.
More from the blog
All postsHow to find every pod running a vulnerable package across a Kubernetes cluster
Given a package name, list every pod that runs it, its Deployment or DaemonSet and its Helm release: one SBOM per running digest, a jq query and a digest join.
12 min read
docker sbom is gone: how to generate an SBOM from a Docker image now
Docker removed docker sbom and archived the plugin on 19 May 2026. Use docker scout sbom, Syft or Trivy instead: the commands, and why their counts differ.
10 min read
How to list every container image running in a Kubernetes cluster, and why the pod spec is the wrong place to look
The kubectl one-liners, and why they list the wrong thing: spec.image is a request, the kubelet’s imageID is what runs. A jq query lists it per pod and digest.
11 min read
Open-source Snyk Container alternatives: one tool per job, and what you give up
There is no single open-source Snyk Container, but every job it does has an open-source tool: Trivy, Grype, Renovate, Harbor, Trivy Operator and Dependency-Track.
10 min read