Deploy Kairos to Kubernetes
In this tutorial we will run Kairos on a Kubernetes cluster from the published
chart and image, with a database and an identity provider beside it. By the end
you will have a deployment answering /healthz and /readyz and serving the
interface, and you will have seen what Kairos needs from a cluster and what it
brings itself.
It takes about twenty minutes, most of which is waiting for images to pull.
Before we start
Section titled “Before we start”You will need:
- Docker, running.
- kind — we will create a throwaway cluster and delete it at the end.
- kubectl and Helm 3.8 or newer (the chart is published as an OCI artifact, which older Helm cannot pull).
We use kind because this is a lesson, not a production deployment. Everything here works the same on a real cluster; the database and identity provider are the parts you would replace.
Identity is always yours, and so is state by default. The chart can stand an evaluation PostgreSQL up inside the release, but this lesson brings its own so you see the shape you would actually run. So the first two things we install are a throwaway Postgres and a throwaway Dex, pinned to exact versions so this lesson behaves the same every time. Neither is suitable for anything but a tutorial: no persistence, no backups, one hard-coded password.
The Postgres image is pgvector/pgvector:pg16 rather than postgres:16: the
same upstream Postgres with the pgvector extension added. Kairos requires
it — the extension backs semantic retrieval, and the first migration installs
it, so a plain postgres:16 fails to migrate rather than merely doing without
the feature. Managed Postgres offers pgvector on RDS, Cloud SQL and Azure, so
this is not a reason to stop bringing your own.
Create the cluster
Section titled “Create the cluster”kind create cluster --name kairos-tutorial --wait 120skubectl create namespace kairosYou should see kind report the cluster ready and namespace/kairos created.
Install a throwaway database
Section titled “Install a throwaway database”kubectl -n kairos create deployment postgres --image=pgvector/pgvector:pg16 --port=5432kubectl -n kairos set env deployment/postgres \ POSTGRES_PASSWORD=kairos POSTGRES_USER=kairos POSTGRES_DB=kairoskubectl -n kairos expose deployment postgres --port=5432kubectl -n kairos rollout status deployment/postgres --timeout=240sdeployment "postgres" successfully rolled outInstall a throwaway identity provider
Section titled “Install a throwaway identity provider”Save this as dex.yaml:
apiVersion: v1kind: ConfigMapmetadata: name: dex namespace: kairosdata: config.yaml: | issuer: http://dex.kairos.svc.cluster.local:5556/dex storage: type: memory web: http: 0.0.0.0:5556 oauth2: skipApprovalScreen: true staticClients: - id: kairos-web name: Kairos Web GUI public: true redirectURIs: - http://localhost:8080/callback enablePasswordDB: true staticPasswords: - email: admin@example.com # bcrypt of "admin-password" hash: "$2y$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W" username: admin userID: "00000000-0000-0000-0000-000000000001"---apiVersion: apps/v1kind: Deploymentmetadata: name: dex namespace: kairosspec: replicas: 1 selector: { matchLabels: { app: dex } } template: metadata: { labels: { app: dex } } spec: containers: - name: dex image: ghcr.io/dexidp/dex:v2.41.1 args: ["dex", "serve", "/etc/dex/config.yaml"] ports: [{ containerPort: 5556 }] volumeMounts: - { name: config, mountPath: /etc/dex } volumes: - name: config configMap: { name: dex }---apiVersion: v1kind: Servicemetadata: name: dex namespace: kairosspec: selector: { app: dex } ports: [{ port: 5556, targetPort: 5556 }]Apply it and wait:
kubectl apply -f dex.yamlkubectl -n kairos rollout status deployment/dex --timeout=240sdeployment "dex" successfully rolled outInstall Kairos
Section titled “Install Kairos”Save this as values.yaml:
database: url: "postgres://kairos:kairos@postgres.kairos.svc.cluster.local:5432/kairos"config: oidc: issuerUrl: "http://dex.kairos.svc.cluster.local:5556/dex" audience: "kairos-web" tenancy: singleTenant: "demo"Those three are the minimum. The chart refuses to render without the two OIDC values, which is deliberate — a deployment that cannot say who its users are is not a deployment.
helm install kairos oci://ghcr.io/colliery-io/charts/kairos --version 0.21.0 \ -n kairos -f values.yaml --wait --timeout 6mHelm pulls the chart from the registry, then the cluster pulls the image:
Pulled: ghcr.io/colliery-io/charts/kairos:0.21.0NAME: kairosSTATUS: deployedThe container applies its own database migrations on boot, so there is no separate migrate step.
Check it is running
Section titled “Check it is running”kubectl -n kairos get podsThe 2 Kairos pods should be 1/1 Running, alongside Postgres and Dex. The
chart runs 2 replicas by default. They share the background work through the
job queue in the database:
NAME READY STATUS RESTARTS AGEdex-6d556d5f6d-nh6ww 1/1 Running 0 3mkairos-5785ff87b8-52d9d 1/1 Running 0 1mkairos-5785ff87b8-7nqvk 1/1 Running 0 1mpostgres-6f59b65747-djxn8 1/1 Running 0 3mReach it
Section titled “Reach it”kubectl -n kairos port-forward svc/kairos 8080:80Leave that running, and in another terminal:
curl http://localhost:8080/healthzcurl http://localhost:8080/readyzokreadywarning: KAIROS_RETENTION_MODE is archive and KAIROS_ARCHIVE_TARGET is not set. The retention pass removes no rows from item_history and activity_log. Set KAIROS_ARCHIVE_TARGET to a directory, or set KAIROS_RETENTION_MODE to discard.Those two answer different questions, and the difference matters when you
deploy for real. /healthz says the process is alive. /readyz says it can
reach its database — which is what Kubernetes should gate traffic on.
The warning: line does not stop the traffic. It tells you that the history
of this deployment does not get smaller. To change this, see
Back up and restore.
Now open http://localhost:8080. Kairos serves the interface from the same
process, and you will be sent to Dex to sign in as admin@example.com /
admin-password.
The tenant is empty: this is a fresh deployment, not the demo data from Run Kairos locally. An empty Kairos is the honest starting point for a real one.
Clean up
Section titled “Clean up”kind delete cluster --name kairos-tutorialThat removes the cluster and everything in it, including the database.
What you have seen
Section titled “What you have seen”Kairos is one stateless image. It brought the interface, the API, MCP and SCIM
in a single process, and it needed exactly two things from you: somewhere to
keep state, and somebody to say who users are — a Postgres, which later versions
need pgvector on, and an OIDC issuer.
That is the whole deployment story, and the rest is making those two things production-grade rather than throwaway.
Where to go next
Section titled “Where to go next”- Install with Helm — the values a real deployment sets, and what each one does
- Configure an OIDC issuer — replacing the throwaway Dex with your own
- Provision a tenant — this lesson pinned a single tenant; a real deployment may host several
- Back up and restore — the database is the only state, and that guide says what to do about it