Skip to main content
CID222 Docs

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.

ReadingWhat it isUse it for
Anonymous memoryPages that must be backed by RAM or swapSizing. This is the number to buy memory against.
What docker stats printsAnonymous memory plus active page cacheNothing, on its own. It over-reports, worst right after a model load.
PeakA high-water mark of the above, which never decreases while the container livesOnly 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.

ContainerDeclaredAnonymous, idledocker statsPeak
attack-guard4 GB2.72 GiB2.803 GiB4.00 GiB
hap-guard-v26 GB2.70 GiB2.769 GiB4.50 GiB
ml-detector6 GB2.45 GiB2.504 GiB3.94 GiB
doc-classifier2 GB920 MiB1.632 GiB1.75 GiB
risk-analyst3 GB882 MiB1.458 GiB2.01 GiB
prompt-compressor3 GB633 MiB796.8 MiB1.02 GiB
mcp-server1 GB594 MiB669.7 MiB892 MiB
language-detector2 GB480 MiB519.8 MiB537 MiB
hallucination-guard4 GB474 MiB654.1 MiB828 MiB
pgadmin—241 MiB312.4 MiB369 MiB
nestjs-core—110 MiB207.5 MiB320 MiB
deepteam2 GB103 MiB136.9 MiB180 MiB
ocr-service4 GB101 MiB193.3 MiB241 MiB
document-parser2 GB77 MiB124.7 MiB157 MiB
report-renderer512 MB62 MiB91.0 MiB115 MiB
redactor2 GB59 MiB95.9 MiB118 MiB
postgres—12 MiB61.4 MiB104 MiB
reconfigure-agent—9 MiB20.1 MiB31 MiB
frontend—8 MiB19.2 MiB30 MiB
cid-proxy128 MB4 MiB11.3 MiB18 MiB
redis—4 MiB14.3 MiB61 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

ProfileServicesDeclared memoryMeasured, idle
Default — no profile1937.63 GB10.85 GiB anonymous
llm-analyst2+4 GB+1.5 GiB anonymous
routing1+1 GBNot measured
inline-proxy1None declaredNot measured
userid-agent1None declaredNot measured
llm1None declaredA GPU host of its own
tools2None declared241 MiB for the database console
ldap-test, mailtest, llm-build3None declaredDevelopment 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

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

PathWhat it isNotes
The in-stack OCR overlayRebuilds the default OCR service to run on CUDAMeasured 3 to 8 times faster per image than the CPU build. Never used on an appliance, which stays CPU-only.
A separate OCR hostThe same image and API on a GPU host of its ownReached through OCR_GPU_SERVICE_URL with a bearer token, and falls back to the in-stack CPU service on error.
The analyst model engineThe local model the LLM Risk Analyst reasons with20 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:

docker compose exec ml-detector \
  awk '/^(anon|file|active_file) /' /sys/fs/cgroup/memory.stat

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.

Last updated on

On this page