DefectDojo is where security findings stop being files produced by scanners and start becoming work.
Trivy, Gitleaks, Kubeconform and Nessus can all produce useful results. Without a common intake, those results remain scattered across CI logs and exported reports. DefectDojo gives them a shared product, engagement and test history so repeated scans can update findings instead of creating another pile of disconnected output.
CI scanners and scheduled scans
│
▼
DefectDojo API
│
▼
Traefik → nginx → Django / uWSGI
│
├──► PostgreSQL
├──► Valkey
└──► Celery worker and beat
This is one of the more substantial application stacks in my homelab. The complexity is justified by the workflow it creates, but it deserves an equally explicit recovery plan.
One inbox for findings
My import automation uses the v2 API and re-import semantics. The first upload establishes the scan context; later pipeline runs update the same Test rather than duplicating every finding.
The current sources include:
Trivy filesystem and dependency findings
Gitleaks secret-scanning findings
Kubeconform Kubernetes schema findings
Nessus scheduled infrastructure findings
The important output is not a larger finding count. It is a shorter path from observation to triage, remediation and evidence that the issue stayed fixed.
Deployment layout
DefectDojo uses a Fleet-native Helm release with a Helm-free Kustomize post-render layer. A separate Helm-enabled Kustomize entry point remains available for local rendering.
defectdojo/
├── base/
├── overlays/
│ ├── homelab/
│ │ ├── values
│ │ └── fleet post-render resources
│ └── local/
│ ├── values
│ └── fleet post-render resources
└── fleet.yaml
The homelab environment currently uses chart 1.9.29; the local, production-like environment uses chart 1.9.37.
The application lives in the defectdojo namespace. Its runtime includes Django, nginx, Celery worker and scheduler, PostgreSQL, Valkey and an initializer Job.
Why the render path is split
Fleet inflates the Helm chart itself. Its post-render Kustomize directory therefore must not contain another helmCharts block. The manual entry point does contain that block and must be rendered with Helm support enabled.
Fleet path
Helm render by Fleet
→ Helm-free Kustomize post-render
manual path
Kustomize with Helm enabled
→ complete local render
Mixing the two paths produces a memorable but unhelpful failure: the controller asks Kustomize to render a Helm chart without the Helm flag. Keeping the paths separate makes both uses explicit.
There is a second Helm wrinkle around the PostgreSQL password. During an upgrade, the subchart tries to recover an existing value with lookup(). Fleet renders in a dry-run context where that lookup cannot see the downstream Secret. I provide the same value through Fleet’s protected values input at render time while the pod still reads its runtime credential from the namespace Secret.
The lesson is broader than this chart: render-time configuration and runtime configuration can have different visibility, even when they represent the same credential.
Node placement and storage
The local cluster spans several cloud providers. I pin all DefectDojo components to one labelled node so Django, workers, PostgreSQL, Valkey and local-path storage remain together.
The chart does not honour one global node selector, so the overlay applies placement to every component. If the label disappears, the failure appears as a collection of Pending pods and unbound volumes.
Persistent state includes:
PostgreSQL findings, users and workflow state
Valkey queue and cache support
media PVC uploaded reports and attachments
The media claim is 20 GiB. PostgreSQL and Valkey also have their own persistent claims. Database backup and media snapshot belong to the same recovery conversation.
Two authentication paths
The browser UI and automation need different ingress behaviour.
The UI passes through Authelia. The API route bypasses the browser-oriented forward-auth middleware and relies on DefectDojo API tokens. Traefik gives the API path a higher-priority match so CI receives JSON from DefectDojo instead of an HTML login page.
browser
→ Authelia
→ DefectDojo UI
CI importer
→ token-authenticated API route
→ DefectDojo API
Bypass does not mean unauthenticated. It means the API uses the authentication mechanism designed for automation.
Operating DefectDojo
I begin with the initializer and the main Django containers:
kubectl -n defectdojo get pods,job,svc,pvc,ingressroute
kubectl -n defectdojo logs job/defectdojo-initializer --tail=200
kubectl -n defectdojo logs deploy/defectdojo-django -c uwsgi --tail=200
kubectl -n defectdojo logs deploy/defectdojo-django -c nginx --tail=200
Then I inspect the background and stateful components:
kubectl -n defectdojo logs deploy/defectdojo-celery-worker --tail=100
kubectl -n defectdojo logs statefulset/defectdojo-postgresql --tail=100
kubectl -n defectdojo logs statefulset/defectdojo-valkey --tail=100
The first diagnostic split is:
first boot fails
└── initializer, bootstrap inputs or database readiness
login or page requests restart
└── uWSGI memory pressure or application settings
imports queue but do not finish
└── Celery, Valkey or worker resources
CI receives a login page
└── API route lost priority to Authelia-protected UI route
all pods Pending on local cluster
└── dedicated node label or local volume binding
A platform with several processes needs several health checks. The Django page can load while background imports are stuck.
Backup and recovery
The useful backup set is:
PostgreSQL backup
+
media volume snapshot
+
runtime cryptographic and database inputs
+
Fleet render inputs
+
Git configuration
My recovery order is:
1. Restore placement prerequisites on the target cluster
2. Restore runtime and Fleet render inputs
3. Restore PostgreSQL and media state
4. Reconcile the correct environment
5. Confirm the initializer and all pods
6. Sign in through the UI
7. Submit or re-import a small known scan
The final import validates the API route, token, Django application, queue, database and finding workflow together.
Rollback
Chart changes, overlay changes and database migrations do not have identical rollback boundaries. A Git revert can restore the previous render. It cannot guarantee that a newer application has not changed persistent state.
For significant upgrades I keep the matching PostgreSQL backup and media snapshot until the UI and an ingestion cycle have been validated. I also change the smallest layer possible: chart version, values, post-render resources and import automation should not all move in one opaque change.
Things worth remembering
At the time of writing:
Role central SecOps findings intake
Namespace defectdojo
Runtime Django, nginx, Celery, PostgreSQL, Valkey
Deployment Fleet Helm + Kustomize post-render
Charts 1.9.29 and 1.9.37 by environment
UI auth Authelia forward auth
API auth DefectDojo token on a separate route
Recovery test login plus a real scan re-import
DefectDojo earns its complexity when it turns another scanner report into a finding with an owner, history and next action. The platform is healthy when that complete loop works—not merely when Django returns a page.