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 package.
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, gateway, 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
gateway:
tls:
enabled: true # browser reaches Scrydon over HTTPS — see Gateway 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 --wait-for-jobsRun the setup wizard
Open https://app.example.com/platform/setup (or /setup if you mounted platform at the root). Six steps:
| # | Step | What it does |
|---|---|---|
| 1 | License | Paste or drop the { jwt, publicKey } JSON bundle. The wizard checks expiry and shows tier / CPU / RAM / VRAM. On the next step the server verifies the JWT against the Scrydon signing keys built into the release and stores it in platform_config; a license it refuses is not stored. The bundle's format is shown on the License Checker. Bare .jwt files are rejected. |
| 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 | Storage | Point the organization at S3-compatible or Azure object storage (endpoint, bucket/container, credentials), or skip to stay on local file storage. Credentials are stored encrypted; the non-secret settings land in storage_config. Configurable later at https://<host>/platform/settings/platform/storage. |
| 5 | Configure delivery (SMTP, the default; Resend; SendGrid; 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. | |
| 6 | Complete | Marks setup_completed = true and redirects to the platform home. |
Skipping the Storage step leaves the organization on local file storage. Analytics managed tables are Iceberg-only and store data in the organization's own object storage, so Analytics stays behind an "Object storage required" notice until S3-compatible or Azure storage is configured. Everything else works normally.
Pre-seed the license
Skip the wizard's license step by giving the chart the jwt field of your license file on first install. The value is 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: "eyJhbGciOiJNTC1EU0EtNjUi…" # the file's `jwt` fieldWhen it is set, the wizard's License step opens already verified. There is no public-key value: auth.secrets.LICENSE_PUBLIC_KEY is ignored in production, and the upgrade deletes any public-key row it stored.
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-platformgateway — exposure and TLS scheme
The edge is the Kubernetes Gateway API (Gateway + HTTPRoute), not Ingress.
Ingress is frozen: it is still GA, keeps its stability guarantees, and Kubernetes
has no plans to remove it — but it receives no further development, and Kubernetes
recommends the Gateway API in its place. The chart follows that recommendation.
The chart renders one Gateway called gateway-frontdoor in the platform namespace
and one HTTPRoute per enabled app in that app's own namespace, attached across
namespaces to the single Gateway.
The Gateway API is not present in a stock cluster. Before installing, the cluster
needs the gateway.networking.k8s.io CRDs and a GatewayClass whose name matches
gateway.className. Set traefik.enabled: true to have the chart install a Traefik
release that provides both, or point gateway.className at a class your platform
team already runs. See Kubernetes prerequisites.
| Key | Default | What / why |
|---|---|---|
gateway.enabled | true | Render the Gateway and its HTTPRoutes. Disable if you front the cluster with your own edge. |
gateway.className | traefik | The GatewayClass name (not an IngressClass). Any conforming implementation routes the chart's traffic; the one control that is not portable is gateway.publicAuthRateLimit below. |
gateway.listenerPorts.http | 8000 | The port your controller listens on, not the public port — see Listener ports. |
gateway.listenerPorts.https | 8443 | Same, for the HTTPS listener. |
gateway.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, and it decides whether an HTTPS listener plus the redirect route are rendered at all. 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). |
gateway.tls.existingSecret | "" | Bring your own certificate instead of the chart's tls-frontdoor / tls-<app> names. Required for air-gapped and on-prem installs with a corporate-CA certificate. One Secret, one namespace: the Gateway owns every listener, so it is only ever read from the platform namespace — even in a split-namespace install. |
gateway.tls.certManager.enabled | true | Stamp cert-manager.io/cluster-issuer on the Gateway (one annotation for the whole front door; cert-manager groups listeners by certificate Secret). Requires cert-manager's Gateway API support — config.enableGatewayAPI=true, see TLS prerequisites. Set false when you supply the certificate yourself and cert-manager is installed — otherwise it can leave a pending ACME order fighting your real certificate. |
gateway.tls.clusterIssuer | letsencrypt-prod | Issuer used when certManager.enabled. |
gateway.publicAuthRateLimit.enabled | false | Coarse public-edge bucket on the routing.paths.apiAuth rule, applied before the request reaches api-platform and separate from Better Auth's own per-endpoint limits. Traefik-only — rate limiting is not in the Gateway API standard channel, so it is attached as a Traefik Middleware through an ExtensionRef filter and the render fails when gateway.className is anything other than traefik. |
gateway.publicAuthRateLimit.average / .period / .burst | 10 / 1m / 10 | The bucket itself. |
gateway.annotations | {} | Annotations merged onto the Gateway. The hook for implementation-specific settings (cloud LB tuning, frontend references). Routes need none. |
traefik.enabled | false | Install Traefik as a bundled chart dependency, with the Gateway provider on, its own default Gateway off, and the traefik GatewayClass published. Off by default: a cluster that already runs a conforming implementation should keep it and only set gateway.className. |
Three controls that used to be per-controller annotations are now core Gateway API and are rendered unconditionally, so they exist on every conforming implementation instead of only on Traefik:
- The Dapr / on-behalf-of header strip. Every forwarding route rule carries a
RequestHeaderModifierfilter that removesdapr-caller-app-id,dapr-api-token,x-scrydon-self-call, thex-on-behalf-of-*identity headers andx-scrydon-storage-scopefrom external requests. There is no class branch and no mode that can silently emit nothing, soingress.stripDaprHeaders.*— and the install warning that went with it — is gone with no replacement. - The HTTP → HTTPS redirect. A
RequestRedirectfilter on the HTTP listener, rendered whenevergateway.tls.enabled.ingress.middleware.forceHttps.*is gone with no replacement. - Path precedence. The spec mandates longest-matching-prefix first across every
route on a listener, so declaration order no longer matters and the apex
/rule is simply the lowest-precedence one.
Internal service paths are not published. On every hostname, /api/internal beneath each
app's root (/api/internal, /platform/api/internal, /agentic/api/internal, and so on)
matches a rule with no backend, so the gateway answers 500 and never forwards the
request; those routes are reachable only inside the service mesh. This is core Gateway API
too.
Certificate choices for Let's Encrypt, private-only domains, internal ACME, and static corporate certificates are covered in TLS Certificates.
Listener ports: 8000 / 8443, not 80 / 443
gateway.listenerPorts.* is the port your controller listens on, not the port
your users connect to. It is the one implementation-specific value in the whole edge.
Traefik matches a Gateway listener to one of its own entryPoints by port, and
those entryPoints bind :8000 (web) and :8443 (websecure) inside the container
while its Service publishes 80/443. A listener declared on port 80 is rejected
outright (verified on Traefik v3.6.10, 2026-09-12):
Accepted=False reason=PortUnavailable
"Cannot find entryPoint for Gateway: no matching entryPoint for port 80 and protocol HTTP"A managed implementation that listens on the public port — Application Gateway
for Containers, a cloud ALB — wants 80 / 443 here instead:
gateway:
className: <your-gatewayclass>
listenerPorts:
http: 80
https: 443Other Gateway API implementations
Set gateway.className to your GatewayClass and the routing works unchanged: the
chart emits only core Gateway and HTTPRoute resources, with no controller
annotations on any route. Two things to get right before you switch:
| Control | Portable? |
|---|---|
Dapr / on-behalf-of header strip (RequestHeaderModifier) | Core. Works on every conforming implementation. |
HTTP → HTTPS redirect (RequestRedirect) | Core. |
| Path precedence (longest prefix wins) | Core, mandated by the spec. |
TLS termination via listener certificateRefs | Core. |
gateway.listenerPorts.* | Implementation-specific — see Listener ports. |
gateway.publicAuthRateLimit | Traefik-only. Attached as a Traefik Middleware through an ExtensionRef filter; the render fails on any other class. Leave it false and apply the limit at your own edge. |
Anything the chart does not express — request-body limits, read timeouts, WebSocket
idle behaviour on the routing.paths.agenticRealtime route — is configured on your
implementation itself rather than through this chart; gateway.annotations is the
only annotation hook and it lands on the Gateway, not on the routes. Size the
request-body limit at least as large as the platform's 100 MB upload cap.
One portable note: implementations whose backend health check defaults to / see a
redirect chain from the SSR apps. Point the check at each app's /api/healthz using
whatever health-check mechanism your implementation provides.
ingress-nginx is retired — the project was archived on 2026-03-24 and ships no
further releases, bugfixes, or security fixes. Do not install it, and do not point
gateway.className at an nginx Ingress class: the chart no longer renders Ingress
objects at all.
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 is gone. It existed only because a subpath install had exactly
one Ingress and no other way to annotate it; both routing modes now render one
Gateway, so gateway.annotations is that place — see Other Gateway API
implementations.
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.reserveSidecarPorts | true | Reserves the sidecar's gRPC ports (50001-50002) from each Dapr-enabled pod's ephemeral port range with the safe sysctl net.ipv4.ip_local_reserved_ports. Without it an app's outbound connection can take one of those ports first, and daprd crash-loops with bind: address already in use. Disable only if a policy engine rejects pod sysctls. |
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), set it to that namespace. It is required in that case, not optional: the chart points its secret-reader binding, the Dapr operator's RBAC and marimo-document's egress policy at it, and left empty they all target the release namespace — marimo-document's sidecar then cannot reach dapr-sentry and the install never becomes ready. |
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. |
dapr.apiLogging.enabled | false | Log every sidecar API call (msg="HTTP API Called"). Off by default — on a busy cluster this is the single largest log producer (it was 46% of all log volume before being disabled), an access log that is rarely read but always indexed and retained. Turn it on while debugging a sidecar routing problem, then turn it back off. |
dapr.apiLogging.obfuscateURLs | true | Redact path segments in those API-log lines. Keep it on: the scheduler routes embed tenant identifiers (/v1.0-alpha1/jobs/data-source-tick-org-<org-uuid>), so turning it off writes organization IDs into your log store. |
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 | "" | On a live helm install, empty generates a random password and keeps it in secrets-infra, which survives helm uninstall alongside db-pvc. Once Postgres is initialised, the password in the live secrets-infra always wins: a different value here is ignored (the install notes say so) instead of breaking every connection. To rotate it, see Rotating the bundled Postgres password. A render without cluster access (helm template, ArgoCD, helm diff) cannot read secrets-infra: it uses this value exactly as written, or postgres when empty, as the chart always did. On a live install an explicit value may only use A-Z a-z 0-9 . _ ~ ! $ & ' ( ) * + , ; = -, because it is embedded unescaped in every connection URL. openssl rand -hex 16 qualifies. |
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 and retentionDays. databases defaults to empty, which dumps every database the chart creates; list names only to dump fewer. Each run also writes globals-<timestamp>.sql.gz (the cluster roles, no passwords), which a restore applies first. Managed-Postgres users use the provider's backup. |
infra.db.backup.offCluster.enabled | false | Ship retained logical dumps plus .sha256 companions to Azure Blob. Set endpoint, container, objectPrefix, and credentialSecret; remote lifecycle/immutability remains an Azure policy. |
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 | 5–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.
One replica on a ReadWriteOnce volume, so a rollout of it (a chart upgrade that
changes its image or credentials) stops the old pod before the new one starts.
Table writes during that window are answered 503 object_store_unavailable and
retried. The pod is marked ready only once its master lists a storage node, which
keeps the window to a few seconds. Point the platform at managed S3 if you need
storage that stays up through upgrades.
| 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 at https://<host>/platform/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. |
Deployment default store
Where every organization without S3 or Azure storage of its own keeps its
files, under orgs/<organizationId>/ in one bucket. Only auth
(api-platform) resolves storage, so only it receives the STORAGE_DEFAULT_S3_*
env. Without a default, those organizations cannot upload until an admin
configures storage, and the setup wizard's storage step becomes mandatory.
| Key | Default | What / why |
|---|---|---|
storage.deploymentDefault.enabled | auto | auto: on when s3.endpoint is set or the bundled SeaweedFS is enabled, which is every install except one with global.azure.enabled. true forces it on and fails the render when there is no store to point at. false turns it off. |
storage.deploymentDefault.s3.endpoint | "" | Blank → the bundled SeaweedFS (svc-seaweedfs-s3.<infra-ns>:8333). Set to use an external S3-compatible store; existingSecret is then required. |
storage.deploymentDefault.s3.existingSecret | "" | Secret with accessKey/secretKey in the auth namespace. Blank → the SeaweedFS credentials, copied into that namespace when it differs from the infra namespace. |
storage.deploymentDefault.s3.bucket | scrydon-uploads | Created on first write. |
storage.deploymentDefault.s3.publicEndpoint | "" | Browser-reachable endpoint for presigned URLs. Blank → transfers go through the platform. |
storage.deploymentDefault.s3.region / forcePathStyle | us-east-1 / true | Standard S3 client settings. |
Upgrading from a release where organizations without storage settings used
local storage? Files on the pods' upload volumes are not moved. On
emptyDir they are lost when a pod restarts. Copy them into the default store
before upgrading. To do that, copy /app/uploads out of every api-platform
and api-agentic pod, then upload each file to
orgs/<organizationId>/<key> in the default bucket. The file_metadata
table in the auth database records which organization owns each key.
Application services
The product apps — auth (api-platform), platform, cortex, horizon, analytics, apiOntology, apiTable, agentic (app + realtime). Each block shares a common shape (horizon is the stateless exception: no database, no Dapr sidecar, always in the platform namespace, and horizon.copEnabled gates only its tactical picture surfaces):
| 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, platform, apiTable) add DB-specific keys (authMax, agenticMax) rendered as AUTH_DATABASE_POOL_MAX / AGENTIC_DATABASE_POOL_MAX overrides — platform opens the auth DB directly, so it sets authMax alongside max. Lower these for small managed Postgres tiers; raise only when database max_connections has matching headroom. |
<app>.gateway.{enabled,hostname} | Per-app hostname (subdomain mode only — ignored in subpath). Per-app annotations and per-app TLS blocks are gone: one Gateway owns every listener, and the per-app Secret names keep their tls-<app> defaults. |
<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
maxInFlight: 64 # open calls per pod, shared by router, platform API and MCP (429 past it)
bodyRead: # a request body must keep arriving (400 body_read_too_slow)
graceMs: 10000 # before the rate floor applies
minBytesPerSecond: 65536 # lower both for clients that upload over slow links
analytics:
marimoDocument:
enabled: true # private, compute-free document renderer
marimoRuntime:
startupDeadlineMs: 300000 # Connect/Run preparation deadline
appRevalidation:
enabled: false # re-pin published apps after a runtime upgrade (staging first)
secrets:
MARIMO_RUNTIME_HMAC_SECRET: "" # blank → generated and reused across upgrades
apiOntology:
secrets:
APP_PUBLICATION_SIGNING_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:
gateway:
hostname: ws-agentic.example.com # subdomain mode only
passkey:
rpId: example.com # subdomain mode: required, the domain every app host shares
origin: https://app.example.com # subdomain mode: required. Subpath mode derives both from routing.host
apiOntology.secrets.APP_PUBLICATION_SIGNING_SECRET— signs app-publication validation seals (Analytics signs, api-ontology verifies). At least 32 bytes. Set it only here; the chart renders the same value into both Secrets. Empty reuses the live value or generates one; withglobal.requireExplicitSecrets=trueit must be set. The key lives only underapiOntology; setting it underanalytics.secretsfails the render.
The realtime sticky-cookie annotations are gone. They only ever applied in subdomain mode, and session affinity for Gateway API backends is not part of the standard channel — so
agentic-realtimehas no affinity if you run it with more than one replica in subdomain mode. It shipsreplicas: 1, which is also what the PodDisruptionBudget settings below assume.
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. Deployed backends and authenticated,
profile-specific readiness determine which tiers are available. The resolver always tries
microvm first, then managed_process only when the organization permits it. The generic
managed-process actor host renders by default (the normal tier); enabling a qualified
Runtime Plane adds the isolated tier without another allow-list or preference value. 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 when neither execution backend is deployed. 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 paths 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.managedProcess.enabled | true | Deploy the one generic Dapr actor host (the normal tier). Effective only when dapr, agentic, and agentic.app.dapr.workflows are enabled (its actor state store). Disable it for a physically microVM-only deployment. |
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 | Deploy the microvm backend after qualifying Kata/KVM, control/callback paths, and egress controls. The controller discovers the RuntimeClass and compatible nodes itself. Interactive Marimo requires it; tier-aware Agent, implementation, and notebook-job requests may instead use managed_process only when organization 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, 300-second provision timeout | Bounds interactive runtime idleness, cleanup polling, and controller provisioning. Analytics has a separate end-to-end preparation deadline. |
runtimePlane.marimo.maxSizeTier | large | Largest Marimo runtime size any request may use: xsmall, small, medium or large (sets RUNTIME_PLANE_MAX_SIZE_TIER). A request above it is refused with HTTP 409 runtime_size_tier_unavailable, never clamped to a smaller size. The cap applies to every request, including reusing a running editor and replaying a published app. Settings → Compute shows sizes above it as unavailable. An unknown value fails schema validation, and the controller refuses to boot on one. Lower it below medium only after Analytics is upgraded: an older Analytics sends no size, which the plane reads as medium, so every runtime it starts would be refused. |
runtimePlane.blockedCidrs | RFC1918 ranges | Deployment-owned network floor: the cluster's own pod, service and node ranges. Included in every isolated-workload egress snapshot, and read under the same name by api-platform, api-ingestion and api-table, which never let a connection, a Log Receiver or an OAuth provider endpoint reach it. Organization policy cannot remove them. Narrow it to the real ranges before adding private networks. |
runtimePlane.upstreamDns | empty | Optional override of the DNS server sandbox egress forwards to. Empty — the right choice on AKS, EKS, GKE, k3s, kind and on-premises alike — uses the first nameserver in the runtime plane's own pod /etc/resolv.conf (the cluster DNS kubelet assigned; loopback and link-local IPv6 are skipped), and the managed-process tier uses its own pod's the same way. With no usable nameserver the runtime plane refuses to start and names this value. Set it only to pin a different resolver, as <ip>:53 or [<ipv6>]:53; 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.renderRuntimeClass | false | Render a self-managed Kata RuntimeClass. Keep false on AKS Pod Sandboxing, where AKS owns it. Whether MicroVM is ready is inferred from successful RuntimeClass discovery and a compatible Ready, uncordoned node. |
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. |
Marimo runtime sizes
Each organization picks a size for notebook editors (default small) and for published
apps (default xsmall) in Settings → Compute. On Kubernetes a size sets only the marimo
container's resources; sidecars and ephemeral storage are fixed:
| Size | CPU limit | CPU request | Memory |
|---|---|---|---|
xsmall | 250m | 250m | 512Mi |
small | 1 | 250m | 1Gi |
medium | 2 | 500m | 4Gi |
large | 4 | 1 | 8Gi |
Lowering maxSizeTier takes effect when the Runtime Plane restarts with the new value, and
that restart stops every runtime running above the new cap: each one ends with the failure
code size_tier_above_ceiling and its resources are removed. Runtimes at or below the cap
keep running. The first start of this release also rebuilds the plane's runtime registry
table once, keeping its rows, so it can record that code. A size that no node can schedule is not refused
up front: the pod stays pending until provisionTimeoutSeconds elapses and the attempt
fails as a startup timeout. Keep maxSizeTier at or below what your Kata node pool can
place.
Lower maxSizeTier only after API Platform and Analytics both run this release. The
restart that stops a runtime above the cap records the stop as an audit event through API
Platform, and an older API Platform rejects the new size_tier_above_ceiling reason. The
plane does not start until that event is recorded, so it fails at startup and keeps
restarting until API Platform is upgraded. An older Analytics also cannot read a runtime that
ended with that code.
When upgrading, roll out the Runtime Plane before Analytics. Analytics sends a size
only to a plane that reports size support. If Analytics is upgraded first, runtimes
started in the meantime run at medium, and after the plane upgrade the next request
for each of them, now at the organization's size, can fail once with an intent conflict.
Upgrading Analytics also changes every runtime's request key once, because the
organization's size policy is part of it.
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:
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 is omitted on purpose: each pod uses the cluster DNS in its own
# /etc/resolv.conf. Set it only to pin a different resolver ("<ip>:53").
isolation:
microvm:
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.
Shared managed Valkey — events, model cache, and Agentic rate limits
The chart ships a durable event backbone (committed workflow-editor synchronization, organization event streams), a model-discovery cache, and durable Agentic webhook/form rate limits. They share one bundled persistent Valkey instance; the rate limiter does not deploy a second database.
| 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. |
agentic.rateLimit.stateStore.managedValkey | true | Durable, replica-safe webhook/form token buckets. Set false to use an operator-managed state.redis component. |
agentic.rateLimit.stateStore.metadata | [] | Dapr state.redis metadata; required and passed through when managedValkey: false. |
Upgrading an installation without managed Valkey: 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 and Agentic rate-limit state store.
networkPolicies.providerEgress — outbound HTTPS to data sources
Data sources and connection endpoints call the provider address an organization admin
configures: connection-endpoint tools from api-platform, and every data source — poll,
on-demand, stream — from api-ingestion. With networkPolicies.enabled, this rule lets those
two services — and no other — reach public HTTPS addresses. It never allows private (RFC 1918),
carrier-grade NAT, link-local or loopback addresses, Azure's platform address
168.63.129.16, or any other special-purpose range; in IPv6 it admits global unicast
(2000::/3) only. Which host a connection may reach is still decided for each connection by
the organization's egress allowlist.
| Key | Default | What / why |
|---|---|---|
networkPolicies.providerEgress.enabled | true | false removes the rule. The NATO UNCLASSIFIED profile sets false; admit reviewed provider ranges there by setting it back to true with your own ipBlocks. |
networkPolicies.providerEgress.ports | [443] | Destination ports. |
networkPolicies.providerEgress.ipBlocks | 0.0.0.0/0 and ::/0, non-public ranges excepted | Kubernetes ipBlock entries. Each except must lie inside its cidr and use the same address family (IPv4 or IPv6); the chart refuses an entry that mixes them. Add your pod and service CIDRs to the IPv4 except when they lie outside the private and carrier-grade NAT ranges. Replace the list to narrow the rule, for example to the ranges your firewall allows. |
Each address family is rendered as its own NetworkPolicy. Azure Network Policy Manager
supports IPv4 only and rejects a policy that contains an IPv6 range, so on those clusters
only the IPv6 policy is rejected and everything else applies. Remove the ::/0 entry there.
networkPolicies.telemetry — your OpenTelemetry collector
If you run an OpenTelemetry collector, dapr.tracing.otel.endpointAddress and
observability.otlpHttpEndpoint point Scrydon at it. A collector in a namespace the chart does
not manage is reachable only through this route, because neither default-deny policy admits
that namespace. Without the route, a workload that a NetworkPolicy isolates cannot export
telemetry. With networkPolicies.enabled: false, that workload is marimo-document, whose own
policy always renders. With networkPolicies.enabled: true, it is every Dapr workload. The
same applies to your scraper's collection of daprd metrics on dapr.metricsPort.
networkPolicies:
telemetry:
collectors:
- namespace: observability
podSelector:
app.kubernetes.io/component: otel-collector
ports: [4317, 4318] # OTLP gRPC (daprd), OTLP HTTP (applications)
metricsScrapers:
- namespace: observability
podSelector:
app.kubernetes.io/component: otel-collector| Key | Default | What / why |
|---|---|---|
networkPolicies.telemetry.collectors | [] | Collector pods that Dapr workloads may send OTLP to, each named by namespace, a non-empty podSelector and at least one port. |
networkPolicies.telemetry.metricsScrapers | [] | Scraper pods that may read dapr.metricsPort on Dapr workloads, each named by namespace and a non-empty podSelector. |
The route opens only the directions you configure. It selects only pods that another chart policy already isolates, so turning it on cannot restrict a workload that was unrestricted. The NATO UNCLASSIFIED profile leaves both lists empty.
global.egress.privateNetworks — private networks
A connection endpoint (a poll, on-demand or stream source whose URL comes from its connection)
reaches public addresses on port 443 only, and a query source's database is reachable through no
network at all. Two more kinds of address an organization admin configures are called from
Scrydon's own servers: a Log Receiver URL, and an
extension's OAuth provider endpoints (its sign-in, token and user-info URLs, which the
organization's provider settings can put on its own host). Both reach public https:// addresses
only. All of them stay there until you, the operator, open a private network. A private network
names address ranges, the ports on them, what may use them and which organizations may:
| Field | What / why |
|---|---|
name | Lower-case words and dashes. Shown in the audit log and in readiness. |
cidrs | The ranges. Never the cluster's own ranges (runtimePlane.blockedCidrs), and never a range holding loopback, link-local, metadata, shared (CGNAT), documentation or reserved addresses. |
ports | The ports on those ranges. There is no global port list: each network carries its own. |
uses | http for connection endpoints, Log Receivers, OAuth provider endpoints and extensions that call self-hosted services (Ollama, vLLM, OpenAI-compatible, SigNoz, the HTTP Request tool); query for databases behind the query gateway. |
organizations or allOrganizations: true | Exactly one. A network opens only for the organizations it names; without it any organization could allowlist the same name and reach your network. |
caRef (http) | A key of global.egress.trustAnchors.existingSecret. A socket into this network verifies the server against those trust anchors only; public endpoints keep the system roots. Without it the system roots apply. |
trustedCaRefs (query, required) | The driver trust anchors (keys of the query driver profiles' trustedCaPaths) a connection into this network may name. A connection cannot pick another. |
| Key | Default | What / why |
|---|---|---|
global.egress.privateNetworks | [] | The networks, above. |
global.egress.trustAnchors.existingSecret | "" | A Secret with one PEM file of trust anchors per caRef, mounted read-only into api-platform and api-ingestion. Rotating them is a rollout. |
Only the operator opens a network: no organization setting widens one. For a connection endpoint or a query source the organization still decides for itself as well: its egress allowlist must name the host (an IP address exactly), and a network in a private range also needs its Private networks switch (Settings → Governance → Egress). A Log Receiver or an OAuth provider setting needs only a network opened to its organization. Loopback, link-local, metadata and the other reserved addresses can never be opened, and every address a name resolves to is checked again each time a source connects, on each delivery and on each provider call. With no network, Log Receivers and provider endpoints stay public-only.
With networkPolicies.enabled, the chart renders one NetworkPolicy per workload and address
family: each network's ranges on its own ports, for api-platform and api-ingestion when it
serves http, and for api-table when it serves query. The broad HTTPS rule
(providerEgress) and the
managed-execution tier exclude the private ranges and runtimePlane.blockedCidrs together, so
narrowing the floor never opens private space outside a network.
Narrow runtimePlane.blockedCidrs first. Its default is all of RFC 1918, which overlaps any
private network, and api-platform, api-ingestion and api-table refuse to start while a
network overlaps it — the error names the network and the floor range. Set it to the cluster's
pod, service and node ranges. That list also bounds notebooks: for an organization with
Private networks on, a notebook can reach private space you no longer list there, as before.
queryJdbc.remoteCidrs and queryJdbc.remotePorts were removed: the render fails while either is
set. Move each database range to a network with uses: [query], its ports and trustedCaRefs.
The query gateway requires at least one such network, and each trustedCaRefs entry must be
distributed by a query driver profile.
Worked example: Azure Private Link on AKS
An AKS cluster with Azure CNI overlay: VNet 10.224.0.0/12, nodes 10.224.0.0/16, pods
10.244.0.0/16, services 10.0.0.0/16 (read yours with
az aks show -g <rg> -n <cluster> --query networkProfile). Contoso's private endpoints live in
subnet snet-pe, 10.225.8.0/24, in the same or a peered VNet — not a pod, service or node range.
The identity provider every organization signs in against sits in 10.225.9.0/28.
runtimePlane:
# The floor: the cluster's own ranges. 168.63.129.16 is always refused already.
blockedCidrs: [10.224.0.0/16, 10.244.0.0/16, 10.0.0.0/16]
global:
egress:
trustAnchors:
existingSecret: scrydon-egress-trust # key: contoso-internal
privateNetworks:
- name: contoso-pg
cidrs: [10.225.8.0/28]
ports: [5432]
uses: [query]
organizations: [<contoso organization id>]
trustedCaRefs: [azure-db]
- name: contoso-api
cidrs: [10.225.8.16/28]
ports: [443, 8443]
uses: [http]
organizations: [<contoso organization id>]
caRef: contoso-internal
- name: corporate-idp
cidrs: [10.225.9.0/28]
ports: [443]
uses: [http]
allOrganizations: true- Database (Azure Database for PostgreSQL, private endpoint). The organization admin enters
the server's public name,
contoso-pg.postgres.database.azure.com. Inside the cluster, CoreDNS forwards to Azure DNS, and theprivatelink.postgres.database.azure.comzone linked to the VNet answers10.225.8.4; the driver verifies the certificate against the name that was typed. Never enter theprivatelink.name: the server's certificate does not carry it. - HTTP API (Private Link Service). Contoso publishes its API behind a Private Link Service; its
private endpoint gets
10.225.8.20. Add an A record forapi.contoso.internalin a private DNS zone linked to the VNet, and put the CA that signs the API's certificate in the trust-anchor Secret undercontoso-internal. - Log Receiver. Contoso's SIEM collector has a private endpoint in the same range,
10.225.8.21, with a certificate from Contoso's internal CA. Contoso's admin saveshttps://siem.contoso.internal:8443/ingestas a Log Receiver (a private DNS zone linked to the VNet answers10.225.8.21). A Log Receiver needs no allowlist entry and no switch. - Identity provider. Any organization can set its extension's identity-provider domain to the
IdP's host, which
corporate-idpopens to every organization. - The organization. Turn on Private networks and add
contoso-pg.postgres.database.azure.comandapi.contoso.internalto the egress allowlist.
AWS PrivateLink (interface endpoints in your subnets) and Google Private Service Connect (a consumer-subnet address; drop the trailing dot from the DNS name) work the same way: the endpoint's subnet is the network, and the cluster's own ranges stay in the floor. On-premises and air-gapped installs list the database's, API's, collector's or IdP's own subnet.
ingestBuffer — durable buffer for stream and push sources
Stream and push data sources accept input into a dedicated buffer before it is committed to
its table, and a stream's lease is held there too; neither runs without it. Poll sources do not
need it: the lease that keeps each one on one replica is held in the platform's database.
ingestBuffer.enabled (default false) turns it on. By default the chart runs it
as a single in-cluster Valkey that writes every accepted input to disk before answering
(ingestBuffer.existingSecret and ingestBuffer.tls.existingSecret are then required). To
use a managed Redis or Valkey service instead, point ingestBuffer.external at it:
| Key | Default | What / why |
|---|---|---|
ingestBuffer.external.urlSecret | "" | Name of a Secret with key url: rediss://[user]:password@host:port. TLS and a password are required. Setting it replaces the in-cluster Valkey for every component that uses the buffer. |
ingestBuffer.external.caSecret | "" | Name of a Secret with key ca.crt, only when the service uses a private certificate authority (Google Memorystore issues one per instance). |
ingestBuffer.external.durability | replicated | replicated: input is accepted only once every replica of the service has acknowledged it. aof-always: a single self-managed server that writes every input to disk; managed services cannot run it. |
ingestBuffer.external.replicas | 1 | The number of replicas the service runs. The buffer checks it against what the service reports. |
ingestBuffer.external.cidrs | [] | Required with networkPolicies.enabled: the address ranges the service's endpoint resolves to, such as its private endpoint's subnet. The two services that use the buffer may reach these ranges, and nothing else may. |
ingestBuffer.external.port | 6379 | With networkPolicies.enabled: the port in the url. Azure Managed Redis uses 10000, Memorystore for Redis 6378, ElastiCache and Memorystore for Valkey 6379. |
The service must be non-clustered and use maxmemory-policy noeviction, and for
replicated it needs at least one replica. Supported configurations: Azure Managed Redis with
the Non-clustered policy and high availability; Amazon ElastiCache with cluster mode disabled
and Multi-AZ; Google Memorystore for Redis (Standard tier) or Memorystore for Valkey with
cluster mode disabled; or your own Valkey or Redis 6.2+ with replicas. Azure Cache for Redis
(Basic, Standard, Premium), ElastiCache Serverless, Amazon MemoryDB and any cluster-mode
service are not supported. The services that consume the buffer refuse to start, with a
message naming the setting to change, when the service does not meet these requirements.
Switching an installation from the in-cluster buffer to an external one does not move
input that is still waiting to be committed. Switch while no stream or push source has
pending input. With networkPolicies.enabled, the chart refuses to render until
ingestBuffer.external.cidrs is set.
apiIngestion — the ingestion service
Every data source that fetches or receives data runs in its own service, api-ingestion: a
person's on-demand fetch and file upload, and poll, stream and push sources. It is deployed on
every installation (while dapr, auth and platform keep their defaults), and runs poll
sources on every installation. Stream and push sources also need the
ingest buffer: with
ingestBuffer.enabled off, api-ingestion runs poll sources, on-demand fetches and uploads, and
the push URL is not published. It keeps no database and stores no credentials: the platform issues
it the credential of a source's connection for each run, connection attempt or delivery, and it
holds that credential only for that attempt.
| Key | Default | What / why |
|---|---|---|
apiIngestion.enabled | true | Set false to run no data source that fetches or receives data: on-demand, upload, poll, stream or push. Data-source readiness then says so for each source. |
apiIngestion.replicas | 2 | Every replica accepts every push delivery, so two keep push available during an upgrade. A stream or a poll source runs on one replica and moves to another when that one stops. |
apiIngestion.autoscaling.* | disabled, 2–6 | Optional CPU-based autoscaling. Streams already running do not move to new replicas. |
apiIngestion.gateway.enabled | true | Publish the push URL. false keeps push private, for installations that only use stream sources. |
apiIngestion.desiredPullIntervalMs | 60000 | How often each replica refreshes the list of sources to run. At most 60000. |
apiIngestion.resources | 256Mi / 100m requested | Not yet sized under load; raise together with replicas. |
The URL a provider delivers to is
https://<routing.host>/api/platform/data-sources/push/<binding id> in subpath mode and
https://<auth.gateway.hostname>/api/platform/data-sources/push/<binding id> in subdomain
mode. gateway.publicAuthRateLimit never applies to it; each source has its own delivery
limit.
The binding id is an address, not a secret. A push source that declares no delivery
authentication (auth.mode: "none") refuses every delivery until an organization admin
opts that source in; after that, anyone who can reach its URL can deliver rows. Prefer
sources that sign or authenticate their deliveries.
What each platform needs — none of these combinations has been validated by Scrydon yet. The
chart's side of outbound HTTPS is networkPolicies.providerEgress:
| Platform | Buffer storage | Edge | NetworkPolicy | Outbound HTTPS to providers |
|---|---|---|---|---|
| AKS | Premium SSD class (global.storageClass) | bundled Traefik or Application Gateway for Containers | Azure NPM, Calico or Cilium | NAT gateway or Azure Firewall |
| EKS | EBS gp3 class | bundled Traefik or AWS Load Balancer Controller | VPC CNI network-policy agent or Calico | NAT gateway |
| GKE | SSD persistent-disk class | bundled Traefik or GKE Gateway controller | Dataplane V2 | Cloud NAT |
| On-premises (kubeadm, k3s), air-gapped | any RWO CSI class with reliable fsync | bundled Traefik or your Gateway API controller | Calico or Cilium (k3s: built in) | a route to each provider's public address; providers on a private network are not supported yet |
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 and never reports a phone-home status. |
license.gracePeriod | 2592000 | No effect. The 30-day expiry grace is fixed in code; the key stays so existing values files still validate. |
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 | {} | No effect. Licenses are verified against the Scrydon signing keys built into each release; the key stays so older values files still validate. |
The license itself is entered in the setup wizard and stored in the platform database; auth.secrets.LICENSE only pre-seeds it. 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 endpoint as your CNI sees it. cidr has no default — the address differs per cluster — and the chart refuses to render while it is empty and the egress is on. Use the ready kubernetes EndpointSlice address rather than the Service ClusterIP, because policy is evaluated after service DNAT. Three pod-selected policies reuse that destination: api-platform is limited by RBAC to get/list on nodes, the PSS labeler to get/patch on exactly the Helm release namespace, and in strict isolation the Runtime Plane controller to its own pods.
kubectl get endpointslice -n default -l kubernetes.io/service-name=kubernetes \
-o jsonpath='{range .items[*].endpoints[?(@.conditions.ready==true)].addresses[*]}{.}{"\n"}{end}'| Cluster | What the command returns | cidr |
|---|---|---|
| AKS, GKE, k3s, kubeadm with one control-plane node | one address | that address with /32 |
| EKS, kubeadm with several control-plane nodes | two or more addresses; a failover moves the API between them | the smallest CIDR that covers all of them |
packSources — chart-managed pack distribution
| Key | Default | What / why |
|---|---|---|
auth.requireSignedExtensionSources | false | Reject extension-source policies with any unsigned-success branch at source CRUD, probe, reconcile, and workspace activation boundaries. Enable for regulated deployments. The key's pre-rename name is still honoured; see Upgrades. |
auth.enforceProcessFlowRequirements | true | Refuse turning a process flow on in a workspace while an extension one of its blocks needs is not installed, connected, or entitled there. Only the first install into a workspace is refused; updates and repairs only report. Set false to log the shortfall without refusing — a kill switch, not a tuning knob. Renders SCRYDON_PROCESS_FLOW_REQUIREMENTS_ENFORCE on api-platform. See Extensions a flow needs. |
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. |
responseCompression — compressed pages
The platform, agentic, analytics, cortex and horizon apps compress their own pages, server-function data and JSON: brotli for a browser that accepts it, gzip otherwise. It works behind any edge, so you do not need compression on your gateway or load balancer for these apps. Event streams and responses under 1 KB are left as they are.
| Key | Default | What / why |
|---|---|---|
responseCompression.enabled | true | false serves every page uncompressed. Use it only if your edge mishandles an encoded response. |
responseCompression.maxConcurrent | 32 | Compressions running at once per pod. Each holds a few MB; over the cap a response is sent uncompressed instead of waiting. |
Sign-in and session answers (routing.paths.apiAuth, and the platform app's /api/auth proxy) are never compressed, because they carry session tokens and a compressed secret can be recovered (BREACH). The platform's proxy marks them Cache-Control: no-transform. If your own edge compresses, keep routing.paths.apiAuth out of it, and honour no-transform.
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 the minimum cluster (8 vCPU / 40 GB across two or more nodes). On a smaller eval cluster, disable the heaviest optional pieces. The three below take the memory the scheduler must reserve from about 24 GiB to about 14 GiB, which fits in roughly 24 GB of node memory:
infra:
starrocks:
enabled: false # 5–8Gi RAM, 20Gi PVC — only Managed Tables + agentic schema-inference affected
apiTable:
enabled: false
analytics:
enabled: false # drops the analytics UI (2 replicas × 1.5Gi) and the marimo-document rendererCommon 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 |
infra.db.credentials.password | Not generated here: the render uses your value as written, or postgres when it is empty. But Postgres keeps the password it was initialised with, so a value that differs from it fails every database connection and migration. Read it out of secrets-infra before you set it |
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. The example ships every
secret as REPLACE-ME-WITH-A-REAL-VALUE-FROM-YOUR-SECRET-STORE, and the render
fails, naming the values path, while any value is still that placeholder.
On an existing install, read the current values out first (the commands are in that file). Inventing new ones rotates live credentials.
agentic.secrets.ENCRYPTION_KEY should be 64 hex characters (generate it
with openssl rand -hex 32). Any other secret of at least 32 characters also
works: api-agentic derives a key from it. The chart fails the render on a
shorter value, and api-agentic refuses to start with one, including when you
manage the secrets-agentic Secret yourself. Leading and trailing whitespace
around a 64-hex key, such as a final newline, is ignored. On upgrade the chart
keeps every older key the app can use, so the key does not change. Only an older
key shorter than 32 characters is replaced with a generated one. Replacing it
invalidates outstanding webhook test tokens. No stored encrypted data is
affected.
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-systemSet both. installControlPlane: false alone leaves the chart assuming Dapr runs in the release namespace, and the install waits on a marimo-document pod whose sidecar times out reaching dapr-sentry. The installer (setup.sh) detects an existing control plane and passes both flags for you. The air-gapped (Zarf) package always installs its own Dapr, so next to an existing one its preflight fails and points you at --backend mirror, which reuses it.
Your 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 edge: behind a TLS-terminating load balancer
App Gateway / ALB / GCP LB / F5 setups need Traefik-controller tuning (trustedIPs, externalTrafficPolicy: Local, the gateway.tls.enabled public-scheme rule, a private edge, 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 gatewayclass traefik # ACCEPTED=True
kubectl get gateway -n scrydon-platform # PROGRAMMED=True, and an ADDRESS
kubectl get httproute -A # one per enabled app, Accepted by the Gateway
kubectl get certificates -A # cert-manager Certificates READY=TrueA Gateway that is Programmed but whose listeners are not Accepted is the case to
look at closely — kubectl describe gateway gateway-frontdoor -n scrydon-platform
prints one condition block per listener.
License claims (tier, entitlements, days-to-expiry) appear under Settings → Organization → License after /setup. Sign in at https://app.example.com/platform 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 | gateway.tls.enabled left false — set it true (see Gateway). |
Gateway listener Accepted=False, reason=PortUnavailable | gateway.listenerPorts.* does not match an entryPoint your controller actually listens on. Traefik binds :8000/:8443 inside the container while its Service publishes 80/443, so the chart defaults to 8000/8443 — a listener on port 80 is refused with no matching entryPoint for port 80 and protocol HTTP. A managed implementation that listens on the public port wants 80/443 instead. See Listener ports. |
helm install fails with gateway.publicAuthRateLimit.enabled=true requires gateway.className=traefik | Rate limiting is not in the Gateway API standard channel, so it is attached as a Traefik Middleware. Set it false and apply the limit at your own edge, or run the traefik class. |
kubectl get gatewayclass returns nothing, or the Gateway never gets an ADDRESS | The cluster has no Gateway API implementation. Install one (traefik.enabled: true brings the CRDs and the traefik GatewayClass), or point gateway.className at the class your platform team runs. |
An app's HTTPRoute is not Accepted by the Gateway | The route's namespace is not in the Gateway's allowedRoutes selector — which the chart derives from the enabled apps' namespaces. Re-run helm upgrade after any namespaces.* change so the selector and the routes move together. |
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 renewed license file under Settings → Organization → License → Update license; no restart. Licensing.
- Backup & restore — bundled Postgres CronJob (
infra.db.backup) or your managed provider. Backup & Restore. - Upgrades — Upgrades.
- Observability — Observability.