Hardware Sizing for Self-Hosted DefectDojo Pro (Pro)
Sizing a DefectDojo deployment comes down to two questions. How much data are you holding, and how many people are working in it at once. This page gives starting points for both.
Treat what follows as general guidance rather than a specification. The figures lean deliberately conservative, and they assume a deployment doing everyday triage alongside regular scan imports. Your own numbers will move depending on how you use the product, so read the notes under the table before you provision anything.
Specs are given as generic vCPU and memory figures so they apply to any cloud provider or on-premise hardware. The sizing table has a tab for each deployment method: Kubernetes runs the application tier as pods across nodes and scales out, while Docker Compose runs everything on one host and scales up. Whichever you choose, read Sizing and tuning the application tier too, where the two differ most.
Sizing table
How to read these numbers
Database memory matters more than database CPU
DefectDojo runs aggregation-heavy queries across your findings. Those stay fast while the working set and its indexes are served from memory, and they degrade quickly once the database starts reaching for disk. When you have to choose, buy memory before you buy cores. The table reflects that. Memory roughly doubles from tier to tier while CPU counts move much more slowly.
The application tier tracks users, not findings
The concurrent user figures in the table assume smaller datasets belong to smaller teams. That assumption breaks often. If you hold 200K findings but have 100 people in the UI at once, size the application tier for the users and leave the database where your finding count puts it. The two scale independently.
There is one exception, at the far end of the table. Import and deduplication run on the application tier rather than in the database, so once a data set is large enough for that work to dominate, the application tier follows ingest volume instead of user count. That is why the 500M row sits well above what its user figure on its own would suggest.
Sizing and tuning the application tier
The “Application nodes” column is the compute the application tier needs. How you provide it, and what you have to do to make the application actually use it, is where the two deployment methods differ.
Tuning the database
The database is where finding queries live or die, and PostgreSQL ships with defaults tuned for a small machine. On a dedicated database host you have to raise a handful of settings before it will use the memory you gave it. A 64 GB host left on stock shared_buffers performs like a small one. These are the settings that move the needle for DefectDojo’s aggregation-heavy reads, in rough order of impact:
| Setting | Starting point | What it controls |
|---|---|---|
shared_buffers | ~25% of the host’s RAM | PostgreSQL’s own cache of table and index pages. This is the lever behind “buy memory before cores”: while the working set and its indexes fit here, finding queries stay fast; once they don’t, the database reaches for disk and latency climbs sharply. |
effective_cache_size | ~50โ75% of RAM | A planner hint, not an allocation. It tells the planner how much data is likely cached, which steers it toward index scans over full-table scans on your findings. Set too low, the planner picks slow plans on a host that had the memory all along. |
work_mem | 32โ128 MB | Memory for one sort or hash step, and DefectDojo sorts and groups findings constantly. It is allocated per operation per connection, so a large value multiplied by hundreds of connections is how you run the host out of memory. Raise it in small steps. |
maintenance_work_mem | 512 MBโ2 GB | Memory for index builds, VACUUM, and schema migrations. It doesn’t affect steady-state queries, but it decides how long an upgrade’s migrations and reindexes take. |
max_connections | above the application’s total | The ceiling on concurrent connections. It has to clear what the application holds open, with headroom. Too low and requests fail outright under load; far too high and every idle connection still costs memory. |
On a managed database (RDS, Cloud SQL, and the like) these are parameter-group settings rather than lines in postgresql.conf, but they are the same knobs. Whichever you run, raise them together with the memory you provision.
Keep the connection budget balanced
The application tier and the database are tuned against one shared number: how many connections the application holds open. Every uWSGI process and every Celery worker keeps its own, so the total is roughly:
(uWSGI processes ร uWSGI threads) + Celery worker concurrencyOn a 12 vCPU host tuned as above that is 18 ร 4 + 12 = 84, and under load it settles near that. Keep the database’s max_connections comfortably ahead of the number you land on. The failure mode is not the obvious one: too many processes and workers open more connections than the database can serve and end up hammering it, which surfaces as slow queries and 500s under load rather than as more throughput. Raise the application’s process and worker counts gradually while watching the connection count and CPU headroom, rather than setting a large multiple of the core count up front.
Storage
Plan on 20โ30 GB of database storage per million findings. Where you fall in that spread depends on how much you hang off each finding. Long descriptions and large endpoint counts push you toward the top of it. The finding rows themselves are a small part of this. Most of the space goes to indexes and to the related tables that hang off each finding, so sizing from row data alone will leave you well short.
Every tier through 10M fits inside a few hundred GB of general-purpose SSD. Storage is cheap next to the cost of running out, so provision for where you expect to be in a year rather than where you are now. If your provider offers storage autoscaling, turn it on.
The 500M row is sized at 2.5 TB. That figure assumes the live data set is actively managed, with older findings archived out of the hot path rather than accumulating indefinitely. Applied naively, the per-million rate above would put an unmanaged 500M deployment several times higher. If you are heading toward this scale, treat the archiving strategy as part of the sizing exercise rather than something to sort out later.
Storage at this scale also needs attention to throughput, not only capacity. Once the working set stops fitting in memory, default baseline IOPS on general-purpose volumes becomes the limit well before capacity does.
Media storage is separate and usually much smaller. It holds uploaded artifacts such as screenshots and risk acceptance documents, so size it from your own upload habits.
What pushes you up a tier
Finding count is the headline number, but several things will have you sizing up sooner than the count alone suggests.
- Import volume and frequency. Large scans arriving often, especially several at the same time, put sustained load on both the database and the async workers. CI pipelines that import on every build are the usual cause.
- Deduplication. Deduplication compares incoming findings against what you already hold. The more findings you have and the broader your deduplication configuration, the more work every import does.
- Reporting and dashboards. Metrics views and large report generation are read-heavy, and they hit the database harder than day-to-day triage does.
- API traffic. Integrations that poll or pull large result sets add concurrent load that never shows up in your interactive user count.
- Retention. Deployments that keep everything forever grow into the next tier on schedule. Archiving or deleting old data keeps you where you are for longer.
Very large deployments
Past the 10M tier, hardware stops being the whole answer. Two things change.
The binding constraint moves from reading to writing. Deduplication compares each incoming finding against what you already hold, so the cost of an import grows with the size of the data set behind it. At the top of the table this is usually what you hit first, ahead of anything users notice in the UI. Whatever import volume built a data set that large is generally still running, so you pay that cost continuously rather than once.
The memory figures assume the hot set stays small. A deployment works recent findings and leaves older ones largely untouched, which is what lets a database hold far more data than it has memory and still perform well. If your access pattern is genuinely spread across the whole data set, you will need more memory than the table lists, and past a point no single instance will have enough.
Both of those point at the same work. Partitioning and archiving cold findings out of the live data set matter more at this scale than another increment of vCPU, and heavy reporting belongs on a read replica rather than on the primary. Plan for that alongside the hardware rather than after it, and talk to us before you provision.
When in doubt, round up
The figures here already lean conservative, and being one size too large costs far less than being one size too small. Database memory pressure in particular does not degrade gracefully. Performance holds up fine until it doesn’t.
Adding application capacity later is straightforward: on Kubernetes you add pods or nodes, and on Docker Compose you resize the host and raise the process and worker counts as above. Resizing a database typically means downtime, so that is the one worth getting right up front.
How long the first start takes
A new install spends most of its first start in the initializer, which creates the whole database schema before the application comes up. That is a long run of database migrations, so it is slowest on the smallest hosts.
On Docker Compose, allow about 30 minutes for the first start on the smallest tier in the sizing table (a 4 vCPU / 16 GB application host with a 2 vCPU / 16 GB database); larger hosts may be faster. The time also depends on disk and database performance, so we do not publish a figure for each tier. Later starts and restarts are much faster, because the schema already exists and the initializer has little left to do.
dojo-compose-cli 2.1.x can stop waiting before a slow first start has finished and report a failure while the initializer is still working. The install can be completed without starting over: see First install reports a failure while the initializer is still running.
Questions or support
These are starting points, not limits. If your deployment sits at the top of the table, or your workload doesn’t resemble the assumptions here, talk to us before you provision. Contact your account representative or support@defectdojo.com.