--- description: Install Actian AI Analyst in your own Kubernetes cluster with the Helm chart from the Release bundle, including Secrets, Keycloak, values, ingress routing and private CAs. --- # 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](install-docker-compose.md) instead. !!! info "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-/`), on a host that can reach your private registry and the cluster. !!! note "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](#step-6-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](configure-inference.md). | | 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 ```sh ./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](release-bundle.md). ## 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: ```sh 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: ```sh 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 `@sha256:`. !!! 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. ```sh 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:@postgres.example.internal:5432/app' \ --from-literal=DATABASE_ADMIN_URL='postgresql://postgres:@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= \ --from-literal=BOOTSTRAP_ORG_NAME='Acme Analytics' \ --from-literal=BOOTSTRAP_ADMIN_EMAIL=admin@example.internal \ --from-literal=BOOTSTRAP_ADMIN_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= \ --from-literal=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= \ --from-literal=LLM_MODEL= \ --from-literal=LLM_CAPABILITY_PRESET=generic \ --from-literal=EMBEDDING_MODEL= \ --from-literal=EMBEDDING_BASE_URL=https://embeddings.example.internal/v1 \ --from-literal=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](configuration-reference.md#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://-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](identity-and-first-sign-in.md). - **`DATABASE_ADMIN_URL`** is used only by the database bootstrap hook ([Step 4](#step-4-set-the-values)). 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://-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. !!! warning "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](configuration-reference.md). ### Licence Secret Create `ai-analyst-licence` from the Licence that Actian sent you: ```sh 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](licence.md). ### Web config Secret The web app's public settings go in a separate Secret (`web.configSecretName`) that only the web pod receives: ```sh 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: ```sh kubectl -n ai-analyst rollout restart deployment/ai-analyst-web ``` ### Keycloak Choose one of these options. === "Chart-managed Keycloak" 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: ```sh psql '' -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`: ```sh kubectl -n ai-analyst create secret generic ai-analyst-keycloak \ --from-literal=KC_DB_USERNAME=app \ --from-literal=KC_DB_PASSWORD= \ --from-literal=KC_BOOTSTRAP_ADMIN_USERNAME=admin \ --from-literal=KC_BOOTSTRAP_ADMIN_PASSWORD="$(openssl rand -hex 32)" \ --from-literal=KEYCLOAK_ADMIN_CLIENT_SECRET= ``` 3. In your values, set `keycloak.existingSecret`, `keycloak.database.url`, `keycloak.hostname`, `keycloak.spaOrigin` and `keycloak.apiAudience` ([Step 4](#step-4-set-the-values)). The chart does not render without them. 4. In `ai-analyst-secrets`, set `KEYCLOAK_BASE_URL=http://-keycloak:8080/auth` and `AUTH0_DOMAIN=/realms/aianalyst`. 5. Route Keycloak's paths through your ingress ([Step 6](#step-6-route-your-ingress)). 6. If your ingress certificate chains to a private CA, set `server.extraCaCerts` ([Trust a private CA](#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. === "Your own Keycloak" 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: ```sh 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](network-posture.md). | | `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](#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](#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: ```yaml 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](warehouse-connectivity.md). ### 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`: ```sh kubectl -n ai-analyst create configmap ai-analyst-ca --from-file=ca.crt=internal-ca.pem ``` 2. In your values, set: ```yaml 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](#step-5-install-the-chart). 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: ```sh 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](identity-and-first-sign-in.md). Every later upgrade is the `helm upgrade` command with the new bundle's chart and image tag. See [Upgrade and back up](upgrade-and-backup.md). ## 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. !!! warning "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](network-posture.md). - **One tested Keycloak replica.** See [Keycloak](#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](#step-5-install-the-chart) 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](#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](#step-6-route-your-ingress). | To collect information for Actian support, see [Diagnostic export](diagnostic-export.md). ## Next steps - [Configure inference](configure-inference.md) - [Warehouse connectivity](warehouse-connectivity.md) - [Identity and first sign-in](identity-and-first-sign-in.md) - [Upgrade and back up](upgrade-and-backup.md) - [Scaling and operations](scaling-and-operations.md)