Self-Hosting Intelligence
Deploy the CopilotKit Intelligence Platform to your own Kubernetes cluster with the copilot-intelligence Helm chart — install, configure, and operate the app-api, app-frontend, and realtime-gateway services with your own Postgres, Redis, ingress, and OIDC provider.
What is this?#
CopilotKit Intelligence — the platform that powers threads, shared state, the inspector, and observability — can be self-hosted on your own Kubernetes cluster using the copilot-intelligence Helm chart. You run the control plane and data plane inside your own network boundary; the chart leaves you in charge of identity, storage, and secrets.
What you bring:
- Postgres and Redis — your own, or the bundled Bitnami subcharts
- An OIDC provider for identity
- Secrets via External Secrets Operator, direct Kubernetes Secrets, or chart-managed credentials
What the chart deploys:
| Component | Role | Port |
|---|---|---|
app-api | Backend service | 4201 |
app-frontend | Web UI | 8080 |
realtime-gateway (optional) | WebSocket service for realtime sync | 4401 |
Plus a database-migrations Job, a thread-culler CronJob, and the usual supporting resources (Services, Ingress, HPAs, PodDisruptionBudgets, ConfigMaps, and — when ESO is enabled — ExternalSecret resources).
When should I use this?#
- Your organization requires CopilotKit Intelligence to run inside your own VPC or data center for compliance, data residency, or security reasons
- You want to connect Intelligence to internal databases, identity providers, or secret stores that are not reachable from Copilot Cloud
- You need to operate the platform under your existing Kubernetes tooling, CI/CD, and observability stack
- You have a Enterprise Intelligence Platform license and the platform-engineering capacity to run a production Kubernetes workload
The chart installs the same way against a local Docker Desktop or k3d cluster as against a production one, so walk this guide end-to-end on your laptop first. Two local paths are supported:
- Bundled overlay — install with the
values-quickstart-local.yamloverlay shipped in the chart. It enables in-cluster Postgres, Redis, and (optionally) Keycloak; you drive the install yourself, following this guide. - One-shot script —
scripts/local-demo.shspins up a disposable k3d cluster, installs the released chart from GHCR, and brings up bundled Keycloak in one command:
./scripts/local-demo.sh --version <chart-version>
Both paths use the same install commands described below — pick whichever fits.
Prerequisites#
Before starting, make sure the following are in place. The How the Enterprise Intelligence Platform Works page explains the layering in more depth.
License and registry access:
- A valid Enterprise Intelligence Platform license key (contact your CopilotKit account team if you do not have one)
- Read access to the chart OCI registry at
oci://ghcr.io/copilotkit/charts/intelligence(anonymous pulls are allowed for the released chart) - The latest released chart version. Check the chart releases on GHCR; substitute the value into the
<chart-version>placeholder used throughout this guide (e.g.0.1.0-rc.16).
Cluster and tooling:
- Kubernetes ≥ 1.28
- Helm ≥ 3.12
kubectlconfigured against the target cluster with an admin-equivalent context
Platform prerequisites (cluster-wide, installed once):
- An ingress controller — either
nginx-ingressor the AWS Load Balancer Controller cert-manager(or a cloud-managed certificate alternative such as AWS ACM) for TLS on the public hostnamesExternal Secrets Operatorif you plan to sync secrets from AWS Secrets Manager, HashiCorp Vault, or GCP Secret Manager (recommended for production, but not required — see Secrets)
External dependencies (reachable from the cluster):
- PostgreSQL ≥ 14 — managed (Amazon RDS, Aurora, Cloud SQL) or operator-deployed in-cluster
- Redis ≥ 7 (or a Valkey-compatible service such as Amazon ElastiCache)
- An OIDC identity provider — Keycloak, Okta, Azure AD, Auth0, Google Workspace, or equivalent
Optional:
- Amazon OpenSearch (only when analytics features are in use)
- An S3-compatible object store (only when the realtime gateway is configured to persist AG-UI events)
Implementation#
Prepare your Kubernetes cluster#
Ensure kubectl points to the cluster that will run Intelligence.
kubectl config current-context
kubectl auth can-i create namespace --all-namespaces
Confirm the context names your target cluster and that the permission check returns yes. If not, fix your kubeconfig before proceeding.
Install platform prerequisites#
These components are cluster-wide and installed once per cluster, independently of the application chart.
After each controller is running, its pods should be Ready in their respective namespaces.
Provision external dependencies#
Intelligence needs Postgres, Redis, and an OIDC issuer. You can either point the chart at managed services you already run, or enable the bundled Bitnami subcharts for in-cluster Postgres and Redis (appropriate for evaluation and small self-hosted installs).
Using managed services (recommended for production):
- Create a Postgres database and user. Record the host, port (default
5432), database name, username, and password. - Create a Redis instance with TLS enabled. Record the host, port (default
6379), and password. - Configure an OIDC client in your identity provider. Record the issuer URL, client ID, and client secret.
Using the bundled in-cluster subcharts:
Set postgresql.enabled: true and redis-subchart.enabled: true in your values file (covered in the next step). A matching StorageClass must exist in the cluster. The bundled Keycloak subchart is available via keycloak.enabled: true if you also need a quick OIDC provider for evaluation; do not use the bundled Keycloak for production workloads. See Bundled Keycloak (eval only) for the realm and credentials it creates.
The chart already ships a tested overlay for this shape — values-quickstart-local.yaml — which enables bundled Postgres + Redis, sets migrations.enabled: true, sizes resources for a laptop, and creates disposable secrets so the install runs end-to-end with no manual prep. Layer your own overlay on top of it (see the next step) to plug in your IdP and ingress.
Create a values file#
The released chart ships several example values files for the common deployment shapes. Pick the one closest to your environment and copy it into a working overlay you can edit. Pull and untar the chart so you have local copies to diff against:
helm pull oci://ghcr.io/copilotkit/charts/intelligence --version <chart-version> --untar
# AWS-flavored (ALB, IRSA, External Secrets from AWS Secrets Manager)
cp intelligence/values-aws-example.yaml my-values.yaml
# Or on-prem-flavored (nginx, manual Kubernetes Secrets)
cp intelligence/values-onprem-example.yaml my-values.yaml
# Or self-hosted eval (bundled Keycloak + in-cluster Postgres/Redis)
cp intelligence/values-self-hosted-eval.yaml.example my-values.yaml
The chart untars into a directory named intelligence/ (the published chart name on GHCR; the chart's nameOverride keeps release-prefixed resources named cpki-*).
Edit my-values.yaml to set at minimum:
database.host,database.port,database.name— your Postgres connection (namedefaults tointelligence)redis.host,redis.port,redis.tls— your Redis connection (TLS is on by default; managed Redis requires it)auth.issuer— your OIDC provider's issuer URLauth.existingSecret— name of the Kubernetes Secret containingauth-secret,auth-client-id,auth-client-secret(or use one of the alternate paths in Secrets)ingress.ui.host— the hostname users will load the Intelligence UI on (for exampleintelligence.example.com)ingress.api.host— optional dedicated API hostname. When omitted, theui.hostrule routes/apiand/authpaths toapp-api, so a single hostname is fine for most installs.ingress.tls— TLS configuration for the hosts abovemigrations.enabled: true— required for first install; defaults tofalse. Without it the database schema is never applied andapp-apiwill crashloop. (The eval overlayvalues-quickstart-local.yamlsets this for you when you layer on top of it.)
Some providers (Auth0 in particular) only accept the issuer URL with a trailing slash (e.g. https://your-tenant.auth0.com/). A missing or extra slash produces an opaque "issuer mismatch" failure at login time. Match the value exactly to what your provider's discovery endpoint advertises.
See the Configuration reference section for the full set of values.
Create secrets#
The chart supports three paths for secrets management. Pick exactly one.
Path A — External Secrets Operator (recommended for production):
- Ensure your secret backend (AWS Secrets Manager, Vault, etc.) has entries for the database URL, Redis URL, and auth credentials.
- Create a
ClusterSecretStore(orSecretStore) that references that backend. - In
my-values.yaml, setexternalSecrets.enabled: true,externalSecrets.store.kind, andexternalSecrets.store.nameto match. The chart then generatesExternalSecretresources that sync those entries into Kubernetes Secrets at the namesapp-apiexpects.
Path B — Direct Kubernetes Secrets (you manage the rotations):
Leave externalSecrets.enabled: false (the default) and create the Secrets manually before installing:
kubectl create namespace copilot-intelligence
kubectl create secret generic cpki-db \
--from-literal=database-url='postgresql://user:pass@host:5432/intelligence' \
-n copilot-intelligence
kubectl create secret generic cpki-redis \
--from-literal=redis-url='rediss://:password@host:6379' \
-n copilot-intelligence
kubectl create secret generic cpki-auth \
--from-literal=auth-secret="$(openssl rand -hex 32)" \
--from-literal=auth-client-id='<OIDC client id>' \
--from-literal=auth-client-secret='<OIDC client secret>' \
-n copilot-intelligence
Reference these names in your values file via database.existingSecret, redis.existingSecret, and auth.existingSecret. The Secret keys are lowercase-hyphenated (auth-secret, database-url, runner-auth-secret); the workloads consume them as the corresponding uppercase env vars (AUTH_SECRET, DATABASE_URL, RUNNER_AUTH_SECRET).
Path C — Chart-managed self-hosted secrets (simplest BYOC):
Useful when you do not run a secret manager and prefer Helm to create the Kubernetes Secrets directly from values you provide at install time. Set selfHostedSecrets.enabled: true and supply the credentials inline:
selfHostedSecrets:
enabled: true
db:
url: "postgresql://user:pass@host:5432/intelligence"
redis:
url: "rediss://:password@host:6379"
auth:
# Auto-generated when left empty.
secret: ""
clientId: "<OIDC client id>"
clientSecret: "<OIDC client secret>"
realtimeGateway:
# Auto-generated when left empty.
runnerAuthSecret: ""
secretKeyBase: ""
beam:
# Auto-generated when left empty.
releaseCookie: ""
The chart auto-generates auth.secret, the realtime-gateway runner/key-base, and the BEAM cookie when those fields are empty, so you only need to provide what you actually have.
Install the chart#
The release can be installed directly from the GHCR OCI registry — no local untar is required for the install itself. Use helm upgrade --install so the same command works for first-time installs and upgrades.
helm upgrade --install copilot-intelligence \
oci://ghcr.io/copilotkit/charts/intelligence \
--version <chart-version> \
-f my-values.yaml \
-n copilot-intelligence \
--create-namespace \
--wait \
--timeout 10m
Layering multiple values files is supported and is the recommended pattern for evaluation: combine the chart's bundled values-quickstart-local.yaml (in-cluster Postgres/Redis, eval-sized resources, migrations.enabled: true, disposable secrets) with your own overlay (IdP, ingress, anything cluster-specific). Pull the chart first so you have a local copy of values-quickstart-local.yaml to reference:
helm upgrade --install copilot-intelligence \
oci://ghcr.io/copilotkit/charts/intelligence \
--version <chart-version> \
-f intelligence/values-quickstart-local.yaml \
-f my-values.yaml \
-n copilot-intelligence --create-namespace \
--wait --timeout 10m
--wait blocks until the Deployments report healthy replicas; --timeout 10m allows enough time for image pulls and the initial database migration job. Right-most -f files win on conflicts, so put your overlay last.
The migrations Job runs as a pre-install/pre-upgrade hook (weight -5) when secrets are pre-created (Path A or Path B above), so the schema is ready before app pods start. It runs as a post-install/post-upgrade hook (weight 5) when secrets are managed by Helm (Path C, or when using postgresql.enabled: true), because the Secret resources don't exist until Helm has created them.
Verify the install#
Check that every pod is Running and the ingress is ready:
kubectl get pods -n copilot-intelligence
kubectl get ingress -n copilot-intelligence
You should see app-api, app-frontend, and — if enabled — realtime-gateway pods running. The migrations Job will appear as Completed.
Confirm the API health check reports ok:
curl https://<ingress.api.host>/api/health
The endpoint returns 200 OK only when the database is reachable — a failed health check is almost always a database connectivity problem.
Service-specific health endpoints, useful when port-forwarding to an individual pod:
| Service | Path |
|---|---|
app-api | /api/health |
app-frontend | /healthz |
realtime-gateway | /health |
Finally, browse to https://<ingress.ui.host> and log in via your OIDC provider. A successful login confirms end-to-end wiring.
On a local cluster (Docker Desktop, k3d) without a public DNS name, port-forward the ingress controller rather than the frontend service so the UI host rule still routes /api and /auth to app-api. Set ingress.ui.host: "localhost" in your overlay, then leave this terminal open for as long as you're using the app:
kubectl -n ingress-nginx port-forward svc/ingress-nginx-controller 8080:80
Browse to http://localhost:8080. Port-forwarding the app-frontend service directly bypasses the ingress and breaks /api and /auth routing.
Upgrade and uninstall#
Upgrade — bump the version in your install command and re-run it. Because the install command already uses helm upgrade --install, the same invocation works for both fresh installs and upgrades:
helm upgrade --install copilot-intelligence \
oci://ghcr.io/copilotkit/charts/intelligence \
--version <new-chart-version> \
-f my-values.yaml \
-n copilot-intelligence \
--wait
Before upgrading, regenerate the example values for the target version (helm pull ... --version <new-chart-version> --untar) and diff against your overlay to catch new keys.
Uninstall — releases leave PersistentVolumes in place by default if you enabled bundled subcharts; delete them manually if you intend to tear down state.
helm uninstall copilot-intelligence -n copilot-intelligence
Bundled Keycloak (eval only)#
When keycloak.enabled: true, the chart deploys the Bitnami Keycloak subchart with a pre-seeded realm and demo user. This is for evaluation and demos — not production. The realm import creates:
- Realm:
cpk-dev - OIDC client:
cpk-self-hostedwith secretcpk-self-hosted-secret(override viaauth.keycloakClient.clientId/auth.keycloakClient.clientSecret) - Demo user:
engineer/engineer(override viaauth.keycloakDemoUser) - Redirect URIs / web origins: default
["*"]for eval flexibility (override viaauth.keycloakClient.redirectUris/webOrigins)
The chart auto-wires auth.issuer to the in-cluster Keycloak service, so leaving auth.issuer empty when keycloak.enabled: true is intentional.
For production self-hosted deployments, leave keycloak.enabled: false and point auth.issuer at your own IdP.
Configuration reference#
The tables below summarize the most common values. For every option, see values.yaml in the pulled chart.
Global#
| Key | Description | Default |
|---|---|---|
global.imageRegistry | Registry prefix for unqualified image names | "" |
global.intelligenceImageRegistry | Registry prefix specifically for the five Intelligence service images | "" |
global.imagePullSecrets | Image pull secrets for private registries | [] |
global.storageClass | StorageClass override for bundled subcharts | "" |
Database#
| Key | Description | Default |
|---|---|---|
database.host | Postgres host | "" (required) |
database.port | Postgres port | 5432 |
database.name | Database name | intelligence |
database.existingSecret | Pre-existing Secret with database-url | "" |
database.secretKeys.url | Key inside the Secret holding the connection string | database-url |
Redis#
| Key | Description | Default |
|---|---|---|
redis.host | Redis host | "" (required) |
redis.port | Redis port | 6379 |
redis.tls | Require TLS (ElastiCache defaults to on) | true |
redis.existingSecret | Pre-existing Secret with redis-url | "" |
redis.secretKeys.url | Key inside the Secret holding the connection URL | redis-url |
OpenSearch (optional)#
| Key | Description | Default |
|---|---|---|
openSearch.host | OpenSearch domain endpoint | "" |
openSearch.port | Port | 443 |
openSearch.tls | Require TLS | true |
openSearch.existingSecret | Pre-existing Secret with opensearch-url | "" |
Authentication#
| Key | Description | Default |
|---|---|---|
auth.deploymentMode | self-hosted (single org) or hosted (multi-org) | self-hosted |
auth.issuer | OIDC issuer URL (auto-set when keycloak.enabled: true) | "" |
auth.existingSecret | Secret with auth-secret, auth-client-id, auth-client-secret | "" |
auth.defaultOrganizationId | Default organization ID in self-hosted mode | default |
auth.providerId | Stable identifier for the OIDC provider | enterprise-sso |
auth.providerName | Display name shown in the UI | Enterprise SSO |
auth.trustHost | Trust the X-Forwarded-Host header (set behind a reverse proxy) | "true" |
Ingress#
| Key | Description | Default |
|---|---|---|
ingress.enabled | Create Ingress resources | true |
ingress.className | nginx or alb | nginx |
ingress.ui.host | UI hostname; the rule for this host routes /api and /auth to app-api and / to app-frontend | "" (required) |
ingress.api.host | Optional dedicated API hostname. When set, this hostname routes / to app-api. When empty, no separate API rule is created — the UI host already serves the API. | "" |
ingress.realtimePlane.host | Optional dedicated realtime hostname (only used when realtimeGateway.enabled: true) | "" |
ingress.tls | TLS configuration | [] |
ingress.websocket.enabled | Add WebSocket-friendly annotations (auto-enabled when realtime-gateway is enabled with nginx) | false |
ingress.annotations | Additional ingress annotations | {} |
Services (appApi, appFrontend, realtimeGateway)#
| Key | Description | Default (appApi) | Default (appFrontend) | Default (realtimeGateway) |
|---|---|---|---|---|
<svc>.enabled | Enable the service | true | true | false |
<svc>.replicaCount | Replicas | 2 | 2 | 2 |
<svc>.image.repository | Image repository (published chart fully-qualifies these to ghcr.io/copilotkit/intelligence/<svc>) | intelligence/app-api | intelligence/app-frontend | intelligence/realtime-gateway |
<svc>.image.tag | Image tag (defaults to chart appVersion) | "" | "" | "" |
<svc>.resources | CPU/memory requests | 250m / 512Mi | 100m / 128Mi | 500m / 512Mi |
<svc>.autoscaling.enabled | Enable HPA | true | false | true |
<svc>.autoscaling.minReplicas | HPA minimum | 2 | 2 | 2 |
<svc>.autoscaling.maxReplicas | HPA maximum | 10 | 4 | 10 |
<svc>.serviceAccount.annotations | Annotations on the ServiceAccount (IRSA, workload identity) | {} | {} | {} |
<svc>.podAnnotations | Pod template annotations (e.g. for Stakater Reloader on ESO secret rotation) | {} | n/a | {} |
Realtime gateway (additional keys)#
| Key | Description | Default |
|---|---|---|
realtimeGateway.enabled | Enable the gateway | false |
realtimeGateway.host | PHX_HOST override | "" |
realtimeGateway.existingSecret | Secret containing keys runner-auth-secret and secret-key-base (mapped to env vars RUNNER_AUTH_SECRET / SECRET_KEY_BASE) | "" |
realtimeGateway.beam.clustering.enabled | BEAM clustering across replicas | true |
realtimeGateway.beam.cookieSecret.name | Secret containing the BEAM cookie | cpki-beam-cookie |
Enabling the realtime gateway requires that either realtimeGateway.existingSecret is set, or that externalSecrets.secrets.realtimeGateway.enabled or selfHostedSecrets.enabled is true — the chart fails validation otherwise.
External Secrets Operator integration#
| Key | Description | Default |
|---|---|---|
externalSecrets.enabled | Generate ExternalSecret resources | false |
externalSecrets.store.kind | ClusterSecretStore or SecretStore | ClusterSecretStore |
externalSecrets.store.name | SecretStore name | "" (required when enabled) |
externalSecrets.refreshInterval | How often ESO syncs | 1h |
externalSecrets.secrets.* | Per-secret mappings — see values.yaml | — |
Self-hosted (chart-managed) secrets#
| Key | Description | Default |
|---|---|---|
selfHostedSecrets.enabled | Create Kubernetes Secrets from inline values; auto-generates blank fields | false |
selfHostedSecrets.db.url | Postgres connection URL | "" (required when enabled) |
selfHostedSecrets.redis.url | Redis connection URL | "" (required when enabled) |
selfHostedSecrets.auth.clientId / clientSecret | OIDC client credentials | "" (required when enabled) |
selfHostedSecrets.auth.secret | Internal auth signing secret | auto-generated when empty |
selfHostedSecrets.realtimeGateway.runnerAuthSecret / secretKeyBase | Runtime gateway secrets | auto-generated when empty |
selfHostedSecrets.beam.releaseCookie | BEAM clustering cookie | auto-generated when empty |
Bundled subcharts (evaluation only)#
| Key | Description | Default |
|---|---|---|
postgresql.enabled | Deploy in-cluster Postgres | false |
postgresql.auth.password | Postgres password (set at deploy time) | "" |
redis-subchart.enabled | Deploy in-cluster Redis (aliased to avoid collision with redis.*) | false |
redis-subchart.auth.password | Redis password | "" |
keycloak.enabled | Deploy bundled Keycloak for quick eval | false |
Object storage (realtime gateway event persistence)#
| Key | Description | Default |
|---|---|---|
objectStorage.enabled | Persist AG-UI events from the realtime gateway to S3-compatible storage | false |
objectStorage.bucket | Bucket name | "" |
objectStorage.region | Bucket region | us-east-1 |
objectStorage.endpoint | S3-compatible endpoint override (e.g. for MinIO) | "" |
objectStorage.forcePathStyle | Force path-style addressing (required for MinIO) | false |
objectStorage.existingSecret | Secret with static access keys (optional if using IRSA) | "" |
Database migrations#
| Key | Description | Default |
|---|---|---|
migrations.enabled | Run the migrations Job. Required for first install — defaults to false. | false |
migrations.image.repository | Migrations image repository | intelligence/db-migrations |
migrations.activeDeadlineSeconds | Job deadline | 1800 |
migrations.backoffLimit | Retry count before failing | 3 |
The migrations Job runs as a pre-install/pre-upgrade Helm hook (weight -5) when secrets are pre-created (External Secrets path or manual existingSecret) and as a post-install/post-upgrade hook (weight 5) when secrets are managed by Helm itself (selfHostedSecrets.enabled or postgresql.enabled).
Thread culler (CronJob)#
| Key | Description | Default |
|---|---|---|
threadCuller.enabled | Run a CronJob that soft-deletes stale threads in unlicensed deployments | false |
threadCuller.schedule | Cron expression | 0 * * * * |
threadCuller.staleHours | Threads older than this many hours (since last update) are culled | "3" |
threadCuller.batchSize | Maximum threads to cull per run | "1000" |
threadCuller.licenseSecret.existingSecret | Secret containing COPILOTKIT_LICENSE_TOKEN. When set, the CronJob skips culling (licensed install). When empty, it culls. | "" |
Shared config (CORS, logging)#
| Key | Description | Default |
|---|---|---|
config.logLevel | Log level for all services (trace/debug/info/warn/error/fatal) | info |
config.nodeEnv | Node environment; affects cookie security and runtime defaults | production |
config.appFrontendOrigin | Browser origin allowed to perform authenticated bootstrap writes | "" |
config.publicAppOrigin | Public UI origin used by server-side callbacks when distinct from appFrontendOrigin | "" |
config.allowedOrigins | Additional CORS allowlist (comma-separated). Entries are exact origins (https://app.example.com) or Phoenix-style //host patterns | "" |
Pod-level controls#
Per-service keys podDisruptionBudget, podAntiAffinity, and networkPolicy are available for high-availability and traffic-isolation requirements. See values.yaml for full shapes.
Next steps#
- Understand how it works: How the Enterprise Intelligence Platform Works — architecture, multi-tenancy model, platform layering, and the decision between hosted and self-hosted
- Premium features overview: CopilotKit Premium — all premium capabilities that require an Intelligence license
- Use threads in your app: Threads — the persistent-conversation surface powered by the Intelligence Platform you just deployed
