Skip to content
M↓ View as Markdown ↗

Install on Kubernetes with Helm

Install Actian AI Analyst in a Kubernetes cluster you run, using the Helm chart from the Release bundle. The images come from the bundle through your own container registry, and you run every dependency (database, sign-in, object storage, language model) yourself.

The shipped example values and these steps cover an air-gapped install: a cluster with no route to Actian or any other vendor. For a single host without Kubernetes, use Install with Docker Compose instead.

Availability

  • Who can do this: the operator who runs the Kubernetes cluster, with permission to create namespaces, Secrets and Helm releases, and to push to your container registry.
  • Deployment: Self-managed deployments only.

Run every command below from the extracted bundle directory (ai-analyst-<VERSION>/), on a host that can reach your private registry and the cluster.

Check your bundle version

The chart-managed Keycloak (keycloak.enabled) and private CA (server.extraCaCerts) options are recent additions to the chart. If your bundle's helm/values-airgap.example.yaml has no keycloak: section, your chart does not include them. Run your own Keycloak instead, and ask Actian which bundle version adds them.

What you run beside the chart

Dependency Required Notes
Postgres 16 with the vector and pg_trgm extensions Yes The product database: one app database, reached through DATABASE_URL.
Keycloak 26 Yes Sign-in, members and organizations. The chart can deploy it (keycloak.enabled: true), or you can use a Keycloak 26 with Organizations that you already run. An air-gapped deployment refuses vendor-hosted sign-in, so Keycloak is required.
S3-compatible object storage Yes For example MinIO or Ceph RGW. Set STORAGE_DRIVER=s3 and an explicit S3_ENDPOINT. An air-gapped deployment refuses the default AWS endpoint.
An ingress controller Yes You route traffic to the chart's Services yourself. See Route your ingress.
An OpenAI-compatible chat API with tool calling Recommended The deployment default LLM endpoint. Without it, the server still starts, but every conversation is refused with Chat needs an LLM endpoint until an organization adds its own endpoint under Settings → Inference. No model ships with AI Analyst. See Configure inference.
An OpenAI-compatible embedding API Optional Without it, semantic-layer search is lexical only.
An SMTP relay Optional For Scheduled Insights and notifications.

Step 1: Verify the bundle

./verify.sh

verify.sh checks the signature on SHA256SUMS, every file's checksum and each artifact's own signature, offline, using openssl. Compare the public-key fingerprint it prints with the one Actian publishes for the release. See Release bundle.

Step 2: Push the images to your registry

The chart needs these images from the bundle:

Image When
server, web Always.
postgres (pgvector) For the database bootstrap hook (dbBootstrap.enabled, on by default).
jdbc-bridge Only with jdbcBridge.enabled: true (Actian-family warehouses).
keycloak Only with keycloak.enabled: true.

images.txt lists each image and its tarball. With a Docker daemon:

REGISTRY=registry.example.internal
while read -r name image archive; do
  case "$name" in caddy) continue ;; esac
  docker load -i "$archive"
  docker tag "$image" "$REGISTRY/${image#quay.io/}"
  docker push "$REGISTRY/${image#quay.io/}"
done < images.txt

Without a Docker daemon, use skopeo for each image, for example:

skopeo copy docker-archive:images/server.tar docker://$REGISTRY/ai-analyst/server:$(cat VERSION)

The product images land as $REGISTRY/ai-analyst/server, web and jdbc-bridge, tagged with the bundle's VERSION. Your registry may report a different digest from the one in the bundle's manifest.json, because pushing re-encodes the manifest. The bundle's signatures cover the tarballs you pushed from. To pin by digest anyway, set the chart's tag to <VERSION>@sha256:<digest your registry reports>.

Note

The chart has no imagePullSecrets setting. If your registry requires authentication, the cluster's nodes must be able to pull from it.

Step 3: Create the Secrets

The chart does not create Secrets on a self-managed install. You create them before installing.

Server Secret

The chart loads server.existingSecret (default ai-analyst-secrets) as environment variables on every server pod.

kubectl create namespace ai-analyst
kubectl -n ai-analyst create secret generic ai-analyst-secrets \
  --from-literal=ENVIRONMENT=production \
  --from-literal=DATABASE_URL='postgresql://app:<PASSWORD>@postgres.example.internal:5432/app' \
  --from-literal=DATABASE_ADMIN_URL='postgresql://postgres:<PASSWORD>@postgres.example.internal:5432/app' \
  --from-literal=API_PORT=8080 \
  --from-literal=FRONTEND_ORIGIN=https://analyst.example.internal \
  --from-literal=PLG_CONFIG=off \
  --from-literal=AGENT_TRACING_ENABLED=false \
  --from-literal=IDENTITY_BACKEND=keycloak \
  --from-literal=AUTH0_DOMAIN=https://analyst.example.internal/auth/realms/aianalyst \
  --from-literal=AUTH0_AUDIENCE=https://analyst.example.internal/api \
  --from-literal=KEYCLOAK_BASE_URL=http://ai-analyst-keycloak:8080/auth \
  --from-literal=KEYCLOAK_REALM=aianalyst \
  --from-literal=KEYCLOAK_ADMIN_CLIENT_ID=ai-analyst-admin \
  --from-literal=KEYCLOAK_ADMIN_CLIENT_SECRET=<KEYCLOAK_ADMIN_CLIENT_SECRET> \
  --from-literal=BOOTSTRAP_ORG_NAME='Acme Analytics' \
  --from-literal=BOOTSTRAP_ADMIN_EMAIL=admin@example.internal \
  --from-literal=BOOTSTRAP_ADMIN_PASSWORD=<TEMPORARY_PASSWORD> \
  --from-literal=WAREHOUSE_CREDENTIAL_ENCRYPTION_KEY="$(openssl rand -hex 32)" \
  --from-literal=STORAGE_DRIVER=s3 \
  --from-literal=S3_ENDPOINT=https://minio.example.internal \
  --from-literal=S3_BUCKET=ai-analyst \
  --from-literal=S3_REGION=us-east-1 \
  --from-literal=S3_FORCE_PATH_STYLE=true \
  --from-literal=S3_ACCESS_KEY_ID=<S3_ACCESS_KEY_ID> \
  --from-literal=S3_SECRET_ACCESS_KEY=<S3_SECRET_ACCESS_KEY> \
  --from-literal=LLM_PROVIDER=openai-compatible \
  --from-literal=LLM_BASE_URL=https://llm.example.internal/v1 \
  --from-literal=LLM_API_KEY=<LLM_API_KEY> \
  --from-literal=LLM_MODEL=<MODEL_NAME> \
  --from-literal=LLM_CAPABILITY_PRESET=generic \
  --from-literal=EMBEDDING_MODEL=<EMBEDDING_MODEL> \
  --from-literal=EMBEDDING_BASE_URL=https://embeddings.example.internal/v1 \
  --from-literal=EMBEDDING_API_KEY=<EMBEDDING_API_KEY> \
  --from-literal=EMBEDDING_DIMENSIONS=1024

The example assumes the chart-managed Keycloak and the release name ai-analyst. Notes on the keys:

  • Fixed values. ENVIRONMENT, API_PORT, PLG_CONFIG and AGENT_TRACING_ENABLED are fixed values for a self-managed install. Keep them as shown. See Core settings.
  • Identity. The issuer and audience keep the AUTH0_ names even with Keycloak. AUTH0_DOMAIN is the public realm issuer URL: keycloak.hostname followed by /realms/aianalyst. With the chart-managed Keycloak, KEYCLOAK_BASE_URL is http://<release>-keycloak:8080/auth. With your own Keycloak, use its URL instead. KEYCLOAK_ADMIN_CLIENT_ID is ai-analyst-admin.
  • Operator bootstrap. BOOTSTRAP_ORG_NAME, BOOTSTRAP_ADMIN_EMAIL and BOOTSTRAP_ADMIN_PASSWORD create the first organization and its Administrator on the first boot. BOOTSTRAP_SIGNUP_DOMAINS is optional. See Identity and first sign-in.
  • DATABASE_ADMIN_URL is used only by the database bootstrap hook (Step 4). Leave it out when you set dbBootstrap.enabled: false. Use an unprivileged application role in DATABASE_URL. The chart does not enforce this.
  • LLM_*. With LLM_PROVIDER=openai-compatible, LLM_BASE_URL, LLM_MODEL and LLM_CAPABILITY_PRESET are required. The preset is generic, or reasoning for models that accept reasoning_effort. The server refuses to boot when the preset is missing or unknown; it never guesses capabilities from the model name. LLM_API_KEY and LLM_FAST_MODEL are optional (LLM_FAST_MODEL defaults to LLM_MODEL).
  • EMBEDDING_* is optional. Leave out all four keys for lexical-only search.
  • AGENT_STREAM_TIMEOUT_MS (default 240000) limits how long one agent response may take. Raise it for a self-hosted model that needs minutes to answer.
  • Product email: EMAIL_TRANSPORT=smtp, EMAIL_FROM, SMTP_HOST, SMTP_PORT, SMTP_TLS, SMTP_USER, SMTP_PASSWORD and UNSUBSCRIBE_SIGNING_KEY.
  • JDBC bridge (Actian-family warehouses): JDBC_BRIDGE_URL=http://<release>-jdbc-bridge:8080 and JDBC_BRIDGE_TOKEN, with jdbcBridge.enabled: true. The bridge pod reads the token from this same Secret.
  • The chart sets the network posture from values (server.networkPosture), not from this Secret.

Keep the encryption key

Store WAREHOUSE_CREDENTIAL_ENCRYPTION_KEY safely. Losing or changing it loses every stored warehouse credential, every API key for an LLM or Embedding endpoint that an organization added under Settings → Inference, and the catalog API secrets stored on connections.

For every key, see the Configuration reference.

Licence Secret

Create ai-analyst-licence from the Licence that Actian sent you:

kubectl -n ai-analyst create secret generic ai-analyst-licence \
  --from-file=licence.json

Add --from-file=licence-public.pem only if you run images built without the verification key.

The chart mounts it on every server pod, and the server then takes your plan's entitlements from the Licence. Release-bundle images carry Actian's verification key built in, and that key takes precedence. A licence-public.pem in the Secret is only a fallback for images built without the key. The server reads the Licence at startup, so restart the server pods after you replace it. See Licence.

Web config Secret

The web app's public settings go in a separate Secret (web.configSecretName) that only the web pod receives:

kubectl -n ai-analyst create secret generic ai-analyst-web-config \
  --from-literal=VITE_API_URL=/api \
  --from-literal=VITE_IDENTITY_BACKEND=keycloak \
  --from-literal=VITE_AUTH0_DOMAIN=https://analyst.example.internal/auth/realms/aianalyst \
  --from-literal=VITE_AUTH0_CLIENT_ID=ai-analyst-spa \
  --from-literal=VITE_AUTH0_AUDIENCE=https://analyst.example.internal/api \
  --from-literal=VITE_AUTH0_CALLBACK_URL=https://analyst.example.internal/auth/callback
  • VITE_AUTH0_DOMAIN is the same issuer as AUTH0_DOMAIN in the server Secret.
  • VITE_IDENTITY_BACKEND must be keycloak.
  • Optionally add VITE_EDITION_LABEL, for example Enterprise Edition. It adds a line reading "AI Analyst label" to the footer of the organization switcher menu.

These values are written into the web app's configuration when the web container starts. After you change them, restart the web Deployment:

kubectl -n ai-analyst rollout restart deployment/ai-analyst-web

Keycloak

Choose one of these options.

With keycloak.enabled: true, the chart deploys Keycloak 26.5 from the bundle's image. It imports the aianalyst realm only if that realm does not exist yet, so changes you make in the admin console survive upgrades. It also installs the AI Analyst sign-in theme and checks Keycloak's health on its management port (9000).

  1. Create an empty keycloak database on your Postgres:

    psql '<DATABASE_ADMIN_URL>' -c 'CREATE DATABASE keycloak OWNER app;'
    

    Use the same admin URL you put in the server Secret. app is the role from DATABASE_URL.

  2. Create the Keycloak Secret and store the generated administrator password, which you need for the Keycloak admin console. KEYCLOAK_ADMIN_CLIENT_SECRET must be the same value as in ai-analyst-secrets:

    kubectl -n ai-analyst create secret generic ai-analyst-keycloak \
      --from-literal=KC_DB_USERNAME=app \
      --from-literal=KC_DB_PASSWORD=<PASSWORD> \
      --from-literal=KC_BOOTSTRAP_ADMIN_USERNAME=admin \
      --from-literal=KC_BOOTSTRAP_ADMIN_PASSWORD="$(openssl rand -hex 32)" \
      --from-literal=KEYCLOAK_ADMIN_CLIENT_SECRET=<KEYCLOAK_ADMIN_CLIENT_SECRET>
    
  3. In your values, set keycloak.existingSecret, keycloak.database.url, keycloak.hostname, keycloak.spaOrigin and keycloak.apiAudience (Step 4). The chart does not render without them.

  4. In ai-analyst-secrets, set KEYCLOAK_BASE_URL=http://<release>-keycloak:8080/auth and AUTH0_DOMAIN=<keycloak.hostname>/realms/aianalyst.
  5. Route Keycloak's paths through your ingress (Step 6).
  6. If your ingress certificate chains to a private CA, set server.extraCaCerts (Trust a private CA). Otherwise the server cannot verify Keycloak's tokens.

One replica is the tested configuration. If you run more replicas, Keycloak clusters over its database (JDBC_PING) and the pods must reach each other on port 7800. Actian has tested only a single replica.

To use a Keycloak 26 with Organizations that you already run, leave keycloak.enabled: false and:

  1. Import compose/keycloak/realm-template.json from the bundle into your Keycloak, with KC_SPA_ORIGIN, KC_API_AUDIENCE and KEYCLOAK_ADMIN_CLIENT_SECRET set for the import.
  2. Install the ai-analyst login theme from the bundle (compose/keycloak/themes/ai-analyst).
  3. In ai-analyst-secrets, set KEYCLOAK_BASE_URL to your Keycloak's URL and AUTH0_DOMAIN to its aianalyst realm issuer URL.

Step 4: Set the values

Copy the example values file and edit the copy:

cp helm/values-airgap.example.yaml values-airgap.yaml

The example sets:

Value Setting Why
server.networkPosture air-gapped The server then refuses vendor-hosted keys at boot, naming the offending key. The chart default is connected. See Network posture.
eso.enabled false The chart does not create Secrets; you supply them (Step 3). The chart default is true, so a self-managed install must turn it off.
server.existingSecret, server.licence.existingSecret, web.configSecretName ai-analyst-secrets, ai-analyst-licence, ai-analyst-web-config The Secrets from Step 3. Setting the Licence Secret makes the server take entitlements from the Licence.
server.healthProbePath, server.apiPathPrefix /api/health, api The API is served under /api. The chart's default probe path is /health, which does not match.
server.trustProxyHops 0 See Proxies in front of the server.
server.resources 2Gi request, 3Gi limit A 250 MiB CSV conversion peaks near 1 GiB on top of the running server.
keycloak.* enabled: true, with existingSecret, hostname, spaOrigin, apiAudience and database.url The chart-managed Keycloak. Set enabled: false to use your own.
jdbcBridge.enabled false Set true for Actian-family warehouses.
embeddings.enabled false Your own embedding API comes from EMBEDDING_* in the server Secret.
dbBootstrap enabled: true, on the bundle's pgvector image See below.
ingress.enabled, app.gsaEmail false, "" The chart's built-in ingress and service account settings are for Actian SaaS. You route with your own ingress controller.

Then replace the placeholders:

  1. Replace registry.example.internal with your registry host in every image repository.
  2. Replace the tag 1a2b3c4d5e6f on the server, web and jdbc-bridge images with the bundle's VERSION.
  3. Keep the Keycloak (26.5) and pgvector (pg16) tags. Change only their registry host.
  4. Replace analyst.example.internal and postgres.example.internal in the keycloak section with your hosts.
  5. If Keycloak's issuer, your LLM or Embedding endpoint or a warehouse chains to a private CA, uncomment server.extraCaCerts. See Trust a private CA.

Database bootstrap hook

While dbBootstrap.enabled is true (the default), a hook Job runs before the migrations on each helm upgrade. It runs CREATE EXTENSION IF NOT EXISTS for vector and pg_trgm, connecting as DATABASE_ADMIN_URL. Only this hook uses that privileged URL, but the server Secret is loaded onto every server pod, so the URL is also present in the server pods' environment. The hook is safe to run repeatedly.

If a DBA has already created both extensions, or the role in DATABASE_URL may create them, set dbBootstrap.enabled: false and leave DATABASE_ADMIN_URL out of the server Secret. This also keeps the privileged URL out of the server pods' environment.

Proxies in front of the server

server.trustProxyHops is the number of proxies in front of the server that append to the X-Forwarded-For header. It is typically 1 for a single ingress-nginx. The default 0 trusts no header. Never set it higher than the real number of proxies: each extra hop lets a client choose its own address.

Private warehouse addresses

On Kubernetes, warehouse connections, and LLM or Embedding endpoints that an organization adds under Settings → Inference, are refused when they resolve to private address ranges. To allow your private networks, list them in your values:

jdbcBridge:
  networkPolicy:
    allowPrivateEgressCidrs:
      - 10.20.0.0/16

The chart applies this list to the server pods and to the JDBC bridge's network policy. Set it in values, not in the server Secret: the chart's value takes precedence. See Warehouse connectivity.

Trust a private CA

Do this before you install if any endpoint the server calls presents a certificate that chains to a private CA, because the server must trust that CA. This applies to Keycloak's issuer URL (which the server reaches through your ingress), your LLM or Embedding endpoint, and warehouses.

  1. Create a ConfigMap with the PEM bundle under the key ca.crt:

    kubectl -n ai-analyst create configmap ai-analyst-ca --from-file=ca.crt=internal-ca.pem
    
  2. In your values, set:

    server:
      extraCaCerts:
        configMap: ai-analyst-ca
        # key: ca.crt   # change if your ConfigMap uses another key
    
  3. Include these values when you run the install in Step 5. If you change the ConfigMap later, run helm upgrade with your values.

The chart mounts the bundle on every server pod. It applies to server pods only, not to the web, migration or bootstrap pods. The server reads it only when it starts, so restart the server pods after you change the ConfigMap.

Step 5: Install the chart

The database bootstrap and migration Jobs are pre-upgrade hooks, so the first install takes two commands. helm install creates the release, and helm upgrade runs the hooks:

helm install ai-analyst helm/ai-analyst-*.tgz -n ai-analyst -f values-airgap.yaml
helm upgrade ai-analyst helm/ai-analyst-*.tgz -n ai-analyst -f values-airgap.yaml --wait --timeout 15m

Until the upgrade finishes, the server pods fail their startup probe and restart. That is expected.

On the first boot, the Operator bootstrap creates the first organization and its Administrator. The Administrator then signs in at FRONTEND_ORIGIN with BOOTSTRAP_ADMIN_EMAIL and the temporary password. See Identity and first sign-in.

Every later upgrade is the helm upgrade command with the new bundle's chart and image tag. See Upgrade and back up.

Step 6: Route your ingress

Route one public origin to the chart's Services. For the release name ai-analyst:

Path Service Notes
/api ai-analyst-server-server:8080 Do not strip the /api prefix.
/auth/realms, /auth/resources, /auth/admin, /auth/js ai-analyst-keycloak:8080 Only with the chart-managed Keycloak. Do not strip the prefix.
Everything else ai-analyst-web:80

Service names start with the release name, so they change if you use a different one.

Do not send all of /auth to Keycloak

The web app owns /auth/login, /auth/callback and /auth/onboarding. Route only the four Keycloak subtrees above to Keycloak.

Also configure your ingress controller to:

  • Terminate TLS at the ingress.
  • Allow request bodies of at least 250 MiB plus multipart overhead. File uploads are capped at 250 MiB. For example, on ingress-nginx set the body size to 260m.
  • Set read and send timeouts of at least 600 seconds, so large uploads can finish. On ingress-nginx, set both the proxy read timeout and the proxy send timeout.
  • Not buffer /api responses. Conversation turns stream to the browser as server-sent events. The server asks proxies not to buffer them (ingress-nginx honors this), but check that any other proxy in the path does not buffer /api.

Known limitations

As shipped, the chart has these limitations:

  • No image pull secrets. The chart has no imagePullSecrets setting.
  • Default service account. Server pods run under the namespace's default ServiceAccount.
  • No pod security context except Keycloak. The server, web and JDBC bridge pods set no securityContext. Only the chart-managed Keycloak runs as non-root, with privilege escalation disabled and all capabilities dropped. As shipped, the chart does not meet the Kubernetes Pod Security "restricted" profile.
  • No network policy for server pods. Your cluster's egress policy is what keeps the install air-gapped. See Network posture.
  • One tested Keycloak replica. See Keycloak.

Troubleshooting

Symptom Likely cause What to do
Server pods restart during the first install The migrations run only during helm upgrade. Run the helm upgrade command from Step 5 and wait for it to finish.
Server pods never become ready The server refused to boot. Check the pod logs. Messages starting NETWORK_POSTURE=air-gapped name a vendor-hosted setting to remove. Missing required env vars lists keys to add to the server Secret. Messages starting LICENCE_PATH mean the Licence is missing, malformed or not signed by Actian.
helm install fails to render A required keycloak.* value is missing while keycloak.enabled is true. Add the value named in the error.
The install behaves as Connected, with no error server.networkPosture: air-gapped is not reaching the chart. The chart default is connected. Check that you pass your values file with -f.
Every API call returns 401 The server cannot verify Keycloak's tokens. Check that AUTH0_DOMAIN matches the public issuer, and that the server trusts its certificate (Trust a private CA).
Members see Chat needs an LLM endpoint There is no deployment default LLM_* and the organization has not added its own endpoint, or the server cannot reach the endpoint. Set LLM_* in the server Secret and restart the server pods, or add an endpoint under Settings → Inference.
Large uploads fail The ingress body size limit or timeouts are too low. See Step 6.

To collect information for Actian support, see Diagnostic export.

Next steps