qaitai docs

Self-host on Kubernetes (Helm)

Deploy web, runners and an optional Steel pool with the chart in deploy/helm/qaitai.

The chart deploys web (Deployment, Service, optional Ingress), runners, and an optional Steel browser pool. Postgres, Hatchet and S3-compatible storage are external; run Hatchet with its own Helm charts.

Prepare Postgres

Create a database owned by a non-superuser role (for example qaitai), then the qaitai_app role the app connects as:

psql "postgres://qaitai:...@pg:5432/qaitai" -v app_password="$(openssl rand -hex 24)" -f docker/sql/app-role.sql

The app role must not be a superuser, have BYPASSRLS or own the tables; otherwise row-level security is skipped or weakened.

Create the secrets

kubectl create secret generic qaitai-migrate \
  --from-literal=DATABASE_MIGRATION_URL='postgres://qaitai:...@pg:5432/qaitai'
kubectl create secret generic qaitai-env \
  --from-literal=DATABASE_URL='postgres://qaitai_app:...@pg:5432/qaitai' \
  --from-literal=BETTER_AUTH_SECRET="$(openssl rand -base64 32)" \
  --from-literal=QAITAI_MASTER_KEY="$(openssl rand -base64 32)" \
  --from-literal=QAITAI_INTERNAL_SECRET="$(openssl rand -hex 32)" \
  --from-literal=HATCHET_CLIENT_TOKEN='eyJ...' \
  --from-literal=ANTHROPIC_API_KEY='sk-ant-...'

Every key in qaitai-env is injected with envFrom. Optional keys include STEEL_API_KEY, AI_GATEWAY_API_KEY, S3_ACCESS_KEY_ID/S3_SECRET_ACCESS_KEY, GITHUB_* and GOOGLE_*.

Install

helm install qaitai deploy/helm/qaitai \
  --set existingSecret=qaitai-env --set migrations.existingSecret=qaitai-migrate \
  --set url=https://qa.example.com \
  --set ingress.enabled=true --set ingress.host=qa.example.com --set ingress.className=nginx \
  --set hatchet.hostPort=hatchet-engine.hatchet:7070 \
  --set runner.replicaCount=2 --set runner.slots=4

Migrations run in an init container of each web pod with the owner URL from migrations.existingSecret, so app containers never hold owner credentials.

First sign-in

kubectl logs deploy/qaitai-web | grep -A2 "no users yet"

Open the one-time link to become the instance admin.

Options

  • runner.variant=runner-chromium: local browsers in the runner pods (size about 1 vCPU and 1.5 GiB per slot).
  • runner.mode=remote with runner.remote.existingSecret=<secret with QAITAI_TOKEN>: remote-mode runners that don't mount the main secret. See Remote runner.
  • Email: --set email.smtp.host=smtp.example.com --set email.smtp.user=qa --set email.from='qaitai <qa@example.com>' and the password from a Secret via email.smtp.existingSecret.
  • Pods run as uid 1000 with a read-only root filesystem, all capabilities dropped and no service account token. Runners get a 900-second termination grace period so runs can finish during rollouts.

On this page