Run OpenChoreo Locally
This guide walks you through setting up OpenChoreo on your machine with k3d. You will install each plane one at a time, and after each one you will do something real with it: log in, deploy a service, or trigger a build.
OpenChoreo has four planes:
- Control Plane runs the API, console, identity provider, and controllers.
- Data Plane runs your workloads and routes traffic to them.
- Build Plane builds container images from source using Argo Workflows.
- Observability Plane collects logs and metrics from all other planes.
By the end you will have all four running in a single k3d cluster.
What you will get:
- A working OpenChoreo installation on localhost
- A deployed web app you can open in your browser
- A source-to-image build pipeline
- Log collection and querying
Prerequisitesβ
| Tool | Version | Purpose |
|---|---|---|
| Docker | v26+ (8 GB RAM, 4 CPU) | Container runtime |
| k3d | v5.8+ | Local Kubernetes clusters |
| kubectl | v1.32+ | Kubernetes CLI |
| Helm | v3.12+ | Package manager |
Verify everything is installed:
docker --version && docker info > /dev/null
k3d --version
kubectl version --client
helm version --short
Step 1: Create the Clusterβ
curl -fsSL https://raw.githubusercontent.com/openchoreo/openchoreo/main/install/k3d/single-cluster/config.yaml | k3d cluster create --config=-
This creates a cluster named openchoreo. k3d maps several host ports into the cluster so you can reach each plane's services from your machine:
| Port | Plane | Purpose |
|---|---|---|
| 8080 | Control Plane | Console, API, Thunder (HTTP) |
| 8443 | Control Plane | Console, API, Thunder (HTTPS) |
| 19080 | Data Plane | Workload HTTP traffic |
| 19443 | Data Plane | Workload HTTPS traffic |
| 10081 | Build Plane | Argo Workflows UI |
| 10082 | Build Plane | Container registry |
| 11080 | Observability Plane | Observer API (HTTP) |
| 11082 | Observability Plane | OpenSearch API |
Generate a machine IDβ
Fluent Bit (the log collector) needs /etc/machine-id to identify the node. k3d containers don't have one by default, so generate it:
docker exec k3d-openchoreo-server-0 sh -c \
"cat /proc/sys/kernel/random/uuid | tr -d '-' > /etc/machine-id"
Your kubectl context is now k3d-openchoreo.
Step 2: Install Prerequisitesβ
These are third-party components that OpenChoreo depends on. None of them are OpenChoreo-specific, they are standard Kubernetes building blocks.
Gateway API CRDsβ
The Gateway API is the Kubernetes-native way to manage ingress and routing. OpenChoreo uses it to route traffic to workloads in every plane.
kubectl apply --server-side \
-f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.1/experimental-install.yaml
cert-managerβ
cert-manager automates TLS certificate management. OpenChoreo uses it to issue certificates for internal communication between planes.
helm upgrade --install cert-manager oci://quay.io/jetstack/charts/cert-manager \
--namespace cert-manager \
--create-namespace \
--version v1.19.2 \
--set crds.enabled=true
Wait for it to be ready:
kubectl wait --for=condition=Available deployment/cert-manager \
-n cert-manager --timeout=180s
External Secrets Operatorβ
External Secrets Operator syncs secrets from external providers (like Vault or AWS Secrets Manager) into Kubernetes. For local dev, you will point it at a fake provider later.
helm upgrade --install external-secrets oci://ghcr.io/external-secrets/charts/external-secrets \
--namespace external-secrets \
--create-namespace \
--version 1.3.2 \
--set installCRDs=true
Wait for it to be ready:
kubectl wait --for=condition=Available deployment/external-secrets \
-n external-secrets --timeout=180s
kgatewayβ
kgateway is the Gateway API implementation that actually handles traffic. It watches for Gateway and HTTPRoute resources across all namespaces, so installing it once is enough. Every plane creates its own Gateway resource in its own namespace, and this single kgateway controller manages all of them.
helm upgrade --install kgateway-crds oci://cr.kgateway.dev/kgateway-dev/charts/kgateway-crds \
--version v2.1.1
helm upgrade --install kgateway oci://cr.kgateway.dev/kgateway-dev/charts/kgateway \
--namespace openchoreo-control-plane \
--create-namespace \
--version v2.1.1
Step 3: Setup Control Planeβ
The control plane is the brain of OpenChoreo. It runs the API server, the web console, the identity provider, and the controllers that reconcile your resources.
Install Thunder (Identity Provider)β
Thunder handles authentication and OAuth flows. The values file includes bootstrap scripts that run on first startup and configure the organization, users, groups, and OAuth applications automatically.
helm upgrade --install thunder oci://ghcr.io/asgardeo/helm-charts/thunder \
--namespace openchoreo-control-plane \
--create-namespace \
--version 0.21.0 \
--values https://raw.githubusercontent.com/openchoreo/openchoreo/main/install/k3d/common/values-thunder.yaml
Confirm the bootstrap completed:
kubectl logs -n openchoreo-control-plane -l app.kubernetes.io/name=thunder --tail=50
CoreDNS Rewriteβ
Pods inside the cluster need to resolve *.openchoreo.localhost hostnames to reach each other. This ConfigMap tells CoreDNS to rewrite those hostnames to the k3d load balancer:
kubectl apply -f https://raw.githubusercontent.com/openchoreo/openchoreo/main/install/k3d/common/coredns-custom.yaml
Backstage Secretsβ
The web console (Backstage) needs a backend secret for session signing and an OAuth client secret to authenticate with Thunder:
kubectl create namespace openchoreo-control-plane --dry-run=client -o yaml | kubectl apply -f -
kubectl create secret generic backstage-secrets \
-n openchoreo-control-plane \
--from-literal=backend-secret="$(head -c 32 /dev/urandom | base64)" \
--from-literal=client-secret="backstage-portal-secret" \
--from-literal=jenkins-api-key="placeholder-not-in-use"
Install the Control Planeβ
helm upgrade --install openchoreo-control-plane oci://ghcr.io/openchoreo/helm-charts/openchoreo-control-plane \
--version 0.0.0-latest-dev \
--namespace openchoreo-control-plane \
--create-namespace \
--values https://raw.githubusercontent.com/openchoreo/openchoreo/main/install/k3d/single-cluster/values-cp.yaml
Wait for all deployments to come up:
kubectl wait -n openchoreo-control-plane \
--for=condition=available --timeout=300s deployment --all
Gateway Patch (Optional)β
On some platforms (especially macOS), the envoy proxy inside the gateway crashes because /tmp is not writable. This patch adds a writable volume. If you don't hit this issue you can skip it, but it's harmless either way. See kgateway#9800.
kubectl patch deployment gateway-default -n openchoreo-control-plane \
--type='json' -p='[{"op":"add","path":"/spec/template/spec/volumes/-","value":{"name":"tmp","emptyDir":{}}},{"op":"add","path":"/spec/template/spec/containers/0/volumeMounts/-","value":{"name":"tmp","mountPath":"/tmp"}}]'
What Got Installedβ
Here is what is now running in the openchoreo-control-plane namespace:
- controller-manager reconciles OpenChoreo resources (Projects, Components, Environments, etc.)
- openchoreo-api is the REST API the console and CLI talk to
- backstage is the web console
- thunder handles authentication and OAuth flows
- cluster-gateway accepts WebSocket connections from agents in remote planes
- gateway (managed by kgateway) routes external traffic to services
What the Bootstrap Configuredβ
The Thunder values file includes bootstrap scripts that run on first startup and configure everything the control plane needs. Here is what was created.
Organization Unit: A Default org unit (handle: default) that groups all users and resources.
User Type: An engineer type with self-registration enabled and four schema fields: username (string, required), password (string, required), given_name (string), family_name (string).
Admin User:
| Type | username | password | given_name | family_name |
|---|---|---|---|---|
engineer | admin@openchoreo.dev | Admin@123 | Admin | User |
Group: platformEngineer with the admin user as a member. This group controls access to platform features.
OAuth Applications:
| Application | Client ID | Type | Grant Types |
|---|---|---|---|
| Backstage | openchoreo-backstage-client | Confidential | authorization_code, client_credentials |
| OpenChoreo CLI | openchoreo-cli | Public (PKCE) | authorization_code, refresh_token |
| Default App | customer-portal-client | Confidential | client_credentials |
| RCA Agent | openchoreo-rca-agent | Confidential | client_credentials |
| System Application | openchoreo-system-app | Confidential | client_credentials |
| User MCP App | user_mcp_client | Public (PKCE) | authorization_code, refresh_token |
| Service MCP App | service_mcp_client | Confidential | client_credentials |
You can browse and modify these in the Thunder admin console at http://thunder.openchoreo.localhost:8080/develop.
| Username | Password |
|---|---|
admin | admin |
Try it: Log in to OpenChoreoβ
Open http://openchoreo.localhost:8080 in your browser.
Log in with the default credentials:
| Username | Password |
|---|---|
admin@openchoreo.dev | Admin@123 |
You should see the OpenChoreo console. The control plane is working.
Step 4: Install Default Resourcesβ
OpenChoreo needs some base resources before you can deploy anything: a project, environments, component types, and a deployment pipeline. These define what kinds of things you can build and where they run.
kubectl apply -f https://raw.githubusercontent.com/openchoreo/openchoreo/main/samples/getting-started/all.yaml
Label the default namespace as a control plane namespace:
kubectl label namespace default openchoreo.dev/controlplane-namespace=true
What was created:
- Project:
default - Environments: development, staging, production
- DeploymentPipeline: default (development -> staging -> production)
- ComponentTypes: service, web-application, scheduled-task, worker
- ComponentWorkflows: docker, google-cloud-buildpacks, ballerina-buildpack, react
- Traits: api-configuration, observability-alert-rule
Step 5: Setup Data Planeβ
The data plane is where your workloads actually run. It has its own gateway for routing traffic, and a cluster-agent that connects back to the control plane to receive deployment instructions.
Namespace and Certificatesβ
Each plane needs a copy of the cluster-gateway CA certificate so its agent can establish a trusted connection to the control plane. This is how planes authenticate with each other.
kubectl create namespace openchoreo-data-plane --dry-run=client -o yaml | kubectl apply -f -
CA_CRT=$(kubectl get configmap cluster-gateway-ca \
-n openchoreo-control-plane -o jsonpath='{.data.ca\.crt}')
kubectl create configmap cluster-gateway-ca \
--from-literal=ca.crt="$CA_CRT" \
-n openchoreo-data-plane
TLS_CRT=$(kubectl get secret cluster-gateway-ca \
-n openchoreo-control-plane -o jsonpath='{.data.tls\.crt}' | base64 -d)
TLS_KEY=$(kubectl get secret cluster-gateway-ca \
-n openchoreo-control-plane -o jsonpath='{.data.tls\.key}' | base64 -d)
kubectl create secret generic cluster-gateway-ca \
--from-literal=tls.crt="$TLS_CRT" \
--from-literal=tls.key="$TLS_KEY" \
--from-literal=ca.crt="$CA_CRT" \
-n openchoreo-data-plane
Secret Storeβ
OpenChoreo uses External Secrets Operator to inject secrets into workloads. In production you would point this at a real secrets provider. For local dev, a fake store with placeholder values is enough:
kubectl apply -f - <<EOF
apiVersion: external-secrets.io/v1
kind: ClusterSecretStore
metadata:
name: default
spec:
provider:
fake:
data:
- key: npm-token
value: "fake-npm-token-for-development"
- key: docker-username
value: "dev-user"
- key: docker-password
value: "dev-password"
- key: github-pat
value: "fake-github-token-for-development"
- key: username
value: "dev-user"
- key: password
value: "dev-password"
- key: RCA_LLM_API_KEY
value: "fake-llm-api-key-for-development"
EOF
Install the Data Planeβ
helm upgrade --install openchoreo-data-plane oci://ghcr.io/openchoreo/helm-charts/openchoreo-data-plane \
--version 0.0.0-latest-dev \
--namespace openchoreo-data-plane \
--create-namespace \
--values https://raw.githubusercontent.com/openchoreo/openchoreo/main/install/k3d/single-cluster/values-dp.yaml
Gateway Patch (Optional)β
Same envoy /tmp workaround as the control plane. Safe to apply even if you don't need it.
kubectl patch deployment gateway-default -n openchoreo-data-plane \
--type='json' -p='[{"op":"add","path":"/spec/template/spec/volumes/-","value":{"name":"tmp","emptyDir":{}}},{"op":"add","path":"/spec/template/spec/containers/0/volumeMounts/-","value":{"name":"tmp","mountPath":"/tmp"}}]'
Register the Data Planeβ
The DataPlane resource tells the control plane about this data plane. It includes the agent's CA certificate (so the control plane trusts its WebSocket connection) and the gateway's public address (so the control plane knows how to route traffic to workloads).
AGENT_CA=$(kubectl get secret cluster-agent-tls \
-n openchoreo-data-plane -o jsonpath='{.data.ca\.crt}' | base64 -d)
kubectl apply -f - <<EOF
apiVersion: openchoreo.dev/v1alpha1
kind: DataPlane
metadata:
name: default
namespace: default
spec:
planeID: default
clusterAgent:
clientCA:
value: |
$(echo "$AGENT_CA" | sed 's/^/ /')
secretStoreRef:
name: default
gateway:
publicVirtualHost: openchoreoapis.localhost
publicHTTPPort: 19080
publicHTTPSPort: 19443
EOF
The cluster-agent in the data plane establishes an outbound WebSocket connection to the control plane's cluster-gateway. The control plane sends deployment instructions over this connection. No inbound ports need to be opened on the data plane.
Try it: Deploy the React Starter Appβ
kubectl apply -f https://raw.githubusercontent.com/openchoreo/openchoreo/main/samples/from-image/react-starter-web-app/react-starter.yaml
Wait for the deployment to come up:
kubectl wait --for=condition=available deployment \
-l openchoreo.dev/component=react-starter -A --timeout=120s
Get the application URL:
HOSTNAME=$(kubectl get httproute -A -l openchoreo.dev/component=react-starter \
-o jsonpath='{.items[0].spec.hostnames[0]}')
echo "http://${HOSTNAME}:19080"
Open that URL in your browser. You should see the React starter application running.
The data plane is routing traffic to your workload through the gateway.
Step 6: Setup Build Plane (Optional)β
The build plane takes source code, builds a container image, pushes it to a registry, and tells the control plane about the new image. It uses Argo Workflows to run build pipelines.
Namespace and Certificatesβ
Same process as the data plane. Copy the cluster-gateway CA so the build plane's agent can connect to the control plane:
kubectl create namespace openchoreo-build-plane --dry-run=client -o yaml | kubectl apply -f -
CA_CRT=$(kubectl get configmap cluster-gateway-ca \
-n openchoreo-control-plane -o jsonpath='{.data.ca\.crt}')
kubectl create configmap cluster-gateway-ca \
--from-literal=ca.crt="$CA_CRT" \
-n openchoreo-build-plane
TLS_CRT=$(kubectl get secret cluster-gateway-ca \
-n openchoreo-control-plane -o jsonpath='{.data.tls\.crt}' | base64 -d)
TLS_KEY=$(kubectl get secret cluster-gateway-ca \
-n openchoreo-control-plane -o jsonpath='{.data.tls\.key}' | base64 -d)
kubectl create secret generic cluster-gateway-ca \
--from-literal=tls.crt="$TLS_CRT" \
--from-literal=tls.key="$TLS_KEY" \
--from-literal=ca.crt="$CA_CRT" \
-n openchoreo-build-plane
Container Registryβ
Builds need somewhere to push images. For local dev, a simple in-cluster Docker registry works:
helm repo add twuni https://twuni.github.io/docker-registry.helm
helm repo update
helm install registry twuni/docker-registry \
--namespace openchoreo-build-plane \
--create-namespace \
--values https://raw.githubusercontent.com/openchoreo/openchoreo/main/install/k3d/single-cluster/values-registry.yaml
Install the Build Planeβ
helm upgrade --install openchoreo-build-plane oci://ghcr.io/openchoreo/helm-charts/openchoreo-build-plane \
--version 0.0.0-latest-dev \
--namespace openchoreo-build-plane \
--values https://raw.githubusercontent.com/openchoreo/openchoreo/main/install/k3d/single-cluster/values-bp.yaml
Register the Build Planeβ
AGENT_CA=$(kubectl get secret cluster-agent-tls \
-n openchoreo-build-plane -o jsonpath='{.data.ca\.crt}' | base64 -d)
kubectl apply -f - <<EOF
apiVersion: openchoreo.dev/v1alpha1
kind: BuildPlane
metadata:
name: default
namespace: default
spec:
planeID: default
clusterAgent:
clientCA:
value: |
$(echo "$AGENT_CA" | sed 's/^/ /')
secretStoreRef:
name: openbao
EOF
Try it: Build from Sourceβ
Apply a sample component that builds a Go service from source:
kubectl apply -f https://raw.githubusercontent.com/openchoreo/openchoreo/main/samples/from-source/services/go-docker-greeter/greeting-service.yaml
Watch the build progress:
kubectl get workflow -n openchoreo-ci-default --watch
You can also open the Argo Workflows UI at http://localhost:10081 to see the build pipeline visually.
After the build completes, wait for the deployment:
kubectl wait --for=condition=available deployment \
-l openchoreo.dev/component=greeting-service -A --timeout=300s
Resolve the hostname and path, then call the service:
HOSTNAME=$(kubectl get httproute -A -l openchoreo.dev/component=greeting-service \
-o jsonpath='{.items[0].spec.hostnames[0]}')
PATH_PREFIX=$(kubectl get httproute -A -l openchoreo.dev/component=greeting-service \
-o jsonpath='{.items[0].spec.rules[0].matches[0].path.value}')
curl "http://${HOSTNAME}:19080${PATH_PREFIX}/greeter/greet"
OpenChoreo built your code, pushed the image to the local registry, and deployed it to the data plane.
Step 7: Setup Observability Plane (Optional)β
The observability plane collects logs and metrics from all other planes. It runs OpenSearch for storage, Fluent Bit for log collection, and an Observer API for querying.
Namespace and Certificatesβ
kubectl create namespace openchoreo-observability-plane --dry-run=client -o yaml | kubectl apply -f -
CA_CRT=$(kubectl get configmap cluster-gateway-ca \
-n openchoreo-control-plane -o jsonpath='{.data.ca\.crt}')
kubectl create configmap cluster-gateway-ca \
--from-literal=ca.crt="$CA_CRT" \
-n openchoreo-observability-plane
TLS_CRT=$(kubectl get secret cluster-gateway-ca \
-n openchoreo-control-plane -o jsonpath='{.data.tls\.crt}' | base64 -d)
TLS_KEY=$(kubectl get secret cluster-gateway-ca \
-n openchoreo-control-plane -o jsonpath='{.data.tls\.key}' | base64 -d)
kubectl create secret generic cluster-gateway-ca \
--from-literal=tls.crt="$TLS_CRT" \
--from-literal=tls.key="$TLS_KEY" \
--from-literal=ca.crt="$CA_CRT" \
-n openchoreo-observability-plane
OpenSearch Credentialsβ
The Observer API needs credentials to connect to OpenSearch:
kubectl create secret generic observer-opensearch-credentials \
-n openchoreo-observability-plane \
--from-literal=username="admin" \
--from-literal=password="ThisIsTheOpenSearchPassword1"
Install the Observability Planeβ
helm upgrade --install openchoreo-observability-plane oci://ghcr.io/openchoreo/helm-charts/openchoreo-observability-plane \
--version 0.0.0-latest-dev \
--namespace openchoreo-observability-plane \
--values https://raw.githubusercontent.com/openchoreo/openchoreo/main/install/k3d/single-cluster/values-op.yaml \
--set openSearch.enabled=true \
--set openSearchCluster.enabled=false \
--set fluent-bit.enabled=true \
--timeout 10m
Gateway Patch (Optional)β
Same envoy /tmp workaround as the other planes.
kubectl patch deployment gateway-default -n openchoreo-observability-plane \
--type='json' -p='[{"op":"add","path":"/spec/template/spec/volumes/-","value":{"name":"tmp","emptyDir":{}}},{"op":"add","path":"/spec/template/spec/containers/0/volumeMounts/-","value":{"name":"tmp","mountPath":"/tmp"}}]'
Register the Observability Planeβ
AGENT_CA=$(kubectl get secret cluster-agent-tls \
-n openchoreo-observability-plane -o jsonpath='{.data.ca\.crt}' | base64 -d)
kubectl apply -f - <<EOF
apiVersion: openchoreo.dev/v1alpha1
kind: ObservabilityPlane
metadata:
name: default
namespace: default
spec:
planeID: default
clusterAgent:
clientCA:
value: |
$(echo "$AGENT_CA" | sed 's/^/ /')
observerURL: http://observer.openchoreo.localhost:11080
EOF
Link Other Planes to Observabilityβ
Tell the data plane (and build plane, if installed) where to send their telemetry:
kubectl patch dataplane default -n default --type merge \
-p '{"spec":{"observabilityPlaneRef":{"kind":"ObservabilityPlane","name":"default"}}}'
# If you installed the build plane:
kubectl patch buildplane default -n default --type merge \
-p '{"spec":{"observabilityPlaneRef":{"kind":"ObservabilityPlane","name":"default"}}}'
Cleanupβ
Delete the cluster and everything in it:
k3d cluster delete openchoreo
Next Stepsβ
- Explore the sample applications
- Read the Deployment Topology guide for production setups
- Learn about Multi-Cluster Connectivity for separating planes across clusters