Skip to main content

Requirements & sizing

Host requirements and capacity planning: how many VMs, how much CPU, memory and disk for a given certificate count and discovery footprint, and which tunables actually move the numbers.

Nothing here is enforced

The installer checks the OS, the ports and the packages — it does not check CPU, memory or disk, and it will happily install onto an undersized host. The floors below are operational guidance, with the one hard exception called out under Minimum host: MongoDB does not run well under 4 GiB.

Minimum host

Per host, for the standalone install (application + nginx + a local MongoDB):

MinimumNotes
Architecturex86-64 or arm64glibc only. Alpine via the Docker path.
vCPU2The application is network-bound, not compute-bound — see Where the work goes.
Memory4 GiBMongoDB's WiredTiger cache alone takes roughly half of (RAM − 1 GiB). Below 4 GiB the database is the first thing to suffer.
Disk20 GiB SSDMostly MongoDB. See Storage for the arithmetic.
Free ports443 (or --port), 18181 loopback, 27017 when Mongo is localStandalone lists the layout.

An application-only host — one that joins an existing cluster with --mongo=external, or the fourth and later nodes of a --nodes install — has no database to feed and runs comfortably in 2 vCPU / 2 GiB.

Distros, ports and the outbound destinations the product dials are covered by Standalone deployment and Outbound network access; this page is only about capacity.

Where the work goes

Sizing follows the concurrency model, not the certificate count directly. Every CertAutoPilot process started as --mode=all — which is what both the standalone installer and the Helm chart run — contains three things in one binary:

  • The API, which does almost nothing expensive. Requests are small Mongo reads and writes.
  • Two worker lanes. A main worker (ACME, AD CS, notifications, discovery) and a dedicated distribution worker, each of which claims up to worker.max_concurrency jobs at a time (default 4). One host therefore runs up to 8 jobs concurrently, no matter how many certificates exist.
  • The scheduler, leader-elected across the cluster through a MongoDB lock, so exactly one host sweeps at a time regardless of cluster size. Default sweep interval 1 hour.

What that means in practice:

ActivityBound byCost shape
Issuance / renewalCertificate authority round-trips and DNS propagationWall-clock, not CPU. The phased DNS-01 flow deliberately releases its worker between polls, so a slow zone costs no lane.
Key generationKey typeECDSA P-256 (the default) is microseconds. RSA-4096 is the only routinely CPU-visible operation, and it runs once per renewal.
DistributionTarget device response timesOne job holds one distribution lane for up to distribution_execution_timeout_minutes (default 30). Per-device locks serialise anything that would conflict.
DiscoveryConcurrent TLS handshakesOne scan job holds one lane. Its connections are capped process-wide by worker.max_concurrent_discovery (default 50), so scans share a budget instead of multiplying.
DatabaseWorking-set sizeSee Storage. At a few thousand certificates the whole working set fits in cache on any of the profiles below.

The practical consequence: certificates buy you database, discovery buys you CPU. They are independent axes — size them separately.

Topology

TopologyHostsMongoDBSurvivesUse when
Single VM1Local, standaloneNothing — host loss is an outageEvaluation, lab, one team, non-critical renewals
Multi-VM, external database2+Managed / existing replica setHost loss, if the database is replicatedYou already run MongoDB and want the app layer redundant
3-VM cluster (--nodes)33-member replica set caprs, installed for youLoss of any single host, including the database primaryThe default recommendation for production
3-VM cluster + app nodes3 + NReplica set on the first three onlySame, with more API/worker capacityDistribution or discovery throughput outgrows three hosts
KubernetesreplicaCount podsYours to supplyPod and node lossYou already run Kubernetes — see Helm chart

Why three, not two

A two-host cluster with local databases is refused by the installer, and deliberately: MongoDB elects a primary by majority, and two members cannot form one when either dies. Three members tolerate the loss of one. The same number falls out of the application layer — the scheduler lock and every job claim are single-document atomic operations, so extra hosts are always safe, but below three there is no majority to elect anything.

Hosts four and upward run the application without a local mongod. They add API and worker capacity — eight more concurrent job lanes each — and nothing else. Add them when lanes are the constraint, not when the database is.

Full mechanics, firewall rules and failover behaviour: High availability.

Sizing profiles

Per host. "Certificates" means certificates CertAutoPilot manages — the license cap is a separate, independent limit.

ProfileCertificatesDiscoveryTopologyvCPUMemoryDisk
Evaluation≤ 50Occasional /241 VM24 GiB20 GiB
Small production≤ 500A few /24s daily1 VM, or 3 for availability48 GiB50 GiB
Standard production≤ 2,500/22–/20 daily, CT feeds on3 VMs48 GiB100 GiB
Large / discovery-heavy≤ 2,500/16 sweeps, deep SNI, cipher enumeration3 VMs + app nodes816 GiB200 GiB

Two notes on reading that table:

  • The certificate column moves disk, not CPU. Going from 500 to 2,500 certificates roughly quintuples the database and changes nothing else, because renewals are paced by the scheduler and bounded by the worker lanes.
  • The discovery column moves CPU, not memory. A /16 sweep with cipher enumeration is the only workload in the product that can saturate a small host, and it does so through concurrent TLS handshakes — see Discovery sizing.

2,500 certificates is the validated capacity figure for the product; the profiles stop there because that is what has been measured, not because a larger number is refused.

Discovery sizing

Discovery is the one workload worth sizing on its own, because its cost is set by settings rather than by inventory. It is also the most tightly bounded: every concurrency knob is capped process-wide, so no combination of sources can open an unbounded number of connections.

KnobWhereDefaultCeilingWhat it bounds
worker.max_concurrent_discoveryconfig.yaml50Process-wide concurrent scan dials, across every source and every lane on the host
concurrency_limitPer source20100Scan worker goroutines for that source. Actual network I/O still passes through the process-wide cap above.
max_targetsPer source51265,536Addresses × ports one run may touch. The ceiling is one /16 scanned on a single port.
timeout_secondsPer source530Dial timeout. A dead address costs this long; a tarpit cannot hold a slot longer.
Crawl deadlinePer source1,800 sHard wall-clock budget per run, kept under the worker's 30-minute stuck-job threshold
Cipher-enumeration budgetFixed15 minWall-clock cap on one source's whole cipher pass

So the realistic ceiling on one host is ~50 concurrent scan handshakes plus up to 64 concurrent cipher-enumeration handshakes — the cipher pass runs after the crawl drains, under its own process-wide limit of 64, and never holds a scan slot. The systemd unit ships LimitNOFILE=65536, so file descriptors are never the constraint; CPU for those handshakes is, which is why the discovery-heavy profile doubles the cores rather than the memory.

Memory does not scale with the size of the scan. Results are persisted as they arrive rather than buffered, so a run holds roughly one small entry per responding host:port — a /16 sweep that finds 50,000 live endpoints costs tens of megabytes, not gigabytes.

Two optional per-source features add handshakes rather than concurrency: TLS version probing costs one extra 3-second handshake per endpoint, and cipher enumeration one connection per suite tested. Both lengthen a run; neither raises the ceilings above.

Storage

MongoDB is the only thing that grows. The figures below are measured avgObjSize values from a running instance — use them as coefficients, not as promises.

CollectionBytes per documentGrows withBounded by
certificate_artifacts~10,500certificates × retained versionscert_history_max_versions (default 10)
certificate_private_keys~2,600certificates × distinct keysSame retention, pruned only when no retained artifact uses the key
certificates~1,000certificates
discovered_endpoints~550live endpoints found180-day TTL after an endpoint disappears
discovered_certificates~970distinct certificates seen
jobs~590renewals × jobs per renewal30-day TTL on terminal jobs
job_logs~165log lines30-day TTL
audit_logs~830operator and API actions30-day TTL
cert_eventssmalllifecycle events365-day TTL
notification_historysmallnotifications sent90-day TTL
scheduler_sweep_runs~290sweeps7-day TTL

Index overhead measured on the same instance is roughly 55–60% of data size. Budget total MongoDB footprint at about 1.6× the data figure.

Worked example

2,500 certificates at default retention:

artifacts 2,500 × 10.5 KB × 10 versions ≈ 260 MB
private keys 2,500 × 2.6 KB × ~1–10 keys ≈ 7–65 MB
certificates 2,500 × 1.0 KB ≈ 2.5 MB
----------
data ≈ 270–330 MB
+ indexes (×1.6) ≈ 430–530 MB

Add the churn collections, all of which are TTL-bounded rather than cumulative: at a 47-day certificate lifetime and a 30-day renewal threshold, 2,500 certificates renew roughly 150 times a day, producing on the order of 1,000 job documents and 6,000 log lines daily — under 100 MB inside the 30-day window, indexes included. Discovery adds ~550 bytes per live endpoint.

A 2,500-certificate deployment is therefore well under 2 GiB of database. Disk goes to the operating system, the journal, and — if you enabled --enable-backup — seven nightly dumps, each roughly the size of the compressed data. The 50–200 GiB in the profile table is headroom for those and for growth, not a prediction of the data size.

Lowering cert_history_max_versions is the single most effective storage lever: it is a direct multiplier on the largest collection. Raising it is how you keep more rollback targets available, which is the trade.

Tunables that matter

SettingWhereDefaultRangeRaise it when
worker.max_concurrencyConfig file / CERTAUTOPILOT_WORKER_MAX_CONCURRENCY4≥ 1Jobs queue while CPU sits idle. Applies to both lanes, so the real ceiling is 2× this per host.
distribution_fanout_thresholdSettings → General0 (off)0–500A distribution has more targets than one 30-minute job can finish. Splits it into batch children.
distribution_fanout_batch_sizeSettings → General500–200Batches finish too fast (raise) or time out (lower).
distribution_ssh_max_concurrencySettings → Generalmodule default 100–50SSH fan-outs are the bottleneck. Per-module concurrency is clamped to 1–50 everywhere.
distribution_execution_timeout_minutesSettings → General305–120Slow appliances abort mid-run.
renewal_check_interval_minutesSettings → General60Rarely. The sweep is cheap; the certificate authority is the slow part.
cert_history_max_versionsSettings → General10−1 (unlimited) – 100You need deeper rollback history, and you have the disk.
worker.max_concurrent_discoveryconfig.yaml only50≥ 1Scans are slow while the host has spare CPU. This is the process-wide handshake budget — raising a source's own concurrency_limit does nothing until this moves. The key is absent from the standalone template, so the CERTAUTOPILOT_* env var is silently ignored until you add it by hand (configuration reference).
concurrency_limit / max_targetsPer discovery source20 / 512≤ 100 / ≤ 65,536Scans take too long — but read Discovery sizing first.

Kubernetes

The chart's shipped requests and limits — 100m/128Mi requested, 500m/256Mi limited — are sized for an API-shaped pod. They are not enough for a pod that wins a large discovery job or a wide distribution fan-out: the same process runs the workers. If you scan anything larger than a /24, raise the limits to at least 1 CPU / 1 GiB before you tune anything else.

The chart runs --mode=all in every replica with a leader-elected scheduler, so replicaCount scales lanes exactly like adding VM nodes does — eight concurrent jobs per replica at the default concurrency. See Helm chart.

When to grow

Signals, in the order they usually appear:

  1. Jobs sit in pending while CPU is idle — lane starvation. Raise worker.max_concurrency, then add application nodes.
  2. Distribution jobs hit the 30-minute timeout — turn on fan-out; the threshold defaults to off.
  3. Discovery runs report partial coverage — the crawl budget expired. Lower max_targets, split the source, or add cores.
  4. MongoDB read latency climbs — the working set no longer fits the WiredTiger cache. Add memory before adding hosts; a replica set does not make reads faster when they default to the primary.
  5. Disk growth tracks renewals — check cert_history_max_versions before buying disk.