Resource sizing
Measured per-container memory against the declared compose limits, per-profile totals, the three GPU paths, and a recipe for computing your own total.
- Version: 0.4
- Role: admin_user
- Type: reference
The per-service ports and profiles are in services, ports and profiles, which is generated from the compose files. This page is the sizing arithmetic on top of them: what each container actually used when it was measured, what that totals per profile, and how to compute a figure for your own deployment. For the host you should provision, start at requirements and sizing; for what an added component costs, see optional services and what each one costs.
Reading the numbers
Container memory has three readings and they differ by gigabytes.
| Reading | What it is | Use it for |
|---|---|---|
| Anonymous memory | Pages that must be backed by RAM or swap | Sizing. This is the number to buy memory against. |
What docker stats prints | Anonymous memory plus active page cache | Nothing, on its own. It over-reports, worst right after a model load. |
| Peak | A high-water mark of the above, which never decreases while the container lives | Only as a hint. For a container whose page cache reaches its limit it saturates at the limit and says nothing. |
Whole stack, 21 containers, idle: 12.53 GiB of anonymous memory, against 15.00 GiB that
docker stats prints and 18.00 GiB of charged memory. The 2.5 GiB difference is active page cache,
most of it the model files the detectors read at startup, and the kernel drops it under pressure
without the process noticing.
Per container
Measured 2026-09-08 on the development server: Ubuntu 24.04, 10 vCPU, 32 GB, no GPU, cgroup v2,
the default compose profile plus the llm-analyst and tools profiles. Idle means the stack had
been up two hours with no traffic. Declared is the compose memory limit; a dash means the service
declares none.
| Container | Declared | Anonymous, idle | docker stats | Peak |
|---|---|---|---|---|
| attack-guard | 4 GB | 2.72 GiB | 2.803 GiB | 4.00 GiB |
| hap-guard-v2 | 6 GB | 2.70 GiB | 2.769 GiB | 4.50 GiB |
| ml-detector | 6 GB | 2.45 GiB | 2.504 GiB | 3.94 GiB |
| doc-classifier | 2 GB | 920 MiB | 1.632 GiB | 1.75 GiB |
| risk-analyst | 3 GB | 882 MiB | 1.458 GiB | 2.01 GiB |
| prompt-compressor | 3 GB | 633 MiB | 796.8 MiB | 1.02 GiB |
| mcp-server | 1 GB | 594 MiB | 669.7 MiB | 892 MiB |
| language-detector | 2 GB | 480 MiB | 519.8 MiB | 537 MiB |
| hallucination-guard | 4 GB | 474 MiB | 654.1 MiB | 828 MiB |
| pgadmin | — | 241 MiB | 312.4 MiB | 369 MiB |
| nestjs-core | — | 110 MiB | 207.5 MiB | 320 MiB |
| deepteam | 2 GB | 103 MiB | 136.9 MiB | 180 MiB |
| ocr-service | 4 GB | 101 MiB | 193.3 MiB | 241 MiB |
| document-parser | 2 GB | 77 MiB | 124.7 MiB | 157 MiB |
| report-renderer | 512 MB | 62 MiB | 91.0 MiB | 115 MiB |
| redactor | 2 GB | 59 MiB | 95.9 MiB | 118 MiB |
| postgres | — | 12 MiB | 61.4 MiB | 104 MiB |
| reconfigure-agent | — | 9 MiB | 20.1 MiB | 31 MiB |
| frontend | — | 8 MiB | 19.2 MiB | 30 MiB |
| cid-proxy | 128 MB | 4 MiB | 11.3 MiB | 18 MiB |
| redis | — | 4 MiB | 14.3 MiB | 61 MiB |
Three services — the jailbreak, toxicity and PII detectors — are 7.87 GiB of the 12.53 GiB total:
63 % of the idle stack is three ONNX detectors. Each uvicorn worker loads its own full copy of
the model, so UVICORN_WORKERS moves this table more than any other setting. The same stack idled
at 7.3 GiB with one worker per service against 12.5 GiB with two to four.
Two containers are missing from this table because nothing constrains them and nothing measured them: the services that declare no limit are the reverse proxy, the dashboard, the gateway, the database, the cache and the host reconfigure agent — all of them small here, but unbounded by design rather than by intent.
Per profile
| Profile | Services | Declared memory | Measured, idle |
|---|---|---|---|
| Default — no profile | 19 | 37.63 GB | 10.85 GiB anonymous |
llm-analyst | 2 | +4 GB | +1.5 GiB anonymous |
routing | 1 | +1 GB | Not measured |
inline-proxy | 1 | None declared | Not measured |
userid-agent | 1 | None declared | Not measured |
llm | 1 | None declared | A GPU host of its own |
tools | 2 | None declared | 241 MiB for the database console |
ldap-test, mailtest, llm-build | 3 | None declared | Development and build hosts only |
Six of the nineteen default-profile services declare no limit, so 37.63 GB is a floor rather than a ceiling and a host running out of memory is not diagnosable from the compose file alone.
What load adds, and does not give back
| Event | Added anonymous memory |
|---|---|
| PII detector, sustained traffic — the ONNX arena grows and never returns | +1.2 GiB |
| OCR, first image in the first language | +1.7 GiB |
| OCR, first image in a second language | +0.8 GiB |
llm-analyst profile started | +1.5 GiB |
Measured end to end: the same 21 containers went from 12.53 GiB in the morning to 15.25 GiB after one OCR matrix and three detector bursts, then idle again. None of the 2.7 GiB came back. Sizing from an idle reading under-counts the PII detector alone by more than a gigabyte.
CPU, not memory, is the binding constraint on the image path. One A4 page costs 190 CPU-seconds and runs 8.4 cores wide, so a 10-vCPU box does under three pages a minute and nothing else while it does them. Concurrency makes it worse: four pages at once cost 238 CPU-seconds each instead of 190.
Where a GPU changes something
A GPU is recommended, never required, and only for the image path and for a self-hosted model. There are three GPU paths and they are independent of each other.
| Path | What it is | Notes |
|---|---|---|
| The in-stack OCR overlay | Rebuilds the default OCR service to run on CUDA | Measured 3 to 8 times faster per image than the CPU build. Never used on an appliance, which stays CPU-only. |
| A separate OCR host | The same image and API on a GPU host of its own | Reached through OCR_GPU_SERVICE_URL with a bearer token, and falls back to the in-stack CPU service on error. |
| The analyst model engine | The local model the LLM Risk Analyst reasons with | 20 GB of VRAM minimum, 48 GB recommended. A host of its own in the supported topology. |
Warning
Earlier CID222 documents describe a second, transformer-based GPU OCR engine behind a gpu
compose profile. Both were removed, and no compose file in this release declares
a gpu profile. PaddleOCR on CUDA is the only GPU OCR path.
Nothing else in the stack uses a GPU. PII, toxicity, jailbreak and injection detection run on ONNX on the CPU, hallucination detection is a CPU-only build, and adding a card does not change their latency.
How to compute your own total
Start from the measured base
Take 10.85 GiB for the default profile at the compose defaults, or about 6.5 GiB if you have
pinned UVICORN_WORKERS to 1 for each detector.
This is an idle figure, and it is the only line in the calculation that is not a margin.
Add the profiles you start
Add 1.5 GiB for llm-analyst. For inline-proxy, userid-agent and routing, no measured
figure exists — allow their declared limit where they have one, and measure them on your own
traffic where they do not.
You now have a resting total for your service set.
Add the load head-room
Add 1.2 GiB for the PII detector's arena growth, 1.7 GiB for the first OCR language, and 0.8 GiB for each further language you serve.
Nothing in this step is returned when traffic stops.
Add the day's drift
Add a further 20 %. The measured stack grew from 12.53 to 15.25 GiB across one working day of mixed load, which is 22 %.
The result is the anonymous memory the host must back.
Leave room for page cache and round up
The measured stack also held about 5 GiB of page cache. It is reclaimable, so it is not part of the total, but a host with no slack for it reclaims constantly.
Round up to the next hypervisor memory step and never go below 32 GB, whatever the arithmetic says.
To measure a service yourself, read its own cgroup rather than docker stats:
Read it before and after a realistic burst. The difference is what that traffic costs, and for the ONNX detectors it is the difference that matters, not the starting value.
Notes
- Disk grows with retention, not with traffic peaks. Detections, event logs and compliance evidence are what accumulate; container images and model caches are a fixed cost paid once. The shipped retention windows and the fact that the sweep does not delete by default are in requirements and sizing.
- Swap is a margin for brief model spikes, not a substitute for memory. 8 GB is enough on a correctly sized host, and no amount of it rescues a host below the 32 GB floor.
- The shipped appliance image is pre-sized at the supported minimum — 32768 MB and 8 vCPU — so it powers on without operator arithmetic. Raise it before you put load on it.
- Two older figures circulate and neither is generated. The repository guide and the OVA build notes say about 42 GB of declared limits across 11 unlimited services, and about 30 GiB measured. The generated figure is 37.63 GB across 19 services with 6 unlimited; the 30 GiB came from a 2026-07 staging rehearsal that recorded all services running with head-room on a 62 GiB box, not a per-container measurement. Use the generated and measured figures on this page.
Related
- Requirements and sizing — the host to provision, per deployment mode.
- Optional services and what each one costs — the requirement line per added component.
- Services, ports and profiles — the generated per-service table.
- Compose profiles and CLI — how a profile is started.
Last updated on