Helm
The complete Helm chart reference for Scrydon — a condensed install plus every value the chart exposes, with what each does and why and how to configure it.
This is the configuration reference for the Scrydon Helm chart. It documents a condensed install and the operationally significant value groups — what each key does, why you'd change it, and how. If you just want the fastest path to a running cluster, start from your Location page (On-Premise, Azure (AKS), Air-Gapped) and come back here for the options.
For an air-gapped cluster (no outbound internet), use Air-Gapped Deployment instead — the same chart ships inside a Zarf bundle.
How configuration works
Three layers, lowest to highest precedence:
- Chart defaults — the chart ships sane production defaults for everything. The authoritative list is the chart's own
values.yaml(see Inspect the chart locally). - Your
values.customer.yaml— the file you pass with-f. Override only what you need; everything else inherits the default. --setflags — highest precedence, useful for resolving secrets at install time from a secret manager.
helm install scrydon oci://scrydonops.azurecr.io/scrydon/charts/scrydon \
--version <version> -n scrydon-platform \
-f values.customer.yaml \
--set apiTable.secrets.STARROCKS_PASSWORD="$(vault kv get -field=password kv/scrydon/starrocks)"Secrets live in your values file.
values.customer.yamlandhelm get values <release>both echo every user-supplied value. Keep the file out of source control, or manage it via Sealed Secrets / SOPS / External Secrets Operator, and restrict cluster RBAC on the release Secret.
Inspect the chart locally
The chart is an OCI artifact in scrydonops.azurecr.io — there is no public Git mirror. Pull it to read every template and the full default values.yaml:
helm pull oci://scrydonops.azurecr.io/scrydon/charts/scrydon \
--version <version> --untar
less scrydon/values.yaml # the authoritative default for every key belowValidate your configuration
The chart ships a values schema (values.schema.json). It rejects unknown or
mistyped keys at the top level and in high-risk strict groups such as
routing, ingress, license, namespaces, Pod Security, and NetworkPolicy.
Some application blocks — including nested runtimePlane keys in current
charts — remain permissive, so schema validation is necessary but not
sufficient. A typo like routing.mdoe fails immediately:
Error: values don't meet the specifications of the schema(s) in the following chart(s):
scrydon:
- routing: Additional property mdoe is not allowedBefore an install or upgrade, run a preflight against your values and your cluster version:
# 1. Schema + lint — catches typo'd keys and structural mistakes
helm lint ./scrydon -f your-values.yaml
# 2. Render and validate against your Kubernetes version
# (kubeconform: https://github.com/yannh/kubeconform)
helm template scrydon ./scrydon -f your-values.yaml \
| kubeconform --strict --ignore-missing-schemas \
--kubernetes-version "$(kubectl version -o json | jq -r '.serverVersion.gitVersion' | tr -d v)"
# 3. Dry-run the install against the live cluster (server-side validation)
helm install scrydon ./scrydon -f your-values.yaml --dry-run=serverThe chart requires Kubernetes 1.28 or later (kubeVersion: ">=1.28.0-0") — helm install refuses older clusters with a clear error
rather than failing mid-deploy.
Quick install
The minimum to a running cluster. Each step is expanded with environment-specific notes on the Location pages.
# 1. Log in to the registry (username = ACR token name from your account team)
helm registry login scrydonops.azurecr.io --username <acr-token-name>
# 2. Create the release namespace + an image-pull secret in it.
# The chart defaults every service to scrydon-platform; split namespaces
# are opt-in via namespaces.* (create the secret in each one you target).
kubectl create namespace scrydon-platform 2>/dev/null || true
kubectl create secret docker-registry scrydon-registry --namespace scrydon-platform \
--docker-server=scrydonops.azurecr.io \
--docker-username=<acr-token-name> --docker-password=<acr-token-password># 3. values.customer.yaml — the entire minimal install.
global:
imageRegistry: scrydonops.azurecr.io # pull images from the ACR you logged into
imagePullSecrets:
- scrydon-registry
storageClass: <your-storage-class> # cloud default class name, or your on-prem provisioner
routing:
host: app.example.com # the hostname your DNS points at
ingress:
tls:
enabled: true # browser reaches Scrydon over HTTPS — see Ingress below
infra:
db:
credentials:
password: REPLACE-WITH-DB-PASSWORD # openssl rand -hex 16
auth:
secrets:
AUTH_SECRET: REPLACE-WITH-AUTH-SECRET # openssl rand -hex 32
apiTable:
secrets:
STARROCKS_PASSWORD: REPLACE-WITH-STARROCKS-PW # openssl rand -hex 24# 4. Install
helm install scrydon oci://scrydonops.azurecr.io/scrydon/charts/scrydon \
--version <version> --namespace scrydon-platform \
-f values.customer.yaml --waitRun the setup wizard
Open https://app.example.com/platform/setup (or /setup if you mounted platform at the root). Five steps:
| # | Step | What it does |
|---|---|---|
| 1 | License | Paste or drop the { jwt, publicKey } JSON bundle. The wizard verifies the JWT signature against the bundled public key, checks expiry, and shows tier / CPU / RAM / VRAM. Stored in platform_config on the next step. The bundle's format is shown on the License Checker. Bare .jwt files are rejected — the public key must travel with the JWT. |
| 2 | Admin account | Create the first administrator (email + password). On submit, the license is persisted and the admin user created. |
| 3 | Organization | Name your organization — the root tenant. |
| 4 | Configure delivery (Resend, SendGrid, SMTP, or skip — configurable later under Settings → Platform → Email). With no real provider, new sign-ups skip email verification; once a provider is set, they receive an OTP. | |
| 5 | Complete | Marks setup_completed = true and redirects to the platform home. |
Pre-seed the license
Skip the wizard's license step by injecting the bundle on first install. The values are read once to seed the DB, after which the license lives in platform_config and is managed in the UI:
auth:
secrets:
AUTH_SECRET: REPLACE-WITH-AUTH-SECRET
LICENSE: | # the bundle's `jwt` field (one line)
eyJhbGciOiJSUzI1NiIs...
LICENSE_PUBLIC_KEY: | # the bundle's `publicKey` field (PEM)
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFA...
-----END PUBLIC KEY-----When both are set, the wizard's License step opens already verified.
Configuration reference
Every group below maps to a top-level key in the chart's values.yaml, in file order. Defaults shown are the chart defaults; override only what you need.
global — registry, images, storage
Cluster-wide settings every service inherits.
| Key | Default | What / why |
|---|---|---|
global.imageRegistry | "" | Container image registry root (e.g. scrydonops.azurecr.io, an internal Harbor, a homelab host:port). Empty emits unprefixed scrydon/<name>:<tag> refs. Set per environment. Does not cover the Dapr control plane — see dapr.global.registry below. |
dapr.global.registry | ghcr.io/dapr | Registry for the five Dapr control-plane images. A separate flag because they come from the Dapr subchart, which global.imageRegistry cannot reach (Helm subchart values are static YAML). On a private-registry or air-gapped cluster you must set both: an injector stuck in ImagePullBackOff makes every Scrydon pod fail admission (webhookFailurePolicy: Fail), which looks like an unrelated hang. helm install warns when only one is set. |
<image>.registry | "" | Per-image override of global.imageRegistry, on every third-party image block (global.images.busybox, infra.db.image, infra.starrocks.image, infra.seaweedfs.image, infra.lakekeeper.image, opa.image, eventBackbone.valkey.image). Empty inherits the global; set an explicit host to pull one image from elsewhere. Needed when your mirror does not carry every upstream under one scope. |
global.imagePullSecrets | [] | Names of pull secrets added to every pod. Set to the secret you created in step 2. |
global.imagePullPolicy | Always | Standard Kubernetes pull policy. |
global.requireExplicitSecrets | false | Fail rendering instead of generating a secret that cannot be preserved. Set true for GitOps and offline manifest rendering — see Rendering without a live cluster. |
global.storageClass | "" | The provisioner for every PVC. No cloud default — set it to your cloud's class (managed-csi, gp3, standard-rwo) or on-prem provisioner (ceph-rbd, vsphere-csi, local-path). |
global.nfs.enabled | false | Use NFS-backed static PVs instead of dynamic provisioning. For homelab / NFS-export clusters — set server + basePath too. |
global.scheduling.spreadAcrossNodes | true | Attach a soft pod anti-affinity to every long-running workload (apps and the memory-heavy infra tier: Postgres, StarRocks, SeaweedFS, Valkey) so they prefer separate nodes. Without it the scheduler may stack several multi-GiB pods on one node and the kubelet then evicts them under memory pressure. Soft means it never blocks scheduling, so single-node and small clusters still come up — leave it on. Set false only to deliberately pack onto one node. |
global.azure.enabled | false | Azure Marketplace mode: images resolve from MCR via global.azure.images.* and third-party data-plane (StarRocks/SeaweedFS) auto-disables. Set by the Marketplace package, not by hand. |
global:
imageRegistry: scrydonops.azurecr.io
imagePullSecrets:
- name: scrydon-registry
storageClass: managed-csinamespaces — workload placement
Every service defaults to scrydon-platform. Override per service for multi-namespace isolation; the chart auto-aligns Dapr ACL policies and the secret-reader RBAC.
namespaces:
infra: scrydon-infra
platform: scrydon-platform
agentic: scrydon-agentic
analytics: scrydon-platform
cortex: scrydon-platformingress — exposure and TLS scheme
| Key | Default | What / why |
|---|---|---|
ingress.enabled | true | Render Ingress objects. Disable if you front the cluster with your own ingress/gateway. |
ingress.className | traefik | Ingress class. traefik and nginx are handled natively (HTTPS redirect + Dapr-header strip). Any other controller renders a plain Ingress — supply its behaviour through ingress.annotations (see Other ingress controllers). |
ingress.tls.enabled | true | Load-bearing. This means "is Scrydon reached over HTTPS by the browser?" — it drives the public URL scheme, CORS origins, secure cookies, and the Better-Auth callback origin. Defaults to true (secure by default), including behind a TLS-terminating load balancer (App Gateway / ALB) that forwards plain HTTP to Traefik — the browser is still HTTPS, so keep it true. Set false only for HTTP-only deployments (local dev / kind smoke). Setting it false on an HTTPS-fronted install silently breaks login (Mixed Content + mismatched callback origin). |
ingress.tls.existingSecret | "" | Bring your own certificate: every Ingress references this named kubernetes.io/tls Secret instead of the per-app tls-<app> / tls-frontdoor names. Required for air-gapped and on-prem installs with a corporate-CA certificate — without cert-manager and a reachable ACME endpoint nothing creates those Secrets and your controller falls back to its default certificate. Kubernetes Secrets are namespace-local, so create the same named Secret in every namespace that receives an Ingress. |
ingress.tls.certManager.enabled | true | Stamp cert-manager.io/cluster-issuer on every Ingress. Set false when you supply the certificate yourself and cert-manager is installed — otherwise it picks the Ingress up and issues against clusterIssuer, leaving a permanently pending ACME order fighting your real certificate. |
ingress.tls.clusterIssuer | letsencrypt-prod | Issuer used when certManager.enabled. |
ingress.annotations | {} | Annotations merged into every Ingress the chart renders. The hook for controller-specific settings: request-body limits, WebSocket timeouts, ALB scheme/target-type, App Gateway timeouts. |
ingress.stripDaprHeaders.mode | auto | Where the Dapr/identity headers (dapr-caller-app-id, dapr-api-token, x-on-behalf-of-*, …) are stripped from external requests. auto resolves from className (Traefik Middleware or an nginx configuration-snippet). Other values: traefik, nginx, external (an upstream proxy/WAF does it — silences the install warning), disabled. On an unhandled controller nothing is emitted and helm install prints a warning. |
ingress.middleware.forceHttps.enabled | true | Redirect HTTP→HTTPS (Traefik middleware, or nginx.ingress.kubernetes.io/ssl-redirect). |
Certificate choices for Let's Encrypt, private-only domains, internal ACME, and static corporate certificates are covered in TLS Certificates.
Other ingress controllers
traefik and nginx need no extra work. For AGIC, AWS ALB, GKE GCE, or HAProxy set
ingress.className and add the controller's own annotations. In the default
subpath mode the front-door Ingress resources share one host, so use
routing.annotations; chart-wide settings go in ingress.annotations.
At minimum you need a request-body limit at least as large as the platform's
100 MB upload cap, and WebSocket-friendly read timeouts on the
routing.paths.agenticRealtime route:
ingress:
className: nginx
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: "100m"
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"Two controller-specific notes:
- nginx: the Dapr-header strip is emitted as a
configuration-snippet, which ingress-nginx ignores unlessallow-snippet-annotations: trueis set in its ConfigMap (defaultfalsesince 1.9). Enable it, or setingress.stripDaprHeaders.mode=externaland strip the headers upstream. - GKE GCE ingress: its default health check targets
/, which the SSR apps answer with a redirect chain. Point it at each app's/api/healthzwith aBackendConfig, or run ingress-nginx instead.
routing — subpath vs subdomain
Chooses how apps are exposed. Full discussion: Routing Modes.
| Key | Default | What / why |
|---|---|---|
routing.mode | subpath | subpath puts every app under one hostname at a path prefix (one DNS record, one cert — recommended for self-hosted). subdomain puts each app on its own hostname (wildcard DNS + cert — used by our SaaS). |
routing.host | — | The single hostname your DNS points at, e.g. app.example.com. |
routing.paths.* | /cortex, /agentic, … | Per-app path prefixes in subpath mode. Set one to "" to mount that app at the root. Apps consume the resolved prefix via BASE_PATH. |
routing.annotations | {} | Annotations for the shared-host front-door Ingress resources (subpath mode only), applied after chart-wide ingress.annotations. Collapsed installs render one; split installs render one per distinct application namespace. This is where controller-specific front-door settings go — see Other ingress controllers. |
routing:
mode: subpath
host: app.example.com
paths:
cortex: /scrydon/cortex # customize prefixes if needed
platform: /scrydon/platformdapr — service mesh and identity
Service-to-service calls use Dapr with mTLS; the chart's ACL policies key on the SPIFFE trust domain.
| Key | Default | What / why |
|---|---|---|
dapr.enabled | true | Sidecar injection + crypto components + service accounts. |
dapr.installControlPlane | true | Install the Dapr control plane as a subchart into the release namespace. Set false if Dapr is already installed cluster-wide. |
dapr.controlPlaneNamespace | "" | When installControlPlane: false and Dapr lives elsewhere (e.g. dapr-system), point the chart's secret-reader binding at that namespace. |
dapr.trustDomain | scrydon | SPIFFE trust domain. All rendered ACLs key on this. Override only to match an existing Dapr CA — but that disables cross-cluster identity isolation. |
dapr.global.tag | 1.18.1 | Dapr control-plane + sidecar image tag, lockstepped with the dependency in Chart.yaml. Match this tested version when bringing your own control plane. |
dapr.crypto.masterKey | "" | AES-256 master key. Auto-generated on fresh install and preserved across upgrades. BYO: openssl rand -base64 32. |
dapr.secretStore.enabled | true | Create the Kubernetes-secrets-backed Dapr SecretStore component. |
Bringing your own Dapr requires the chart-tested 1.18.1 version, trust domain
scrydon(or match it viadapr.trustDomain), and a running sidecar injector. See Existing Dapr installations.
Database — bundled Postgres or BYO
infra.db controls the bundled Postgres (pgvector). For an external/managed instance, the full recipe — per-provider notes, infra.db.external.* keys, pre-creating databases — is on BYO Database.
| Key | Default | What / why |
|---|---|---|
infra.db.enabled | true | Run bundled in-cluster Postgres. Set false to use a managed instance via infra.db.external.*. |
infra.db.credentials.password | postgres | Change this. openssl rand -hex 16. |
infra.db.storage.size | 5Gi | PVC size for the bundled DB. |
infra.db.postgres.maxConnections | 250 | max_connections for bundled Postgres. Keep this above the sum of rendered app pool caps plus migration/admin headroom. |
infra.db.resources.limits.memory | 4Gi | Memory ceiling for bundled Postgres, sized for maxConnections: 250 (request 2Gi). Lowering it without also lowering the connection ceiling risks the kernel OOM-killing the postmaster under load. |
infra.db.tls.enabled | true | In-cluster TLS (auto self-signed cert, ISO 27001 A.8.24). Leave on for the bundled DB; managed-Postgres users disable and use the provider's TLS. |
infra.db.backup.enabled | false | Opt-in pg_dump CronJob (A.8.13). Set schedule, retentionDays, databases. Managed-Postgres users use the provider's backup. |
infra.db.external.existingSecrets.* | "" | BYO Mode A (recommended) — per-app Secret names holding DATABASE_URL (auth/agentic), DATABASE_URL_ANALYTICS, etc. |
infra.db.external.<app>Url | "" | BYO Mode B — inline app DSNs (appear in helm get values). sslMode appended as ?sslmode=. |
infra.db.external.lakekeeperUrl | "" | Lakekeeper Postgres DSN. Required when infra.db.enabled=false and Lakekeeper is enabled; used for both read and write connections and stored in secrets-lakekeeper. |
# Bundled (default) — just set a strong password:
infra:
db:
credentials:
password: REPLACE-WITH-DB-PASSWORD
# BYO managed Postgres:
infra:
db:
enabled: false
external:
existingSecrets:
auth: scrydon-auth-db
agentic: scrydon-agentic-db
lakekeeperUrl: "postgres://scrydon:REDACTED@pg.example.com:5432/lakekeeper"
sslMode: requireStarRocks — Managed Tables (OLAP)
Single-pod allin1-ubuntu. Backs the Tables UI and the agentic schema-inference fallback. Pairs with apiTable.
| Key | Default | What / why |
|---|---|---|
infra.starrocks.enabled | true | Run bundled StarRocks. Set false (with apiTable.enabled: false, or point apiTable.starrocks.host at an external cluster) to skip it. Auto-disabled in Azure mode. |
infra.starrocks.storage.size | 20Gi | PVC size. FE metadata + BE storage share this PVC, so it survives pod restarts (the root password you set below persists). |
infra.starrocks.resources | 2–8Gi / 0.5–2 CPU | The heaviest bundled data-plane piece — see Trimming. |
apiTable.starrocks.replicationNum | "1" | StarRocks table replica count used in every CREATE TABLE. The default "1" matches the bundled single-pod allin1 deployment (one BE node). Raise it to match your BE count only when apiTable.starrocks.host points at an external multi-node cluster — a value above the number of alive BE nodes fails table creation with ERROR 1064: replication num should be less than or equal to the number of available BE nodes. |
The bundled StarRocks is single-pod (FE + one BE in one process). apiTable.secrets.STARROCKS_PASSWORD is handled for you: the bundled image ships root with no password, and if you set a non-empty value a password-reconciler sidecar in the starrocks-fe pod applies the same value to StarRocks automatically — on install, after restarts, and when you rotate the value. Leave it empty for a password-less root, or set it and the chart provisions it — no manual step. See StarRocks credentials.
apiTable.starrocks.replicationNum now defaults to "1" to match the single BE, so it no longer needs adjusting for the bundled deployment — only raise it when pointing apiTable.starrocks.host at an external multi-node cluster.
For multi-AZ production, switch to the starrocks-kubernetes-operator (FE/BE split), set apiTable.starrocks.replicationNum to match its BE count, and point apiTable.starrocks.host at its FE Service.
SeaweedFS — object storage
Single-pod S3-compatible storage for uploads.
| Key | Default | What / why |
|---|---|---|
infra.seaweedfs.enabled | true | Run bundled storage. Disable if you front the platform with managed S3 (AWS S3, Azure Blob via S3 API, GCS, MinIO) configured under https://<host>/settings/platform/storage. Auto-disabled in Azure mode. |
infra.seaweedfs.s3.existingSecret | "" | Blank → chart manages random access/secret keys (preserved across upgrades, kept on uninstall). Set to a pre-created Secret with accessKey/secretKey to BYO. |
infra.seaweedfs.storage.size | 20Gi | PVC size. |
Application services
The product apps — auth (api-platform), platform, cortex, analytics, apiOntology, apiTable, agentic (app + realtime). Each block shares a common shape:
| Key (per app) | What / why |
|---|---|
<app>.enabled | Render the app. Most stay on; analytics.enabled: false drops the analytics UI + marimo (heaviest optional workload). |
<app>.replicas | Horizontal scale. |
<app>.image.tag | Defaults to Chart.AppVersion — leave blank to track the chart. |
<app>.resources | Requests/limits. Defaults are tuned from production OOM incidents (agentic limit is 4Gi, the SSR apps 2Gi); lower only on eval clusters. |
<app>.databasePool.max | Per-pod postgres-js pool cap, rendered as the generic DATABASE_POOL_MAX env var. Every pool in the pod honors it; multi-database pods (auth, apiTable) add DB-specific keys (authMax, agenticMax) rendered as AUTH_DATABASE_POOL_MAX / AGENTIC_DATABASE_POOL_MAX overrides. Lower these for small managed Postgres tiers; raise only when database max_connections has matching headroom. |
<app>.ingress.* | Per-app hostname (subdomain mode only — ignored in subpath). |
<app>.corsOrigins | Allowed browser origins. |
<app>.secrets | Per-app secrets (below). |
<app>.dapr.appApiToken | Per-app sidecar-to-app token, unique per app. Blank → auto-generated and preserved across upgrades. BYO: openssl rand -base64 32. |
Notable app-specific keys:
auth:
secrets:
AUTH_SECRET: REPLACE-WITH-AUTH-SECRET # session signing — openssl rand -hex 32
auditLog:
enabled: true # SIEM forwarding + retention crons (ADR 2026-04-16)
chain:
enabled: false # tamper-evident hash chain — opt-in, has signing cost
analytics:
marimoDocument:
enabled: true # private, compute-free document renderer
marimoRuntime:
startupDeadlineMs: 300000 # Connect/Run preparation deadline
secrets:
MARIMO_RUNTIME_HMAC_SECRET: "" # blank → generated and reused across upgrades
apiTable:
starrocks:
host: "" # blank → bundled FE Service; set for external StarRocks
user: root
secrets:
STARROCKS_PASSWORD: REPLACE-WITH-STARROCKS-PW
agentic:
realtime:
ingress:
annotations: # sticky cookie for the WebSocket route
traefik.ingress.kubernetes.io/service.sticky.cookie: "true"
passkey:
rpId: app.example.com # MUST match routing.host or passkey registration fails
origin: https://app.example.com
auth.secrets.SERVICE_ADMIN_TOKENis deprecated — production uses Dapr mTLS (SPIFFE). Only set it for local dev without sidecars. Where underscore keys are disallowed (Azure Marketplace protected settings), useauth.authSecretinstead ofauth.secrets.AUTH_SECRET.
executionFabric and runtimePlane
The runtime plane is opt-in until its Kata/KVM substrate and network controls are qualified.
It owns fresh Agent exec pods, long-lived generation-fenced Marimo sessions, and fresh
single-use notebook jobs. Opening a notebook still uses the compute-free document renderer;
Connect, disconnected Run, and jobs refuse when the plane is unavailable. Marimo compute
never falls back to a product-service process. The deployment-owned executionFabric
policy selects between microvm and managed_process before dispatch. Both tiers ship
by default: the generic managed-process actor host renders out of the box (the normal
tier), and microvm (the isolated tier) is allowed and preferred, so a cluster that
enables the Runtime Plane isolates automatically. An organization admin turns on
Require isolated execution under Settings → Platform → Compute to make the microVM
a hard floor for that organization — its executions then refuse rather than use the
managed-process tier. A bound execution never downgrades after it starts.
The chart refuses to render an executionFabric whose allow-list has no deployed
backend (for example allowedTiers: [microvm] with runtimePlane.enabled: false).
That combination used to install silently and every execution then failed at runtime
with "No permitted … execution tier is currently ready"; it now fails at helm install
time with the values path to fix. A strict production posture that wants microVM only
sets managedProcess.enabled: false explicitly — see values.production.example.yaml.
The managed tier is one generic Dapr actor-host Deployment and image. Dapr owns placement and horizontal routing; each invocation gets a fresh child process tree and workspace while reusing the same Agent, implementation, and notebook runners as the microVM backend. It shares the actor-host pod kernel and is explicitly weaker than a microVM.
Standard Agents always use AgentCapabilityContract with the fixed
agent-capability-contract protocol. There is no integration-tool broker mode
to enable during install or upgrade, and no operator value selects another
Agent architecture. The chart pins the named protocol into Runtime Plane; an
incompatible runner image is refused before an Agent workload is created.
The chart replicates your global.imagePullSecrets from the release namespace into the two runtime namespaces at install/upgrade time, so private registries work without extra steps. (helm template-then-apply pipelines have no cluster to read from — pre-create the pull secrets in both runtime namespaces in that workflow.)
| Key | Default | What / why |
|---|---|---|
executionFabric.allowedTiers | [microvm, managed_process] | Deployment security allow-list. Supported values are only microvm and managed_process. At least one allowed tier must have a deployed backend or the chart refuses to render. |
executionFabric.preferredTiers | [microvm, managed_process] | Exact ordering of every allowed tier. The resolver selects the first eligible, ready tier before dispatch. |
executionFabric.managedProcess.enabled | true | Render the one generic Dapr actor host (the normal tier). Must agree with the allow-list; effective only when dapr, agentic, and agentic.app.dapr.workflows are enabled (its actor state store). |
executionFabric.managedProcess.replicas | 2 | Fixed replicas when HPA is disabled. Dapr placement routes per-execution actors across replicas. |
executionFabric.managedProcess.enabledProfiles | all three profiles | Fixed platform profiles: agent_turn, implementation, and notebook. These do not create separate apps or images. |
executionFabric.managedProcess.maxConcurrentInvocations | 4 | Maximum fresh process trees supervised concurrently by each replica. Saturation fails with a typed capacity error while Dapr placement/HPA provide horizontal scale. |
executionFabric.managedProcess.autoscaling.* | disabled, 2–10 replicas | Optional CPU-based HPA for the generic actor host. |
runtimePlane.enabled | false | Enable the microvm backend after qualifying Kata/KVM, control/callback paths, and egress controls. Interactive Marimo requires it; tier-aware Agent, implementation, and notebook-job requests may instead use managed_process only when policy permits. |
runtimePlane.namespace | scrydon-runtimes | Controller namespace. Must differ from the hostile workload namespace. |
runtimePlane.workloadNamespace | scrydon-runtime-workloads | Hostile-workload namespace. Controller RBAC manages owned exec pods and Marimo pods/Services/NetworkPolicies there. Both workload ServiceAccounts receive no API token. |
runtimePlane.replicas | 1 | Enforced single-controller topology. Recreate updates preserve exclusive ownership of the durable registry PVC. |
runtimePlane.persistence.* | 1Gi, cluster storage class | Required durable execution-claim and terminal-result store. A corrupt/unwritable store prevents boot or dispatch. |
runtimePlane.dapr.enabled | true | The plane's authenticated control path. Disabling this or global dapr.enabled fail-closes the plane off (nothing renders; agent turns refuse with a typed error). |
runtimePlane.image.* | release app version | Controller image. Pin the published release/canary tag; do not deploy latest. |
runtimePlane.runnerImage | empty | Empty resolves scrydon/bundle-runner at the chart's app version (same registry as every app image). When set, it must be a pinned tag or digest — production profiles pin registry/repo@sha256:<digest>; schema validation rejects latest. |
runtimePlane.enforcerImage | empty | Empty resolves scrydon/egress-enforcer at the chart's app version. When set, pin the reviewed digest; schema validation rejects latest. |
runtimePlane.marimoRuntimeImage | empty | The controller's immutable Marimo profile requires a sha256 digest reference (registry/repo@sha256:<digest>) — a tag-only value (including the empty-default app-version tag) makes the controller refuse to boot with Marimo runtime image must be pinned by sha256 digest. Always set the digest when runtimePlane.enabled: true. |
runtimePlane.marimo.* | 30-minute idle TTL, 30-second cull, 180-second provision timeout | Bounds interactive runtime idleness, cleanup polling, and controller provisioning. Analytics has a separate end-to-end preparation deadline. |
runtimePlane.blockedCidrs | RFC1918 ranges | Deployment-owned network floor included in every isolated-workload egress snapshot. Set the cluster pod/service CIDRs, metadata ranges, and private control networks; organization policy cannot remove them. |
runtimePlane.upstreamDns | 10.0.0.10:53 | Real kube-dns/CoreDNS address. Discover it from kube-system; a wrong address breaks all hostname egress. |
runtimePlane.execution.maxConcurrentPerTenant | 8 | Maximum pending/running execs for one tenant before typed HTTP 429. |
runtimePlane.execution.maxConcurrentGlobal | 128 | Single-controller global overload ceiling across tenants; must be at least the per-tenant ceiling. |
runtimePlane.isolation.microvm.enabled | false | Advertise a reachable kata-vm-isolation tier. Set this only after the RuntimeClass and backing Kata/KVM node pool exist. |
runtimePlane.isolation.microvm.renderRuntimeClass | false | Render a self-managed Kata RuntimeClass. Keep false on AKS Pod Sandboxing, where AKS owns it. |
networkPolicies.enabled | false | Render ingress/egress NetworkPolicies for the controller and hostile workloads. Set true on a cluster with an enforcing CNI; otherwise the Kubernetes network boundary is absent. |
The two deployment choices are microvm and managed_process. Historical Runtime Plane
wire fields (require_true_vm, fallback, mediated, and hardened) are not Helm or
organization tier controls. Tier-aware adapters emit a fixed microVM-only wire value after
the shared resolver selects microvm, solely for rolling compatibility with the Rust plane.
networkPolicies:
enabled: true
executionFabric:
allowedTiers: [microvm, managed_process]
preferredTiers: [microvm, managed_process]
managedProcess:
enabled: true
runtimePlane:
enabled: true
namespace: scrydon-runtimes
workloadNamespace: scrydon-runtime-workloads
persistence:
size: 1Gi
runnerImage: registry.example.com/scrydon/bundle-runner@sha256:<digest>
enforcerImage: registry.example.com/scrydon/egress-enforcer@sha256:<digest>
marimoRuntimeImage: registry.example.com/scrydon/marimo-runtime@sha256:<digest>
blockedCidrs:
- "10.0.0.0/8" # replace/extend with the actual pod + service CIDRs
- "172.16.0.0/12"
- "192.168.0.0/16"
upstreamDns: "10.0.0.10:53"
isolation:
microvm:
enabled: true
renderRuntimeClass: false # AKS owns kata-vm-isolationThe controller Deployment and both workload identities consume global.imagePullSecrets.
Single-use Agent/notebook-job pods use sa-runtime-exec; interactive Marimo pods use
sa-runtime-workload. Both set automountServiceAccountToken: false, so untrusted code
receives no Kubernetes API token while kubelet can still use the configured registry
credential. Do not patch the namespace default ServiceAccount. When the registry requires a
pull secret, create the same named Secret in runtimePlane.namespace,
runtimePlane.workloadNamespace, and every enabled application namespace before installation,
or provide equivalent node-level registry access.
The enforcer runs as UID 0 with only NET_ADMIN and NET_BIND_SERVICE; the untrusted app remains non-root with all capabilities dropped. A blanket Kubernetes restricted Pod Security profile rejects the enforcer. Configure a reviewed runtime-namespace admission exception and verify the exact rendered pods with kubectl apply --dry-run=server. Do not disable admission controls cluster-wide to make the plane schedule.
networkPolicies.enabled: true is required for the chart's controller/workload ingress boundary and only works with a CNI that enforces Kubernetes NetworkPolicy. A rendered policy on a non-enforcing CNI is not a security control; prove enforcement during deployment validation.
After deployment, check the controller's boot capability report and prove the selected tier separately for Agent exec, Marimo interactive, and notebook-job profiles. Also prove that opening a notebook creates no runtime, Connect runs no cells, and disconnected Run executes the selected cell once. A Ready controller alone does not prove Kata, image pulls, callbacks, DNS, dependency installation, cleanup, or egress enforcement.
eventBackbone — durable platform events (managed Valkey)
Chart v1.3.22+ ships a durable event backbone (committed workflow-editor synchronization, organization event streams) and a model-discovery cache. Both are on by default and share one bundled persistent Valkey instance.
| Key | Default | What / why |
|---|---|---|
eventBackbone.enabled | true | Durable platform event backbone. |
eventBackbone.broker.managedValkey | true | Bundled persistent Valkey (StatefulSet + PVC). Set false to supply your own Dapr pub/sub component via broker.componentType/broker.metadata. |
eventBackbone.valkey.password | "" | Blank auto-generates 64 chars on a fresh install and later upgrades preserve the live Secret. See the upgrade callout below. |
eventBackbone.valkey.storage.size | 2Gi | Broker PVC size. |
modelDiscoveryCache.enabled | true | Durable dynamic-model snapshots; stateStore.managedValkey: true reuses the same Valkey and password. |
Upgrading an existing installation to v1.3.22+: the first upgrade that activates the
managed Valkey has no existing event-backbone-valkey Secret to recover, and the chart
deliberately refuses to invent a password mid-upgrade (an offline/GitOps render would
silently rotate it). Set eventBackbone.valkey.password explicitly before that upgrade —
e.g. LC_ALL=C tr -dc 'A-Za-z0-9' </dev/urandom | head -c 64 — or the upgrade aborts with
cannot recover the managed Valkey password during upgrade. GitOps/offline rendering must
always set this value explicitly. One value covers both the event backbone and the
model-discovery cache.
opa — authorization decision point
Every workspace/workflow/ontology authorization decision is evaluated by OPA.
| Key | Default | What / why |
|---|---|---|
opa.enabled | true | Keep on. With it off (and no opa.url), those decisions fail closed. |
opa.url | "" | Blank → in-cluster Service DNS. Override for an external OPA or non-default Service. |
opa.logLevel | error | Only debug/info/error are valid — warn crashloops the pod. |
license — validation posture
| Key | Default | What / why |
|---|---|---|
license.enabled | true | Online license flow — phones home to license.scrydon.com every 24h. Air-gapped overlays set false / mode: offline. |
license.mode | online | online phones home; offline verifies the local JWT only. |
license.gracePeriod | 2592000 | Seconds of grace when phone-home fails (30 days). |
license.diagnostics.enabled | true | Send the installed Scrydon version. Disable for optional-metadata opt-out; contractual capacity still reports. |
license.capacity.acceleratorResources | [] | Vendor-neutral extended-resource-to-VRAM mappings used by hourly capacity sampling. Required when licensed VRAM is deployed. |
license.publicKeys | {} | Extra public keys for zero-downtime key rotation. |
auth.secrets.LICENSE_PUBLIC_KEY must be set whenever license.enabled: true so api-platform can verify JWT signatures. Scrydon must run on a dedicated cluster because capacity includes all schedulable worker nodes. See Licensing and License capacity reporting.
When networkPolicies.enabled=true, set networkPolicies.kubernetesApiEgress.enabled=true and its cidr to the Kubernetes API Service IP with a /32 mask. This permits only the node-capacity API connection; RBAC still limits the identity to get/list on nodes.
packSources — chart-managed pack distribution
| Key | Default | What / why |
|---|---|---|
packSources.enabled | false | Seed pack sources from chart values on every upgrade. |
packSources.sources[] | [] | List of (organization, name, kind, url, …) entries. Helm-managed rows are read-only in the UI. Credentials are referenced by authSecretRef and provisioned out-of-band. See Pack Sources. |
Pod security and disruption budgets
| Key | Default | What / why |
|---|---|---|
podSecurityStandards.hardenAppContainers | true | Stamp runAsNonRoot, seccompProfile: RuntimeDefault, drop: [ALL] and allowPrivilegeEscalation: false on the Scrydon app workloads and their util init containers. Safe by construction — all app images already declare a non-root USER. readOnlyRootFilesystem is deliberately not set (the compiled binaries stream their SSR bundle through a temp file at cold start). |
podSecurityStandards.enabled | false | Apply pod-security.kubernetes.io/* labels to the Scrydon namespaces. |
podSecurityStandards.enforce | baseline | Enforced profile. Not restricted, because the bundled data services (Postgres, StarRocks, SeaweedFS, Valkey, OPA, Lakekeeper) and the migration Jobs still run as root, and in the default single-namespace layout one PSS level governs them together with the apps. To enforce restricted, point the chart at external data services or move the bundled ones to their own namespaces.infra. |
podSecurityStandards.audit / .warn | restricted | Report the residual gap without blocking. |
podSecurityStandards.labelReleaseNamespace | true | Label the release namespace via a pre-install/pre-upgrade hook Job. Helm owns that namespace, so the chart cannot render a Namespace object for it — without this, enabling PSS in the default topology (every namespaces.* equal to the release namespace) labelled nothing and enforced nothing. The hook's ClusterRole is restricted by resourceNames to that one namespace, get/patch only. Set false to label it yourself; helm install then prints the commands. |
podDisruptionBudgets.enabled | false | PDBs for the stateless app Deployments. Off by default and rendered only for Deployments scaled past one replica: a PDB over a 1-replica Deployment blocks voluntary eviction, hanging kubectl drain, autoscaler consolidation, and managed node-pool upgrades. Enable it together with replicas > 1. |
podDisruptionBudgets.maxUnavailable | 1 | Never take more than N app pods down at once. |
The bundled data services are single-pod on ReadWriteOnce volumes and get no PDB —
one would convert a short restart into a stuck drain without adding availability.
For real HA run Postgres, StarRocks, object storage and the broker externally
(infra.db.external.*, apiTable.starrocks.host, managed S3/Blob,
eventBackbone.broker.managedValkey: false). agentic-realtime is pinned to one
replica while workflow-editor sync or the durable realtime consumer is enabled,
because Socket.IO has no cluster adapter configured yet.
Pod scheduling
Chart-wide nodeSelector, tolerations, and affinity apply to every Deployment, StatefulSet, and migration Job — so pods can land on tainted or dedicated nodes. Per-component overrides are not exposed.
tolerations:
- { key: workload, operator: Equal, value: scrydon, effect: NoSchedule }
nodeSelector:
workload: scrydon
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- { key: node-role.kubernetes.io/scrydon, operator: Exists }Trimming for low-resource installs
Defaults assume a production-shape cluster (≥ 8 vCPU / ≥ 32 GiB across nodes). On eval clusters (4 vCPU / 16 GiB), disable the heaviest optional pieces:
infra:
starrocks:
enabled: false # ~2–8Gi RAM, 20Gi PVC — only Managed Tables + agentic schema-inference affected
apiTable:
enabled: false
analytics:
enabled: false # drops the analytics UI + marimo sidecar (~2Gi)Common configuration recipes
Customizing routing paths
Mount apps under different prefixes (e.g. everything under /scrydon/...) via routing.paths.* — see the routing table above and Routing Modes.
Rendering without a live cluster (GitOps / offline)
Use this whenever the chart is rendered without a connection to the target
cluster — anything that runs helm template under the hood:
- ArgoCD (its default renderer)
- Kustomize's
helmCharts:inflator helm template … | kubectl apply -f -, the usual offline air-gap flowhelm install --dry-run,helm diff
Flux's HelmRelease drives a real helm upgrade --install and does not need
this. Neither does a direct helm install/helm upgrade, nor zarf package deploy.
Why it matters. Every chart-managed secret resolves live cluster Secret →
value you provided → generate a random one. Helm's lookup returns empty with no
API connection, so the third branch fires and produces a different value on
every render. Applying that rotates 38 secrets on each sync, and some rotations
cannot be undone:
| Value | Effect of rotating it |
|---|---|
dapr.crypto.masterKey | AES root for secrets at rest — every stored integration credential becomes permanently undecryptable |
infra.lakekeeper.encryptionKey | same, for Iceberg warehouse credentials |
auth.secrets.AUTH_SECRET | invalidates all sessions and API-key verification |
| SeaweedFS S3 keys | orphans every already-stored object |
What to do. Set global.requireExplicitSecrets: true. Each generate branch
becomes a render-time failure naming the exact values path, so a missing entry is
loud instead of destructive. Start from
values.gitops.example.yaml,
which lists the full required set and the kubectl commands to read the current
values out of an existing install before you adopt it:
helm template scrydon ./helm/scrydon \
-f helm/scrydon/values.yaml \
-f helm/scrydon/values.gitops.example.yaml \
-n scrydon-platformGenerate each value once, store it in your secret manager, and inject it at render
time (ArgoCD valuesFrom, a sops-decrypted overlay, --set-file). Keep the values
stable — changing one here changes it in the cluster.
On an existing install, read the current values out first (the commands are in that file). Inventing new ones rotates live credentials.
Existing Dapr installations
If the cluster already runs Dapr, set dapr.installControlPlane: false so the chart doesn't try to manage it, and point dapr.controlPlaneNamespace at where Dapr lives:
dapr:
installControlPlane: false
controlPlaneNamespace: dapr-systemYour Dapr must satisfy three prerequisites or sign-in works but account creation / cross-service calls fail with PermissionDenied:
| Requirement | Why |
|---|---|
| Dapr 1.18.1 (the chart-pinned and tested version) | Older 1.17.3–1.17.6 builds issued incorrect workload SPIFFE trust domains; matching the chart version avoids mixed-control-plane drift. |
Trust domain scrydon on dapr-sentry (or set dapr.trustDomain to match yours) | All ACLs key on it. |
| Sidecar injector running | Without it pods come up 1/1 (no daprd) and service invocation short-circuits. |
kubectl -n <dapr-ns> get deploy dapr-sentry \
-o jsonpath='{.spec.template.spec.containers[0].image}' # expect sentry:1.18.1Fail-closed sidecar injection (bundled Dapr only). The bundled control plane configures the injector webhook with failurePolicy: Fail, scoped to Scrydon app pods (label scrydon.io/dapr-mesh). If the injector is briefly unavailable — typically the first minute of a fresh install — Scrydon app pods are held back (ReplicaSet FailedCreate events mentioning sidecar-injector.dapr.io) and admitted automatically once it's Ready, instead of starting silently without their sidecar and breaking every cross-service call. A short burst of those events during install is normal; only investigate if it persists after the dapr-sidecar-injector pod is Running. A bring-your-own Dapr keeps its own webhook policy — upstream defaults to Ignore, so the 1/1 pods failure mode below remains possible there.
Advanced ingress: behind a TLS-terminating load balancer
App Gateway / ALB / GCP LB / F5 setups need Traefik-controller tuning (trustedIPs, externalTrafficPolicy: Local, the ingress.tls.enabled public-scheme rule, private ingress, 502 troubleshooting). Full playbook: TLS Offloading.
StarRocks credentials
The bundled StarRocks ships root with no password. If you set a non-empty apiTable.secrets.STARROCKS_PASSWORD, the chart applies it for you — a password-reconciler sidecar inside the starrocks-fe pod reconciles the root password on every boot and then re-verifies it once a minute: it applies the password on a password-less root, no-ops when it already matches, rotates root automatically when you change the value (a checksum annotation restarts the pod; the sidecar re-keys from the previous password it recorded on the StarRocks PVC), and logs loudly — without clobbering anything — if root was changed out-of-band. No manual step is required; leave the value empty for a password-less root.
Two behaviors worth knowing:
- First passworded boot may restart
starrocks-feonce. The image's own boot check races the very first password apply and shuts the container down if it loses; the password is already persisted by then, so the next boot is clean. One restart on a fresh install is benign — CrashLoop or repeated restarts are not. - Rotation order is handled for you. Changing the value and running
helm upgraderestartsstarrocks-fe(sidecar rotates root) andapi-table(its secret checksum changed) — no manualSET PASSWORD.
Automatic on charts that ship the password-reconciler sidecar. Charts v1.3.19–v1.3.21 used a starrocks-ensure-password post-install/post-upgrade hook Job instead — it worked on a warm cluster but could time out on a fresh install while the multi-GB StarRocks image was still pulling, marking the release failed and leaving root password-less (the Job then deletes itself, so there is nothing to inspect — check helm status). Charts before v1.3.19 never applied the password at all. In either case, apply it manually:
NEW_PW="<your-starrocks-password>" # same value as apiTable.secrets.STARROCKS_PASSWORD
# StarRocks runs in the release namespace by default (namespaces.infra → scrydon-platform).
kubectl -n scrydon-platform exec -it deploy/starrocks-fe -- \
mysql -h127.0.0.1 -P9030 -uroot -e "SET PASSWORD FOR 'root'@'%' = PASSWORD('${NEW_PW}');"
kubectl -n scrydon-platform rollout restart deployment/api-tableIf the password never reaches StarRocks (older chart with the hook Job failed or the manual step skipped), api-table stays 1/2 Running — Ready container plus a sidecar, but the app fails its readiness probe (/api/table/health/ready → 503, "starrocks": false). It does not CrashLoop, so the pod looks healthy at a glance. Because a not-Ready pod is removed from its api-table-dapr service endpoints, any service that calls api-table over Dapr (api-ontology, for one) gets ERR_DIRECT_INVOKE: failed to resolve address for api-table-dapr…. See Troubleshooting. (replication_num is auto-clamped to the live BE count, so a single-pod StarRocks no longer fails table provisioning on its own.)
The root password is stored in FE metadata on the StarRocks PVC, so it survives pod restarts, and the reconciler sidecar re-verifies it continuously.
To bring your own StarRocks instead:
infra:
starrocks:
enabled: false
apiTable:
starrocks:
host: starrocks-fe.starrocks.svc.cluster.local
user: scrydon
secrets:
STARROCKS_PASSWORD: REPLACE-WITH-STARROCKS-PASSWORDVerify the deployment
kubectl get pods -n scrydon-platform # all Running, 2/2 with the Dapr sidecar
kubectl get ingress -n scrydon-platform # each Ingress has an ADDRESS
kubectl get certificates -A # cert-manager Certificates READY=TrueLicense claims (tier, entitlements, days-to-expiry) appear under Platform Settings → License after /setup. Sign in at https://app.example.com/ and switch apps via the product switcher without re-authenticating.
Troubleshooting
| Symptom | Cause / fix |
|---|---|
Pods 1/1 (not 2/2) | Pod was admitted without its daprd sidecar — BYO-Dapr injector not running (see Existing Dapr installations), or a pre-fail-closed chart raced the injector on a fresh install. Fix: ensure dapr-sidecar-injector is Ready, then kubectl rollout restart the affected deployments. |
App pods not created; ReplicaSet events show FailedCreate … admission webhook "sidecar-injector.dapr.io" … | The bundled injector is down or still starting and fail-closed injection is holding Scrydon pods back (by design — better than sidecar-less pods). Transient during install; if persistent, check the dapr-sidecar-injector pod. |
Sign-up fails with PermissionDenied from agentic | Wrong Dapr trust domain. kubectl logs deploy/agentic -n scrydon-platform -c daprd | grep spiffe (or your namespaces.agentic override) — IDs should be spiffe://scrydon/..., not spiffe://public/.... Bump Dapr to ≥ 1.17.7. |
Migration Job CrashLoopBackOff | On BYO Postgres, a database wasn't pre-created — see BYO Database → Pre-create databases. |
Init:ImagePullBackOff with insufficient_scope (403) | Your ACR token's scope-map is missing a third-party mirror repo (scrydon/busybox, scrydon/pgvector, scrydon/opa). Send your account team the exact error + repo path — only the scope-map is updated, no token re-issue. |
| Login silently broken behind a load balancer | ingress.tls.enabled left false — set it true (see Ingress). |
api-table stuck 1/2 Running (Ready never reached, but no CrashLoop) | App fails its StarRocks readiness gate. Check kubectl -n scrydon-platform exec deploy/api-table -c api-table -- wget -qO- localhost:7500/api/table/health/ready — "starrocks": false means the root password doesn't match. Apply it inside StarRocks (StarRocks credentials). Confirm a fresh mysql -uroot -p"<pw>" connects to svc-starrocks-fe:9030. |
Day-2 operations
- Renew the license — paste the refreshed bundle under Settings → License → Update license; no restart. Licensing.
- Backup & restore — bundled Postgres CronJob (
infra.db.backup) or your managed provider. Backup & Restore. - Upgrades — Upgrades.
- Observability — Observability.