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_CONFIGandAGENT_TRACING_ENABLEDare 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_DOMAINis the public realm issuer URL:keycloak.hostnamefollowed by/realms/aianalyst. With the chart-managed Keycloak,KEYCLOAK_BASE_URLishttp://<release>-keycloak:8080/auth. With your own Keycloak, use its URL instead.KEYCLOAK_ADMIN_CLIENT_IDisai-analyst-admin. - Operator bootstrap.
BOOTSTRAP_ORG_NAME,BOOTSTRAP_ADMIN_EMAILandBOOTSTRAP_ADMIN_PASSWORDcreate the first organization and its Administrator on the first boot.BOOTSTRAP_SIGNUP_DOMAINSis optional. See Identity and first sign-in. DATABASE_ADMIN_URLis used only by the database bootstrap hook (Step 4). Leave it out when you setdbBootstrap.enabled: false. Use an unprivileged application role inDATABASE_URL. The chart does not enforce this.LLM_*. WithLLM_PROVIDER=openai-compatible,LLM_BASE_URL,LLM_MODELandLLM_CAPABILITY_PRESETare required. The preset isgeneric, orreasoningfor models that acceptreasoning_effort. The server refuses to boot when the preset is missing or unknown; it never guesses capabilities from the model name.LLM_API_KEYandLLM_FAST_MODELare optional (LLM_FAST_MODELdefaults toLLM_MODEL).EMBEDDING_*is optional. Leave out all four keys for lexical-only search.AGENT_STREAM_TIMEOUT_MS(default240000) 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_PASSWORDandUNSUBSCRIBE_SIGNING_KEY. - JDBC bridge (Actian-family warehouses):
JDBC_BRIDGE_URL=http://<release>-jdbc-bridge:8080andJDBC_BRIDGE_TOKEN, withjdbcBridge.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_DOMAINis the same issuer asAUTH0_DOMAINin the server Secret.VITE_IDENTITY_BACKENDmust bekeycloak.- Optionally add
VITE_EDITION_LABEL, for exampleEnterprise 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).
-
Create an empty
keycloakdatabase on your Postgres:psql '<DATABASE_ADMIN_URL>' -c 'CREATE DATABASE keycloak OWNER app;'Use the same admin URL you put in the server Secret.
appis the role fromDATABASE_URL. -
Create the Keycloak Secret and store the generated administrator password, which you need for the Keycloak admin console.
KEYCLOAK_ADMIN_CLIENT_SECRETmust be the same value as inai-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> -
In your values, set
keycloak.existingSecret,keycloak.database.url,keycloak.hostname,keycloak.spaOriginandkeycloak.apiAudience(Step 4). The chart does not render without them. - In
ai-analyst-secrets, setKEYCLOAK_BASE_URL=http://<release>-keycloak:8080/authandAUTH0_DOMAIN=<keycloak.hostname>/realms/aianalyst. - Route Keycloak's paths through your ingress (Step 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:
- Import
compose/keycloak/realm-template.jsonfrom the bundle into your Keycloak, withKC_SPA_ORIGIN,KC_API_AUDIENCEandKEYCLOAK_ADMIN_CLIENT_SECRETset for the import. - Install the
ai-analystlogin theme from the bundle (compose/keycloak/themes/ai-analyst). - In
ai-analyst-secrets, setKEYCLOAK_BASE_URLto your Keycloak's URL andAUTH0_DOMAINto itsaianalystrealm 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:
- Replace
registry.example.internalwith your registry host in every image repository. - Replace the tag
1a2b3c4d5e6fon theserver,webandjdbc-bridgeimages with the bundle'sVERSION. - Keep the Keycloak (
26.5) and pgvector (pg16) tags. Change only their registry host. - Replace
analyst.example.internalandpostgres.example.internalin thekeycloaksection with your hosts. - 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.
-
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 -
In your values, set:
server: extraCaCerts: configMap: ai-analyst-ca # key: ca.crt # change if your ConfigMap uses another key -
Include these values when you run the install in Step 5. If you change the ConfigMap later, run
helm upgradewith 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
/apiresponses. 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
imagePullSecretssetting. - Default service account. Server pods run under the namespace's
defaultServiceAccount. - 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.