• 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.

ComponentWhat it isChart default
API serverdependencytrack/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)
Frontenddependencytrack/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
PostgreSQLVersion 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 storageUploaded BOMs. A PVC by default; s3 for more than one API-server replica.local, ReadWriteOnce
Key-encryption keyEncrypts 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:

bash
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.

The quickstart values are for evaluation and say so in their first line. The bundled PostgreSQL has no persistence, so a pod restart loses everything, and the key-encryption key is a public fixture, so any integration secret you store is readable by anyone with the file. The API server is also capped at 1 Gi, below the 2 GB the production guide starts from.

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.

values.yaml
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.com

What 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_connections covering 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 the pg_trgm extension, 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-upgrade hook runs the tasks once; it requires database.existingSecret because the hook runs before the chart's own Secrets exist. Argo CD runs it as a PreSync step.
  • TLS at the ingress, probes on 9000. The API server does not terminate TLS. It serves /health/live, /health/ready and /health/started on the management port, and the chart's startup probe budgets 30 minutes for a long migration, so pass --timeout to 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.

IN THE CLUSTERDEPENDENCY-TRACKsbom-operatorSyft per image, --targets dtrackTrivy Operator + a bridgeSbomReport webhook → uploadYour own CronJobsyft + curl POST /api/v1/bomOne project per imageparent project = the cluster,version = the tag or digestautoCreate=trueparentName = the clusterprojectTags, isLatest=true

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:

sbom-operator-values.yaml
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 timestamps
bash
helm 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.yaml

Two 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:

bash
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:

cronjob-pod-spec.yaml
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: token
bash
ACCESS_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.

All posts