Skip to content

Configuration Reference

The Controller and Daemon each read one YAML file at startup. Every key maps to a Java record with a compact constructor that applies the default below — omit a key and you get that default. This page is the canonical reference. The records under java/cloud-controller/.../controller/config/ and java/cloud-daemon/.../daemon/config/ are the ground truth.

What you’ll learn

  • Every Controller and Daemon configuration key, its type, default, and effect
  • Which keys ConfigValidator rejects, and the exact error each emits
  • What is required in the production profile
  • Which environment variables exist (fewer than you’d expect) and what they do

How config is loaded

  • The Controller reads config/controller.yml, relative to its working directory (the install root). The Daemon reads config/daemon.yml.
  • If the file is absent on first boot, the process copies the bundled defaults/controller.yml or defaults/daemon.yml from the classpath, then loads it.
  • Parsing uses Jackson with FAIL_ON_UNKNOWN_PROPERTIES disabled. Unknown keys are ignored, not rejected. A removed key (for example dashboard.path) left in an old file is silently dropped.
  • There is no signal-based hot reload. Changing the file requires a restart.
  • Defaults are applied per record. For most numeric fields the rule is “value <= 0 resolves to the default”, so 0 does not mean “zero” — it means “use the default”. The exceptions are noted inline.

The Controller writes back to controller.yml in two cases: it generates and persists a uuid on first boot if none is set, and it generates and persists a security.jwtSecret if none is set.

controller.yml

uuid

FieldTypeDefaultNotes
uuidstringrandom UUIDStable Controller identifier. Generated on first boot and written back into controller.yml. In a cluster, every Controller must have its own.

http

FieldTypeDefaultNotes
hoststring0.0.0.0REST/SSE/dashboard bind address.
portint8080REST + SSE + bundled dashboard. Must differ from grpc.port.
cors.allowedOriginslist<string>http://localhost:3000, :3001, :3002, :3003Browser origins allowed. Each entry must start with http:// or https:// or the Controller refuses to boot. Add the dashboard origin when it is served from another domain.

grpc

FieldTypeDefaultNotes
hoststring0.0.0.0Bind address for Daemon connections.
portint9090gRPC port; mTLS terminated by the Controller. Must differ from http.port.

network

FieldTypeDefaultNotes
allowedSubnetslist<string>0.0.0.0/0, ::/0CIDR allowlist for HTTP clients. Lock this down for production. An empty list rejects all clients.

database (MongoDB)

FieldTypeDefaultNotes
uristringmongodb://localhost:27017Required — boot fails if blank.
databasestringprexorcloudDatabase name.

PrexorCloud does not embed Mongo. Replica-set URIs are supported and recommended for HA: mongodb://h1,h2,h3/prexorcloud?replicaSet=rs0&w=majority.

redis

FieldTypeDefaultNotes
uristringredis://localhost:6379Redis or Valkey (same protocol). rediss://... enables TLS.

The whole redis block is nullable. Omit it entirely and the Controller runs with in-process coordination fallbacks (single-controller development only). Set it for multi-controller or load-testing. The production profile refuses to start without it.

When present but with a blank uri, boot fails with redis.uri must not be blank when redis is configured.

runtime

FieldTypeDefaultNotes
profilestringdevelopmentOne of development, production (case-insensitive, trimmed). Any other value is rejected. Production swaps in Redis-backed coordination for leases, JWT revocation, login lockout, SSE replay, and rate limits, and tightens signing defaults.

logging

FieldTypeDefaultNotes
levelstringINFOSLF4J level: ERROR, WARN, INFO, DEBUG, TRACE.
formatenumHUMANHUMAN for single-line text, JSON for line-delimited structured JSON. An unrecognized value falls back to HUMAN.

scheduler

FieldTypeDefaultNotes
evaluationIntervalSecondsint15Tick frequency for placement and scaling decisions. Must be >= 1.
scalingCooldownSecondsint60Default per-group cooldown after a scale event. Per-group config can override.
nodeTimeoutSecondsint90Time without heartbeat before a node is marked offline.
auditRetentionDaysint90TTL on the audit_log collection, keyed off createdAt.

heartbeat

FieldTypeDefaultNotes
intervalMslong30000Daemon → Controller heartbeat cadence. Must be >= 1000.
missedThresholdint3Consecutive misses before a node session is stale. Must be >= 1.

security

FieldTypeDefaultNotes
jwtSecretstringempty → auto-generatedWhen blank, a secret is generated on first boot and written back into controller.yml. Set a managed value in production.
jwtExpirationMinutesint144024 hours. Must be >= 1 and <= 43200 (30 days).
initialAdminPasswordstringempty → auto-generatedFirst-boot bootstrap only. See note below.
jwtPreviousSecretslist<string>[]Old secrets accepted during rotation until their tokens expire. Invalid entries are logged and skipped.
rateLimiting.perIpPerMinuteint100Per-IP REST limit. Must be >= 1.
rateLimiting.perUserPerMinuteint300Per-user REST limit. Must be >= 1.
rateLimiting.failOpenOnRedisErrorboolfalseAllow traffic when Redis is unreachable. Default closed.
lockout.enabledbooltrueAccount lockout on failed logins.
lockout.maxAttemptsint5Failed logins before a lock. Must be >= 1.
lockout.windowSecondsint900Sliding failure window. Must be >= 1.
lockout.lockoutSecondsint900Lock duration after threshold. Must be >= 1.
passwordReset.enabledboolfalseWhen false, /api/v1/auth/password-reset/* returns 404 and no manager is wired.
passwordReset.tokenTtlMinutesint30Single-use token lifetime.
passwordReset.resetUrlBasestringemptyDashboard base URL. The mailer appends /auth/reset-password?token=.... Blank still mints tokens; the email falls back to a relative path.
passwordReset.smtp.hoststringemptyBlank means SMTP is disabled — LogMailer writes the reset link to the Controller log instead of sending mail.
passwordReset.smtp.portint587
passwordReset.smtp.startTlsboolfalse*
passwordReset.smtp.implicitTlsboolfalse
passwordReset.smtp.usernamestringempty
passwordReset.smtp.passwordstringempty
passwordReset.smtp.fromstringempty
passwordReset.smtp.connectTimeoutMsint10000
passwordReset.smtp.readTimeoutMsint10000

* startTls is a primitive boolean. Its no-arg record default is true, but because absent YAML keys deserialize to the primitive zero value, an omitted startTls resolves to false. Set it explicitly to true when your relay requires STARTTLS.

The initial admin password: when no users exist on first boot, the Controller creates admin. If initialAdminPassword is blank it generates a random one. Either way it writes the plaintext to config/.initial-admin-password (mode 0600) and logs the file location, not the password. Change the password and delete the file after first login.

In the production profile, lockout and password-reset state live in Redis and are shared across Controllers.

crashes

FieldTypeDefaultNotes
ringBufferSizeint500Per-process in-memory ring of recent crashes for the dashboard.
crashLoopThresholdint3Crashes within the window before a group is paused.
crashLoopWindowSecondsint300Sliding crash-loop window.

metrics

FieldTypeDefaultNotes
enabledbooltrueExposes /metrics (Prometheus exposition). Gate access with a reverse-proxy ACL.
retentionHoursint168In-process retention for the dashboard’s mini-graphs. Independent of your Prometheus retention.
collectionIntervalSecondsint30Internal sampling cadence for derived gauges.

modules

FieldTypeDefaultNotes
directorystringmodulesInstalled module bundles.
dataDirectorystringmodules/dataPer-module on-disk storage root.
hotReloadNot a config field. Module hot-reload is not toggled here.
registrieslist<string>[]Registry index URLs trusted for install-from-registry. Empty means no registry browsing.
quotas.<moduleId>map{}Per-module soft quotas. See below.
signing.requiredboolprofile-defaultnull resolves to true in production, false in development.
signing.trustRootstringemptyPEM bundle. PUBLIC KEY blocks for KEYED; PUBLIC KEY and/or CERTIFICATE blocks for COSIGN_BUNDLE.
signing.modeenumKEYEDKEYED for a .sig sidecar; COSIGN_BUNDLE for a .cosign.bundle from cosign sign-blob --bundle.
signing.allowUnsignedDevelopmentbooltrueLets the development profile install unsigned bundles even when required=true.
signing.rekor.policyenumDISABLEDREQUIRE_SET enforces offline Rekor SET verification — the bundle must carry a SignedEntryTimestamp.
signing.rekor.publicKeystringemptyPEM for the Rekor public key. Required when policy=REQUIRE_SET.

Each quotas.<moduleId> entry holds maxCpuMillisPerMinute (long), maxAllocatedMbPerMinute (long), and maxThreads (int). Any non-positive value means unlimited for that dimension, so an empty quota block enforces nothing. Breaches are advisory: a WARN log and the prexorcloud.module.quota.exceeded metric, no throttling.

See Cosign Pipeline for the verification flow.

maintenance

FieldTypeDefaultNotes
enabledboolfalseGlobal maintenance mode. Overrides per-group settings.
messagestringThe network is currently under maintenance.Surfaced to dashboards and proxy plugins.

Per-group bypass is configured on the group (maintenanceBypass), not here. There is no global bypass list.

dashboard

FieldTypeDefaultNotes
enabledbooltrueServes the bundled dashboard from a fixed dashboard/ directory under the install root. Set false when running the dashboard separately so the Controller does not serve a stale bundle.

dashboard.path is no longer configurable; a leftover path: entry is ignored.

backup

FieldTypeDefaultNotes
directorystringbackupsBackup root, relative to the install root.
retentionCountint10Manifests kept by the catalog. Older ones are pruned by prexorctl backup prune.

share

Controls the --share workflow that uploads redacted diagnostics to a pastebin. Sharing is operator-invoked; redaction is unconditional.

FieldTypeDefaultNotes
enabledboolfalseOpt in before any artifact leaves the cluster.
pasteUrlstringhttps://pste.devPastebin endpoint.
pasteTokenstringemptyOptional auth token for the pastebin.
defaultExpirystring1dPaste expiry.
defaultPrivateboolfalse*
e2eboolfalseEnd-to-end encryption flag.

* The no-arg default for defaultPrivate is true, but an omitted YAML key resolves the primitive boolean to false. Set it explicitly.

cluster

FieldTypeDefaultNotes
idstringnullPins this Controller to a Mongo cluster. Cross-checked against cluster_meta at boot; mismatch refuses to start.
joinedFromstringnullInformational, written by the join wizard.
joinedAtstringnullInformational.

raft

Node-local Raft transport for the cluster control plane. Cluster-wide tuning (election timeout, snapshot retention) lives in the state machine, not here.

FieldTypeDefaultNotes
hoststring0.0.0.0Raft bind address.
portint9190Raft gRPC port.
dataDirstringdata/raftOn-disk Raft log/snapshot directory.
joinAddrslist<string>[]gRPC endpoints of existing members, used at boot for discovery. Empty means first member of a new cluster or a restarting member; the on-disk Raft data dir disambiguates.

telemetry

Distributed tracing (OpenTelemetry). Disabled by default with zero runtime cost — a no-op tracer is installed and the SDK never starts.

FieldTypeDefaultNotes
enabledboolfalseTurn tracing on.
otlpEndpointstringhttp://localhost:4317OTLP target (Jaeger, Tempo, Honeycomb, Datadog).
serviceNamestringprexorcloud-controllerSpan service name.
samplerRatiodouble1.0Parent-based head-sampler ratio, clamped to [0,1].
traceUiTemplatestringemptyDeep-link template with a literal {traceId} placeholder, e.g. http://localhost:16686/trace/{traceId}. Empty means no “view trace” link.

networks

A list of seed NetworkComposition records applied on first boot only. Later edits via POST /api/v1/networks win — a seed whose name already exists is not re-applied. See Network Composition.

events

A list of seed EventChoreography scaling overlays. Same “first-boot only” semantics as networks.

daemon.yml

Lives at config/daemon.yml under the Daemon install root.

nodeId

FieldTypeDefaultNotes
nodeIdstringnode-1Cluster-unique. Two Daemons on the same nodeId is undefined behavior.

advertiseAddress

FieldTypeDefaultNotes
advertiseAddressstringemptyAddress the Controller and other nodes reach this Daemon at. Empty means auto-detect.

controller

FieldTypeDefaultNotes
hoststring127.0.0.1Controller hostname/IP reachable from this Daemon.
grpcPortint9090Controller gRPC port.

health

FieldTypeDefaultNotes
enabledbooltrueLocal readiness/liveness HTTP endpoint.
bindAddressstring127.0.0.1Bind locally; expose to systemd/k8s only.
portint9091

security

FieldTypeDefaultNotes
certificateDirstringconfig/securitymTLS material. Bootstrap writes the Daemon cert, key, and CA cert here.
joinTokenstringemptyOne-time bootstrap credential. The first successful registration clears it. When no cert exists and joinToken is blank, the Daemon refuses to start.

instances

FieldTypeDefaultNotes
directorystringinstancesPer-instance working directories.
shutdownTimeoutSecondsint30Graceful-stop budget before forced kill.
killTimeoutSecondsint10Time after SIGTERM before SIGKILL.
logRingBufferLinesint500Console buffer per instance.
maxConsoleOutputLinesPerSecondint200Per-instance console flood cap.

resources

FieldTypeDefaultNotes
maxMemoryMblong00 auto-detects 80% of total physical memory. Set explicitly to cap how much the Daemon will admit.

logging

Same shape as the Controller: level (default INFO), format (default HUMAN).

reconnect

FieldTypeDefaultNotes
initialDelayMslong1000First retry delay after gRPC stream loss.
maxDelayMslong60000Cap for exponential backoff.
multiplierdouble2.0Backoff multiplier.

modules.signing

Daemon-side platform-module signing policy. Defaults match a development cluster.

FieldTypeDefaultNotes
requiredboolfalseProduction should set true.
modeenumCOSIGN_BUNDLEKEYED for a Base64 .sig; COSIGN_BUNDLE for a .cosign.bundle. Note this default differs from the Controller’s KEYED.
trustRootstringemptyPEM bundle matching mode.

telemetry

Mirrors the Controller. Disabled by default.

FieldTypeDefaultNotes
enabledboolfalse
otlpEndpointstringhttp://localhost:4317
serviceNamestringprexorcloud-daemon
samplerRatiodouble1.0Clamped to [0,1]. Keep aligned with the Controller so a sampled trace stays sampled across the hop.

labels

Free-form key-value pairs. Groups target nodes via placement.nodeSelector.

labels:
region: "eu-west"
tier: "dedicated"
hardware: "ryzen-9950x"

Environment variables

PrexorCloud does not read PREXORCLOUD_* env vars into the config records. The loader parses YAML only — there is no env-var override layer for http.host, database.uri, and so on. Put those values in controller.yml / daemon.yml. The env vars that do exist fall into three groups.

Compose stack (substituted by Docker Compose, not by the app)

These are read by deploy/compose/compose.yml to template the stack, not by the Controller or Daemon:

VariableDefaultPurpose
PREXORCLOUD_CONTROLLER_IMAGE…/prexorcloud-controller:latestController image pin.
PREXORCLOUD_DAEMON_IMAGE…/prexorcloud-daemon:latestDaemon image pin.
PREXORCLOUD_DASHBOARD_IMAGE…/prexorcloud-dashboard:latestDashboard image pin.
PREXORCLOUD_CONTROLLER_HEAP1gController -Xmx.
PREXORCLOUD_DAEMON_HEAP512mDaemon -Xmx.
PREXORCLOUD_HTTP_PORT8080Host port mapped to the Controller HTTP port.
PREXORCLOUD_GRPC_PORT9090Host port mapped to the Controller gRPC port.
PREXORCLOUD_DASHBOARD_PORT3000Host port mapped to the dashboard.

The compose stack mounts controller.yml and daemon.yml read-only into the containers. Secrets (security.jwtSecret, security.initialAdminPassword, database.uri, redis.uri) belong in those files; .env.example lists them only as a pointer.

Per-instance (injected by the Daemon into each MC server process)

Read by the bundled plugin inside each instance JVM:

VariablePurpose
CLOUD_INSTANCE_IDInstance identifier.
CLOUD_GROUPGroup name.
CLOUD_PORTAssigned port.
CLOUD_NODE_IDHost node identifier.
CLOUD_CONTROLLER_URLController REST API URL (set only when non-blank).
CLOUD_PLUGIN_TOKENShort-lived plugin auth token (set only when present).
CLOUD_CPU_RESERVATIONCPU reservation hint.
CLOUD_DISK_RESERVATION_MBDisk reservation hint, MB.

CLI (prexorctl)

VariablePurpose
PREXOR_CONTROLLERDefault --controller URL.
PREXOR_TOKENDefault auth token.
PREXOR_CONTEXTDefault context name.
PREXOR_OUTPUT=jsonJSON output across all commands.
PREXOR_NO_BROWSERSkip the browser launch in setup.
NO_COLORDisable colored output.

Validation

ConfigValidator.validate(...) runs at Controller startup. It collects every error before failing, so one pass fixes all of them, then throws IllegalStateException (the process exits non-zero before binding ports). The checks:

  • runtime.profile must be development or production.
  • http.port and grpc.port must each be 1..65535.
  • http.port and grpc.port must differ.
  • security.jwtExpirationMinutes must be >= 1 and not exceed 43200.
  • security.rateLimiting.perIpPerMinute and perUserPerMinute must be >= 1.
  • security.lockout.maxAttempts, windowSeconds, lockoutSeconds must be >= 1.
  • database.uri must not be blank.
  • redis.uri must not be blank when the redis block is present.
  • redis must be set when runtime.profile=production.
  • When module signing is required (explicit true, or production default), modules.signing.trustRoot must be set.
  • modules.signing.rekor.policy=REQUIRE_SET requires modules.signing.mode=COSIGN_BUNDLE and a non-blank modules.signing.rekor.publicKey.
  • scheduler.evaluationIntervalSeconds must be >= 1.
  • heartbeat.intervalMs must be >= 1000; heartbeat.missedThreshold >= 1.
  • Every cors.allowedOrigins entry must start with http:// or https://.

Many of these are also enforced by the record defaults (a <= 0 numeric resolves to the default before validation sees it), so in practice the validator catches profile/cross-field mistakes — port collisions, a production profile without Redis, a Rekor policy without a key.

Roles (roles.yml)

The Controller seeds three roles on first boot:

  • ADMIN["*"], all permissions.
  • OPERATOR — node/group/instance/template/module/catalog/audit/metrics view + mutate.
  • VIEWER — read-only across the same surfaces.

Custom roles are editable via prexorctl role or the roles Mongo collection.

Next up