Scrydon
DeploymentReference

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:

  1. 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).
  2. Your values.customer.yaml — the file you pass with -f. Override only what you need; everything else inherits the default.
  3. --set flags — 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.yaml and helm 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 below

Validate 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 allowed

Before 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=server

The 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 --wait

Run the setup wizard

Open https://app.example.com/platform/setup (or /setup if you mounted platform at the root). Five steps:

#StepWhat it does
1LicensePaste 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.
2Admin accountCreate the first administrator (email + password). On submit, the license is persisted and the admin user created.
3OrganizationName your organization — the root tenant.
4EmailConfigure 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.
5CompleteMarks 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.

KeyDefaultWhat / 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.registryghcr.io/daprRegistry 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.imagePullPolicyAlwaysStandard Kubernetes pull policy.
global.requireExplicitSecretsfalseFail 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.enabledfalseUse NFS-backed static PVs instead of dynamic provisioning. For homelab / NFS-export clusters — set server + basePath too.
global.scheduling.spreadAcrossNodestrueAttach 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.enabledfalseAzure 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-csi

namespaces — 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-platform

ingress — exposure and TLS scheme

KeyDefaultWhat / why
ingress.enabledtrueRender Ingress objects. Disable if you front the cluster with your own ingress/gateway.
ingress.classNametraefikIngress 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.enabledtrueLoad-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.enabledtrueStamp 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.clusterIssuerletsencrypt-prodIssuer 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.modeautoWhere 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.enabledtrueRedirect 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 unless allow-snippet-annotations: true is set in its ConfigMap (default false since 1.9). Enable it, or set ingress.stripDaprHeaders.mode=external and 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/healthz with a BackendConfig, or run ingress-nginx instead.

routing — subpath vs subdomain

Chooses how apps are exposed. Full discussion: Routing Modes.

KeyDefaultWhat / why
routing.modesubpathsubpath 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.hostThe 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/platform

dapr — service mesh and identity

Service-to-service calls use Dapr with mTLS; the chart's ACL policies key on the SPIFFE trust domain.

KeyDefaultWhat / why
dapr.enabledtrueSidecar injection + crypto components + service accounts.
dapr.installControlPlanetrueInstall 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.trustDomainscrydonSPIFFE 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.tag1.18.1Dapr 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.enabledtrueCreate 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 via dapr.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.

KeyDefaultWhat / why
infra.db.enabledtrueRun bundled in-cluster Postgres. Set false to use a managed instance via infra.db.external.*.
infra.db.credentials.passwordpostgresChange this. openssl rand -hex 16.
infra.db.storage.size5GiPVC size for the bundled DB.
infra.db.postgres.maxConnections250max_connections for bundled Postgres. Keep this above the sum of rendered app pool caps plus migration/admin headroom.
infra.db.resources.limits.memory4GiMemory 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.enabledtrueIn-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.enabledfalseOpt-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: require

StarRocks — Managed Tables (OLAP)

Single-pod allin1-ubuntu. Backs the Tables UI and the agentic schema-inference fallback. Pairs with apiTable.

KeyDefaultWhat / why
infra.starrocks.enabledtrueRun 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.size20GiPVC size. FE metadata + BE storage share this PVC, so it survives pod restarts (the root password you set below persists).
infra.starrocks.resources2–8Gi / 0.5–2 CPUThe 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.

KeyDefaultWhat / why
infra.seaweedfs.enabledtrueRun 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.size20GiPVC 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>.enabledRender the app. Most stay on; analytics.enabled: false drops the analytics UI + marimo (heaviest optional workload).
<app>.replicasHorizontal scale.
<app>.image.tagDefaults to Chart.AppVersion — leave blank to track the chart.
<app>.resourcesRequests/limits. Defaults are tuned from production OOM incidents (agentic limit is 4Gi, the SSR apps 2Gi); lower only on eval clusters.
<app>.databasePool.maxPer-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>.corsOriginsAllowed browser origins.
<app>.secretsPer-app secrets (below).
<app>.dapr.appApiTokenPer-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_TOKEN is deprecated — production uses Dapr mTLS (SPIFFE). Only set it for local dev without sidecars. Where underscore keys are disallowed (Azure Marketplace protected settings), use auth.authSecret instead of auth.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.)

KeyDefaultWhat / 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.enabledtrueRender 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.replicas2Fixed replicas when HPA is disabled. Dapr placement routes per-execution actors across replicas.
executionFabric.managedProcess.enabledProfilesall three profilesFixed platform profiles: agent_turn, implementation, and notebook. These do not create separate apps or images.
executionFabric.managedProcess.maxConcurrentInvocations4Maximum 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 replicasOptional CPU-based HPA for the generic actor host.
runtimePlane.enabledfalseEnable 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.namespacescrydon-runtimesController namespace. Must differ from the hostile workload namespace.
runtimePlane.workloadNamespacescrydon-runtime-workloadsHostile-workload namespace. Controller RBAC manages owned exec pods and Marimo pods/Services/NetworkPolicies there. Both workload ServiceAccounts receive no API token.
runtimePlane.replicas1Enforced single-controller topology. Recreate updates preserve exclusive ownership of the durable registry PVC.
runtimePlane.persistence.*1Gi, cluster storage classRequired durable execution-claim and terminal-result store. A corrupt/unwritable store prevents boot or dispatch.
runtimePlane.dapr.enabledtrueThe 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 versionController image. Pin the published release/canary tag; do not deploy latest.
runtimePlane.runnerImageemptyEmpty 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.enforcerImageemptyEmpty resolves scrydon/egress-enforcer at the chart's app version. When set, pin the reviewed digest; schema validation rejects latest.
runtimePlane.marimoRuntimeImageemptyThe 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 timeoutBounds interactive runtime idleness, cleanup polling, and controller provisioning. Analytics has a separate end-to-end preparation deadline.
runtimePlane.blockedCidrsRFC1918 rangesDeployment-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.upstreamDns10.0.0.10:53Real kube-dns/CoreDNS address. Discover it from kube-system; a wrong address breaks all hostname egress.
runtimePlane.execution.maxConcurrentPerTenant8Maximum pending/running execs for one tenant before typed HTTP 429.
runtimePlane.execution.maxConcurrentGlobal128Single-controller global overload ceiling across tenants; must be at least the per-tenant ceiling.
runtimePlane.isolation.microvm.enabledfalseAdvertise a reachable kata-vm-isolation tier. Set this only after the RuntimeClass and backing Kata/KVM node pool exist.
runtimePlane.isolation.microvm.renderRuntimeClassfalseRender a self-managed Kata RuntimeClass. Keep false on AKS Pod Sandboxing, where AKS owns it.
networkPolicies.enabledfalseRender 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-isolation

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

KeyDefaultWhat / why
eventBackbone.enabledtrueDurable platform event backbone.
eventBackbone.broker.managedValkeytrueBundled 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.size2GiBroker PVC size.
modelDiscoveryCache.enabledtrueDurable 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.

KeyDefaultWhat / why
opa.enabledtrueKeep 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.logLevelerrorOnly debug/info/error are valid — warn crashloops the pod.

license — validation posture

KeyDefaultWhat / why
license.enabledtrueOnline license flow — phones home to license.scrydon.com every 24h. Air-gapped overlays set false / mode: offline.
license.modeonlineonline phones home; offline verifies the local JWT only.
license.gracePeriod2592000Seconds of grace when phone-home fails (30 days).
license.diagnostics.enabledtrueSend 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

KeyDefaultWhat / why
packSources.enabledfalseSeed 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

KeyDefaultWhat / why
podSecurityStandards.hardenAppContainerstrueStamp 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.enabledfalseApply pod-security.kubernetes.io/* labels to the Scrydon namespaces.
podSecurityStandards.enforcebaselineEnforced 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 / .warnrestrictedReport the residual gap without blocking.
podSecurityStandards.labelReleaseNamespacetrueLabel 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.enabledfalsePDBs 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.maxUnavailable1Never 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 flow
  • helm 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:

ValueEffect of rotating it
dapr.crypto.masterKeyAES root for secrets at rest — every stored integration credential becomes permanently undecryptable
infra.lakekeeper.encryptionKeysame, for Iceberg warehouse credentials
auth.secrets.AUTH_SECRETinvalidates all sessions and API-key verification
SeaweedFS S3 keysorphans 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-platform

Generate 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-system

Your Dapr must satisfy three prerequisites or sign-in works but account creation / cross-service calls fail with PermissionDenied:

RequirementWhy
Dapr 1.18.1 (the chart-pinned and tested version)Older 1.17.31.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 runningWithout 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.1

Fail-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-fe once. 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 upgrade restarts starrocks-fe (sidecar rotates root) and api-table (its secret checksum changed) — no manual SET 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-table

If 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-PASSWORD

Verify 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=True

License 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

SymptomCause / 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 agenticWrong 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 CrashLoopBackOffOn 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 balanceringress.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.
  • UpgradesUpgrades.
  • ObservabilityObservability.
On this page

On this page