Skip to content

3. Configure & install Helm

Owner: Cloud Ops · Time: ~45 min
You’ll finish with: the application chart installed, every pod Ready, and authentication configured against your Entra tenant.
Before you start: provider overlay from Step 2 saved in ~/arbium-install · tenant ID and agent client ID from the walkthrough’s Entra steps · GHCR credential.

This step is identical on AWS, Google Cloud, and Azure.

Every installation uses three layers, in this order:

  1. The chart’s built-in values.yaml defaults.
  2. One cloud preset: values-aws.yaml, values-gcp.yaml, or values-azure.yaml.
  3. Two untracked overlays that you own:
    • <environment>.<provider>.values.local.yaml from Step 2 for cloud wiring.
    • <environment>.values.local.yaml from this page for application, Entra, and optional fleet-console configuration.

Later files win. Keep cloud-specific values in the provider overlay and shared application values in the final overlay.

  • The application services and the embedding service.
  • Database migrations, run automatically before the application starts.
  • Scheduled background jobs, run by a bundled workflow controller (Argo Workflows). A first install applies its custom resource definitions automatically; an upgrade from a release that predates it applies them by hand first, as in the “Upgrading from the CronJob engine” section of the chart’s INSTALL.md.
  • ConfigMaps, Services, service accounts, and optional network policies.
  • External Secrets resources that read your cloud secret store.
  • The shared Ingress routes.
  • Optional SCIM provisioning and the fleet console.
  • Optional HPA, PDB, and telemetry components.

Terraform owns cloud infrastructure; Helm owns everything inside Kubernetes.

Use the GHCR credential supplied by Arbium:

Terminal window
echo '<github-package-token>' | helm registry login ghcr.io \
--username <github-username-or-bot-account> \
--password-stdin
mkdir -p ~/arbium-install
cd ~/arbium-install
helm pull oci://ghcr.io/try-caret/charts/chaindb \
--version <release-version> \
--untar --untardir chaindb-<release-version>

The chart is unpacked into chaindb-<release-version>, so a release you pull later never lands on top of this one.

The chart’s appVersion selects the matching runtime image tags. Do not override image repositories or tags unless you mirror images into ECR, Artifact Registry, or ACR.

Save this as ~/arbium-install/<environment>.values.local.yaml beside the provider overlay. Substitute your HTTPS host and Entra values. Keep this file out of source control.

global:
namespace: arbium
environment: <environment>
publicBaseUrl: https://arbium.<your-domain>
config:
deployment: selfHosted
edgeFnsUrl: https://arbium.<your-domain>
edgeFnsAnonKey: self-host-local
verifyJwt: "true"
auth:
method: oidc
label: Customer SSO
publicBaseUrl: https://arbium.<your-domain>
provisioningMode: signon
oidcIssuerUrl: https://login.microsoftonline.com/<entra-tenant-id>/v2.0
oidcAudience: <arbium-agent-client-id>
oidcAllowedAlgs: RS256
oidcClientId: <arbium-agent-client-id>
oidcScopes: openid,email,profile,offline_access
oauthRedirectUrl: caretcapture://auth-callback
embedder:
enabled: true
mode: http
replicaCount: 1
scheduler:
enabled: true
migrations:
runOnStartup: true
admin:
enabled: false
scim:
enabled: false
telemetry:
sentry:
enabled: false

Sentry is opt-in. When telemetry.sentry.enabled is true, chart-managed Secrets must also set telemetry.sentry.dsn; Helm fails the render otherwise. External-Secret installs must provide a SENTRY_DSN key in the shared runtime Secret. CaptureLake, the edge functions, roots-api, Admin, SCIM, the embedder, and the LLM proxy send error events. CaptureLake sends scheduled-job check-ins and aggregate backlog and freshness metrics; CaptureLake, the edge functions, the embedder, and the LLM proxy send sampled latency metrics, while roots-api sends unsampled MCP latency metrics. The Roots desktop app can send main-process JavaScript crashes, but not renderer events, sessions, screenshots, or native dumps. Set telemetry.sentry.desktopDsn to the Sentry DSN it should report to, and the deployment hands it to every device the next time the app starts, with nothing to change in your device management. With the relay and ingress both enabled, devices send through this deployment’s own address instead of directly to Sentry. Request data, user identity, the transaction name, and free-form tags are removed, as are console breadcrumbs and the query string of any outgoing URL recorded as a breadcrumb. Exception messages, stack-frame source context and the remaining breadcrumbs are kept: an error stripped of its message cannot be acted on. An error message can therefore quote the text that caused it, which may include captured content. Tracing and Sentry console-log forwarding are disabled.

Organizations are always enabled. Every self-hosted startup runs organization setup before the service becomes ready. With a write-capable signed license and valid tenant-scoped OIDC configuration, it creates one organization using the license’s customer name. Restarts reuse it and reconcile licensed capacity; later license-name changes do not rename it. There is no opt-out flag.

The first eligible person who signs in through the Admin dashboard’s configured OIDC application claims the administrator role. Restrict application assignment and, when configured, require the initial administrator group. Claiming requires a write-capable license and a fresh Admin sign-in. Automated uploads and normal product sign-on cannot claim this role.

Upgrade note: organizations and membership-based sharing are no longer optional

Section titled “Upgrade note: organizations and membership-based sharing are no longer optional”

Organization setup and membership-based memory sharing are always enabled. Eligible active members can search their organization or teams within administrator search permissions. Raw screen content and profiles remain personal. People without eligible membership retain personal memory (including analysts), unless administrator policy denies access; explicit shared searches require eligible membership. Automatic organization setup does not add memberships for existing sign-on users, and matching email domains and ordinary sign-on do not grant shared access.

Starting with chart 0.6.66, remove rootsApi.env.CHAINDB_ORGANIZATION_SHARING_ENABLED from complete values files and any manually configured environment. It is no longer read: an old "false" does not keep sharing off. There is no deployment opt-out. Verify membership coverage, administrator search permissions, privacy/access disclosures and any required user notice before upgrading. Administrator permissions can restrict API access to personal or team searches; a no-access rule continues to deny all tools even when sharing membership is unavailable. These API permissions do not change older stored-memory SQL policies.

Upgrading also changes database-backed access to older stored memory. By default, the chart applies all pending database migrations before starting upgraded services. If the structured-membership migration has not yet been applied, older stored-memory sharing changes from matching email domains to active organization memberships. Installations already on 0.6.65 retain those policies; this release removes the API rollout flag without changing migration SQL. There is no switch to defer only the policy change. migrations.runOnStartup=false skips all startup migrations and is not a safe per-change gate; it does not undo changes already applied. Approve coverage, privacy/access disclosures, user notice and migration ordering before the upgrade.

Remove config.organizationsEnabled from your values and CHAINDB_ORGANIZATIONS_ENABLED from any manually configured environment. Neither setting is read anymore; leaving an old false does not disable organizations. Upgrade with complete values files, not --reuse-values.

Before upgrading, check config.auth.method: oidc, a valid external HTTPS config.auth.oidcIssuerUrl (no whitespace, at most 2,048 characters, not a built-in /auth/v1 issuer), the correct audience and supported asymmetric algorithms (RS256 or ES256, not HS256). Check Admin application assignment/group restrictions. For an installation without an organization, the write-capable signed license needs a non-blank customer name of at most 200 characters and a positive integer user limit no greater than 2,147,483,647. For an existing organization with memberships, retain its mapped issuer.

Setup runs before any application requests are served. Invalid configuration prevents the whole main service from becoming ready, not just the Admin dashboard; its startup log supplies a sanitized failure reason without printing credentials or license values. Fix that setting before retrying. Without a write-capable license, no organization is created and Admin access remains unavailable.

Shared searches now require eligible active memberships in the same active organization, not matching email domains. Until memberships are provisioned, people retain personal memory only, including analysts; explicit team and organization searches are refused. A search permission of none still denies all tools. Claim the first administrator through the Admin dashboard, then provision memberships through your trusted SCIM connection or explicit authorized invitations/operator actions. Normal sign-on does not grant membership. For SCIM installations, verify membership coverage after the post-claim reconciliation. This upgrade does not infer memberships from email addresses or transfer ownership of previously recorded activity.

The OIDC issuer must use your Entra tenant and the v2 endpoint. The audience and client ID are the bare Arbium Agent application GUID — not api://<client-id>, not an object ID. IT sets requestedAccessTokenVersion = 2 during app registration. oauthRedirectUrl is the desktop app’s own return address; IT registers it on the Arbium Agent app under Mobile and desktop applications. oidcScopes is what the deployment publishes at /functions/v1/auth-config and what the Windows desktop app asks Entra for; IT grants admin consent to exactly these. The Windows desktop app needs no Application ID URI or exposed scope — its token’s audience is the client ID itself. After the first administrator joins, changing the configured issuer prevents startup. Restore the prior issuer until an explicit audited recovery workflow is available.

The publishable edgeFnsAnonKey is not an administrative secret. The scheduler, enrollment, JWT, license, registry, SCIM, and Admin OIDC values remain in the cloud secret store configured by the provider overlay.

Use this when you want an explicit Entra group allowlist for agent access. The provider overlay must map scim-bearer to the SCIM token in the cloud secret store.

config:
auth:
provisioningMode: scim
scim:
enabled: true
existingSecret: scim-bearer

Who can use Arbium and who is an admin are not Helm values. Once the release is installed, set them once from inside the cluster. Until you do, nobody can sign in or capture:

Terminal window
cat > access-groups.json <<'EOF'
{"spec":{"containers":[{"name":"psql","image":"postgres:16-alpine",
"command":["psql","$(DATABASE_URL)","-v","ON_ERROR_STOP=1","-c",
"select * from public.set_access_groups(array['<users-group-object-id>'], array['<admins-group-object-id>'], '<your name>')"],
"env":[{"name":"DATABASE_URL","valueFrom":{"secretKeyRef":{"name":"chaindb-runtime","key":"DATABASE_URL"}}}]}]}}
EOF
kubectl -n arbium run access-groups --rm --attach --restart=Never \
--image=postgres:16-alpine --overrides="$(cat access-groups.json)"

chaindb-runtime is the chart’s default runtime Secret; if your values point DATABASE_URL at another Secret, use that name instead.

List several groups as array['<id>', '<id>']. Use array[]::text[] for the admin groups until you enable the fleet console. The command prints the groups now in effect. After that, an admin changes them in the dashboard under Organization → Access.

Give IT the Tenant URL https://arbium.<your-domain>/scim/v2 and the SCIM bearer token through the approved secure channel. Entra provisioning must map SCIM externalId from the Entra objectId for both users and groups.

The fleet console shares the same hostname and is served under /admin. It requires SCIM provisioning and at least one admin group, and the provider overlay must map the scim-bearer Secret from the selected cloud secret store.

Current dashboard releases sign in through the Arbium Agent app as a public client: IT adds the console’s callback, https://arbium.<your-domain>/admin/api/auth/oidc/callback, to that registration in the walkthrough’s Configure sign-in step. Chart versions that still use a separate Arbium Admin registration also need the admin-ui-oidc Secret and the Web redirect URI below.

config:
auth:
provisioningMode: scim
scim:
enabled: true
existingSecret: scim-bearer
admin:
enabled: true
existingSecret: admin-ui-oidc
entra:
tenantId: <entra-tenant-id>
adminGroupId: ""
authz:
mode: scim

For chart versions with a separate Arbium Admin registration, its Web redirect URI must be exactly:

https://arbium.<your-domain>/admin/api/auth/callback

If you set the groups before enabling the console, run the set_access_groups command above again with the admin group listed.

The chart generates and persists the console’s session secret through External Secrets. Do not put the Admin client secret or a session secret in the values file.

CPU mode works on every supported cloud and is the baseline for small installs:

embedder:
enabled: true
gpu:
enabled: false
resources:
requests:
cpu: "1"
memory: 2Gi
limits:
cpu: "2"
memory: 4Gi

For AWS or Google Cloud, enable GPU mode only after the provider guide’s GPU node pool is ready. The cloud preset supplies the provider-specific selectors and tolerations:

embedder:
enabled: true
gpu:
enabled: true

The Azure baseline is CPU-only. Add and validate an AKS GPU pool before enabling GPU mode there.

Set <provider> to aws, gcp, or azure and use the same three files for lint, render, and install. For a later upgrade, use the provider file from the release you install:

Terminal window
helm lint ./chaindb-<release-version>/chaindb \
--namespace arbium \
-f ./chaindb-<release-version>/chaindb/values-<provider>.yaml \
-f <environment>.<provider>.values.local.yaml \
-f <environment>.values.local.yaml
helm template arbium ./chaindb-<release-version>/chaindb \
--namespace arbium \
-f ./chaindb-<release-version>/chaindb/values-<provider>.yaml \
-f <environment>.<provider>.values.local.yaml \
-f <environment>.values.local.yaml \
> /tmp/arbium-rendered.yaml

Review the rendered resources before applying. Confirm:

  • Every image is versioned for the intended release.
  • The External Secrets provider matches your cloud.
  • The database connection method matches the provider guide.
  • ingress.host, global.publicBaseUrl, and the Entra Admin redirect host agree.
  • OIDC uses the v2 issuer, bare client GUID, and RS256.
  • Workloads reference the ghcr-pull image pull Secret.
  • The rendered OIDC_ISSUER_URL, OIDC_AUDIENCE, and OIDC_CLIENT_ID values are non-empty.
  • Optional SCIM and fleet-console resources are either fully configured or disabled.
Terminal window
helm upgrade --install arbium ./chaindb-<release-version>/chaindb \
--namespace arbium \
--create-namespace \
--timeout 15m \
-f ./chaindb-<release-version>/chaindb/values-<provider>.yaml \
-f <environment>.<provider>.values.local.yaml \
-f <environment>.values.local.yaml

The chart creates the arbium namespace. External Secrets then materializes the runtime and GHCR pull Secrets. Pods can briefly report ImagePullBackOff while the pull Secret is being created; Kubernetes retries automatically.

Watch the rollout:

Terminal window
kubectl get externalsecret -n arbium
kubectl get pods -n arbium -w
kubectl rollout status deployment/chaindb-edge-fns -n arbium --timeout=15m

Read the ALB hostname and point your DNS record at it:

Terminal window
kubectl get ingress chaindb -n arbium \
-o jsonpath='{.status.loadBalancer.ingress[0].hostname}{"\n"}'

Use a Route 53 alias or a CNAME. The ACM validation record from Step 2 remains separate from this application record.

DNS should already point to the reserved static address. Wait for the Google managed certificate:

Terminal window
kubectl get managedcertificate chaindb-tls -n arbium -w

After it becomes Active and HTTPS works, change kubernetes.io/ingress.allow-http to "false" in the GCP provider values and run the Helm command again.

Apply the prepared Certificate after the namespace exists:

Terminal window
kubectl apply -n arbium -f manifests/tls-certificate.yaml
kubectl get certificate chaindb-tls -n arbium -w

The certificate should become Ready=True after DNS resolves and Let’s Encrypt completes HTTP-01 validation.

Terminal window
kubectl -n arbium get configmap chaindb-config \
-o jsonpath='{.data.OIDC_ISSUER_URL}{"\n"}{.data.OIDC_AUDIENCE}{"\n"}{.data.OIDC_ALLOWED_ALGS}{"\n"}'

Expected: your v2 issuer URL, the Arbium Agent client GUID, and RS256. If they differ, correct the values file and run helm upgrade again.

Migrations run through the version-matched chaindb-migrations init container: pending migrations apply before each application pod starts, and versions already recorded in schema history are skipped. No separate migration Job is required.

migrations.workloads lists which deployments gate their startup on that run — by default edge-fns, scim, admin-ui and roots-api. A listed workload will not start while the database or the migrations image is unavailable, so if a pod is wedged in Init during an outage, check its migrations container first. Drop a name to let that workload start without confirming the schema.

If an approved external pipeline owns migrations, set:

migrations:
runOnStartup: false

Run that pipeline before the upgraded services start. From the first release whose migrations include 20261023210000_mcp_token_revocations, the API checks a database table on every connector request (it is how a connector token is withdrawn), so with startup migrations off, or with roots-api removed from migrations.workloads, connector requests answer “Could not check access right now” until the migrations have run. Most connector clients retry on their own; some may ask people to sign in again. Apply the migrations first to avoid the gap.

Authenticate to GHCR, pull the new chart into a new directory, review the release notes and values changes, then repeat the render and install commands with the same overlays. Always pin the new chart version; never track latest.

Upgrading a release that predates the bundled workflow controller. Helm installs a chart’s custom resource definitions only on the first install, never on an upgrade, so apply them by hand from the new chart before the helm upgrade, or the upgrade fails with no matches for kind "CronWorkflow":

Terminal window
kubectl --kubeconfig <path> --context <name> apply --server-side --force-conflicts \
-f ./chaindb-<release-version>/chaindb/crds/

Then follow the “Upgrading from the CronJob engine” section of the chart’s INSTALL.md: it also covers the values to remove, the namespace the release must be installed into, and the moment to pick so no scheduled job is running.

The chart makes each serving pod keep answering for 10 seconds after Kubernetes starts to stop it, so new requests that are still being routed to the old pod during a rolling update are not refused, and gives it 40 seconds in total to finish what it is doing. The first upgrade to a version with this setting still drops some calls, because the pods it replaces do not have it yet. Clients that keep connections open may still see a reset when the old pod closes idle connections. A rollout is not slower unless the cluster is short of capacity, because each old pod keeps its CPU and memory for up to those 10 seconds plus its shutdown; a node drain waits for it. This needs Kubernetes 1.30 or newer. Tune it with gracefulStop.preStopSleepSeconds and gracefulStop.terminationGracePeriodSeconds (one setting for all pods). The 10-second sleep must be longer than your load balancer takes to stop sending new requests to a stopping pod, and the load balancer’s connection draining (backendConfig.drainingTimeoutSec on Google Cloud; once you enable backendConfig the chart sets it to 40 seconds, until then GKE’s own default applies) should be at least the 40-second total, so that requests already in flight can finish.

Check the release before and after the upgrade

Section titled “Check the release before and after the upgrade”

When the release check is set up (see Run the release check, which also shows how to create the check account), wrap the upgrade with it:

  1. Run the check before the upgrade. It must pass. If it fails, do not upgrade: a failing starting point hides what the upgrade changes. Fix the cause first.

  2. Note the revision that is deployed now, from helm history arbium -n arbium.

  3. Run the upgrade as above.

  4. Run the check again. If it fails, run it once more. If it fails twice in a row with exit 3, 4, 5, 6 or 7 (a real failure; exit 2 and 8 are not a broken release, see the exit codes in Read the result; on the Argo path, a Failed run whose result line lists a failed part that is not "judged":false), roll back to the revision you noted and run the check a third time to confirm that the previous release serves again:

    Terminal window
    helm rollback arbium <revision> -n arbium --wait --timeout 20m

If you run the check as a Helm test, helm test runs the test of the revision that is deployed, so the run before the upgrade uses the old chart’s check. Renew the connector token first if it has less than 14 days left.

A rollback puts back the previous chart’s workloads and settings. It does not reverse changes the new release made to the database. The first upgrade that turns the check on has no “before” run, because the check arrives with that upgrade.

Uninstall the application before destroying the provider foundation:

Terminal window
helm uninstall arbium -n arbium
kubectl delete namespace arbium

Continue to 4. Verify deployment.