> ## Documentation Index
> Fetch the complete documentation index at: https://docs.xpander.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Install the Helm Chart

> SSL certificates, ingress controller, Helm chart installation, DNS configuration, API keys, verification, and SDK setup for xpander.ai

## Overview

This guide covers everything needed to deploy xpander.ai onto your Kubernetes cluster — SSL certificates, ingress controller, Helm chart installation, DNS configuration, API key management, verification, and SDK setup.

<Warning>
  **Before you start**, ensure you have:

  * A running Kubernetes cluster (see [EKS Cluster Setup](/self-hosted/eks-setup) if you need one)
  * Decided on your deployment manager connectivity — [public TLS or PrivateLink](/self-hosted/privatelink)
  * Your `organizationId`, `environmentId`, and `deploymentManagerApiKey` from xpander
  * A domain managed in Route 53 (or ability to create DNS records)
</Warning>

***

## 1. Create a Self-Hosted Location

1. Go to [app.xpander.ai/admin\_settings#locations](https://app.xpander.ai/admin_settings#locations)
2. Click **"Add Location"**
3. Fill in the details:
   * **Name**: e.g., "Production"
   * **URL** (optional): Your server's FQDN (e.g., `xpander.my-company.com`)
   * **Use dash-based subdomains**: Enable if your DNS uses `chat-xpander.domain.com` format instead of `chat.xpander.domain.com`
4. Click **"Continue to connect location"**

The platform will generate your Helm installation commands with all required keys — including your `organizationId`, `environmentId`, and `deploymentManagerApiKey`.

After deployment, the location page shows a **Components** tab with heartbeat status for each service, and a **Configuration** panel where you can toggle Cloud Controls (Chat, Streaming, Scheduler).

***

## 2. External Access (Choose One)

The xpander services (agent controller, chat UI, API, etc.) communicate with each other internally via Kubernetes Services — no ingress is needed for that. You only need external access so that **users** can reach the chat UI, API, and other endpoints from their browser or client.

<Info>
  **Already have an ingress controller?** Skip to [step 4 (Install the Helm Chart)](#4-install-the-helm-chart) and set `ingress.enabled=true`.
</Info>

### Option A: VPN / Corporate Network Access

If your organization has a VPN that routes into the VPC, users can access xpander services directly — no ingress controller, NLB, ACM certificate, or public DNS needed.

Point internal DNS records to the Kubernetes services:

```
chat.<DOMAIN>              → xpander-chat.xpander.svc.cluster.local
agent-controller.<DOMAIN>  → xpander-agent-controller.xpander.svc.cluster.local
ai-gateway.<DOMAIN>        → xpander-ai-gateway.xpander.svc.cluster.local
```

When installing the Helm chart, set `ingress.enabled=false` (the default). Skip ahead to [step 4](#4-install-the-helm-chart).

### Option B: Public Access via Ingress + NLB

For public or internet-facing access, set up an ACM certificate, nginx ingress controller, and NLB.

#### SSL Certificate

Request an ACM certificate for your domain.

<Warning>
  The certificate **must** include a wildcard for `*.chat.<DOMAIN>` because the xpander chat UI generates per-thread subdomains (e.g., `moccasin-prawn.chat.<DOMAIN>`). Standard wildcards only match one level, so `*.<DOMAIN>` does not cover these. If you omit `*.chat.<DOMAIN>`, the chat UI will show SSL certificate errors for thread URLs.
</Warning>

```bash theme={"dark"}
aws acm request-certificate \
  --domain-name "<DOMAIN>" \
  --subject-alternative-names "*.<DOMAIN>" "*.chat.<DOMAIN>" \
  --validation-method DNS \
  --region <REGION> --profile <PROFILE>
```

Add the DNS validation CNAME records to your Route 53 hosted zone. There will be two unique validation records — one for `<DOMAIN>` / `*.<DOMAIN>` (shared) and one for `*.chat.<DOMAIN>`:

```bash theme={"dark"}
aws acm describe-certificate --certificate-arn <CERT_ARN> \
  --region <REGION> --profile <PROFILE> \
  --query 'Certificate.DomainValidationOptions[0].ResourceRecord'
```

Wait for validation:

```bash theme={"dark"}
aws acm wait certificate-validated --certificate-arn <CERT_ARN> \
  --region <REGION> --profile <PROFILE>
```

#### Ingress Controller

Install the nginx ingress controller with AWS NLB and ACM SSL termination:

```bash theme={"dark"}
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update

helm upgrade --install ingress-nginx ingress-nginx/ingress-nginx \
  --namespace ingress-nginx --create-namespace \
  --set controller.service.type=LoadBalancer \
  --set controller.service.annotations."service\.beta\.kubernetes\.io/aws-load-balancer-type"=nlb \
  --set controller.service.annotations."service\.beta\.kubernetes\.io/aws-load-balancer-scheme"=internet-facing \
  --set controller.service.annotations."service\.beta\.kubernetes\.io/aws-load-balancer-ssl-cert"="<ACM_CERT_ARN>" \
  --set controller.service.annotations."service\.beta\.kubernetes\.io/aws-load-balancer-ssl-ports"="443" \
  --set controller.service.annotations."service\.beta\.kubernetes\.io/aws-load-balancer-backend-protocol"="tcp" \
  --set controller.service.targetPorts.https=http
```

Get the NLB hostname (you'll need this for DNS in [step 5](#5-dns-configuration)):

```bash theme={"dark"}
kubectl get svc -n ingress-nginx ingress-nginx-controller \
  -o jsonpath='{.status.loadBalancer.ingress[0].hostname}'
```

***

## 4. Install the Helm Chart

### Add the Repository

```bash theme={"dark"}
helm repo add xpander https://charts.xpander.ai
helm repo update
```

### Install

Set the deployment manager URL based on your connectivity choice:

* **Public:** `https://deployment-manager.xpander.ai`
* **PrivateLink:** `https://deployment-manager-privatelink.xpander.ai` (requires [PrivateLink setup](/self-hosted/privatelink))

```bash theme={"dark"}
helm upgrade --install xpander xpander/xpander \
  --namespace xpander --create-namespace \
  --set ingress.enabled=true \
  --set domain=<DOMAIN> \
  --set global.organizationId=<ORGANIZATION_ID> \
  --set global.environmentId=<ENVIRONMENT_ID> \
  --set secrets.static.deploymentManagerApiKey=<DEPLOYMENT_MANAGER_API_KEY> \
  --set urls.deploymentManager=<DEPLOYMENT_MANAGER_URL> \
  --timeout 10m
```

### Configuration Parameters

**Required:**

| Parameter                                | Description                                    |
| ---------------------------------------- | ---------------------------------------------- |
| `global.organizationId`                  | Your xpander.ai organization ID                |
| `global.environmentId`                   | Your xpander.ai environment ID                 |
| `secrets.static.deploymentManagerApiKey` | Deployment manager API key                     |
| `domain`                                 | Base domain for ingress hosts                  |
| `urls.deploymentManager`                 | Deployment manager URL (public or PrivateLink) |

**Optional API keys** — add during install or later:

```bash theme={"dark"}
  --set secrets.static.agentsOpenaiApiKey=<KEY> \
  --set secrets.static.anthropicApiKey=<KEY> \
  --set secrets.static.fireworksApiKey=<KEY> \
  --set secrets.static.awsBedrockApiKey=<KEY> \
  --set secrets.static.googleApiKey=<KEY> \
  --set secrets.static.nebiusApiKey=<KEY> \
  --set secrets.static.heliconeApiKey=<KEY> \
  --set secrets.static.openrouterApiKey=<KEY>
```

### Using a Values File

For production, use a values file instead of `--set` flags:

```yaml xpander-values.yaml theme={"dark"}
domain: "xpander.production.com"

global:
  organizationId: "your-org-id"
  environmentId: "your-env-id"
  env:
    LOG_LEVEL: "info"
    ENVIRONMENT: "production"

urls:
  deploymentManager: "https://deployment-manager.xpander.ai"

secrets:
  static:
    deploymentManagerApiKey: "your-key"
    agentsOpenaiApiKey: "sk-your-openai-key"
    anthropicApiKey: "sk-ant-your-anthropic-key"

ingress:
  enabled: true
  tls:
    enabled: true
    source: "cert-manager"
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"

resources:
  agentController:
    limits:
      cpu: "1000m"
      memory: "1Gi"

redis:
  storage:
    size: "32Gi"
    storageClass: "fast-ssd"
```

```bash theme={"dark"}
helm upgrade --install xpander xpander/xpander \
  --namespace xpander --create-namespace \
  --values xpander-values.yaml
```

***

## 5. DNS Configuration

Create wildcard CNAME records pointing `*.<DOMAIN>` and `*.chat.<DOMAIN>` to the NLB hostname. Both are required — the chat UI generates per-thread subdomains under `chat.<DOMAIN>`.

```bash theme={"dark"}
cat <<EOF > /tmp/dns-record.json
{
  "Changes": [
    {
      "Action": "UPSERT",
      "ResourceRecordSet": {
        "Name": "*.<DOMAIN>.",
        "Type": "CNAME",
        "TTL": 300,
        "ResourceRecords": [{ "Value": "<NLB_HOSTNAME>" }]
      }
    },
    {
      "Action": "UPSERT",
      "ResourceRecordSet": {
        "Name": "*.chat.<DOMAIN>.",
        "Type": "CNAME",
        "TTL": 300,
        "ResourceRecords": [{ "Value": "<NLB_HOSTNAME>" }]
      }
    }
  ]
}
EOF

aws route53 change-resource-record-sets \
  --hosted-zone-id <HOSTED_ZONE_ID> \
  --change-batch file:///tmp/dns-record.json \
  --profile <PROFILE>
```

This creates the following endpoints:

| Service          | URL                                 |
| ---------------- | ----------------------------------- |
| API              | `https://api.<DOMAIN>`              |
| Chat             | `https://chat.<DOMAIN>`             |
| AI Gateway       | `https://ai-gateway.<DOMAIN>`       |
| Agent Controller | `https://agent-controller.<DOMAIN>` |
| Agent Worker     | `https://agent-worker.<DOMAIN>`     |
| MCP              | `https://mcp.<DOMAIN>`              |
| Code Runner      | `https://code-runner.<DOMAIN>`      |
| AWS Operator     | `https://aws-operator.<DOMAIN>`     |

***

## 6. Verification

### Check All Pods Are Running

```bash theme={"dark"}
kubectl get pods -n xpander
```

All pods should show `Running` with `1/1` ready:

```
xpander-agent-controller    1/1     Running
xpander-agent-worker        1/1     Running
xpander-ai-gateway          1/1     Running
xpander-api                 1/1     Running
xpander-aws-operator        1/1     Running
xpander-chat                1/1     Running
xpander-code-runner         1/1     Running
xpander-docker-registry     1/1     Running
xpander-mcp                 1/1     Running
xpander-postgres-0          1/1     Running
xpander-redis-0             1/1     Running
```

### Test PrivateLink (If Using PrivateLink)

```bash theme={"dark"}
kubectl run curl-test --restart=Never --image=curlimages/curl:latest -n xpander \
  --command -- sh -c "curl -sk -o /dev/null -w '%{http_code}' \
  https://deployment-manager-privatelink.xpander.ai/health; echo"
sleep 15 && kubectl logs curl-test -n xpander
# Expected: 200
kubectl delete pod curl-test -n xpander
```

### Test Ingress

```bash theme={"dark"}
curl -sk https://api.<DOMAIN>/health
```

### Test via Port Forward

```bash theme={"dark"}
kubectl -n xpander port-forward service/xpander-agent-controller 9016:9016 &
kubectl -n xpander port-forward service/xpander-ai-gateway 9018:9018 &
kubectl -n xpander port-forward service/xpander-mcp 8081:8081 &

curl http://localhost:9016/health
curl http://localhost:9018/health
curl http://localhost:8081/health
```

### Connect in xpander.ai Console

1. Go to [app.xpander.ai/admin\_settings#locations](https://app.xpander.ai/admin_settings#locations)
2. Find your location — it should show as **"Self-deploy"**
3. Click the **Components** tab to verify all services show **Alive** status with recent heartbeats

***

## Managing API Keys

### Adding or Updating Keys After Installation

Use `helm upgrade` with `--reuse-values` to add or change API keys without affecting existing configuration:

<CodeGroup>
  ```bash Single Key theme={"dark"}
  helm upgrade xpander xpander/xpander \
    --namespace xpander --reuse-values \
    --set secrets.static.anthropicApiKey=<KEY> \
    --timeout 5m
  ```

  ```bash Multiple Keys theme={"dark"}
  helm upgrade xpander xpander/xpander \
    --namespace xpander --reuse-values \
    --set secrets.static.anthropicApiKey=<KEY> \
    --set secrets.static.agentsOpenaiApiKey=<KEY> \
    --set secrets.static.fireworksApiKey=<KEY> \
    --timeout 5m
  ```
</CodeGroup>

After the upgrade, pods that use the updated secrets will automatically restart. Verify with:

```bash theme={"dark"}
kubectl get pods -n xpander
```

<Accordion title="Secret not updating after helm upgrade?">
  The `xpander-static` secret has a Helm resource keep policy — `helm upgrade` may not update it on subsequent installs. If your API key isn't being picked up after a helm upgrade, set it directly in the secret:

  ```bash theme={"dark"}
  # Set the key directly in the Kubernetes secret
  kubectl patch secret xpander-static -n xpander --type=merge \
    -p "{\"data\":{\"ANTHROPIC_API_KEY\":\"$(echo -n '<YOUR_KEY>' | base64)\"}}"

  # Restart the worker to pick up the new key
  kubectl rollout restart deployment xpander-agent-worker -n xpander
  ```

  **Secret field name mapping:**

  | Helm Value                               | Secret Field                 |
  | ---------------------------------------- | ---------------------------- |
  | `secrets.static.anthropicApiKey`         | `ANTHROPIC_API_KEY`          |
  | `secrets.static.agentsOpenaiApiKey`      | `AGENTS_OPENAI_API_KEY`      |
  | `secrets.static.fireworksApiKey`         | `FIREWORKS_API_KEY`          |
  | `secrets.static.awsBedrockApiKey`        | `AWS_BEARER_TOKEN_BEDROCK`   |
  | `secrets.static.googleApiKey`            | `GOOGLE_API_KEY`             |
  | `secrets.static.nebiusApiKey`            | `NEBIUS_API_KEY`             |
  | `secrets.static.heliconeApiKey`          | `HELICONE_API_KEY`           |
  | `secrets.static.openrouterApiKey`        | `OPENROUTER_API_KEY`         |
  | `secrets.static.deploymentManagerApiKey` | `DEPLOYMENT_MANAGER_API_KEY` |
</Accordion>

### Using Kubernetes Secrets (Recommended for Production)

```bash theme={"dark"}
kubectl create secret generic ai-service-keys \
  --namespace xpander \
  --from-literal=openai-api-key=sk-your-openai-key \
  --from-literal=anthropic-api-key=sk-ant-your-anthropic-key
```

```yaml theme={"dark"}
# In your values file
agent-worker:
  envFromSecretKeys:
    AGENTS_OPENAI_API_KEY:
      secretName: "ai-service-keys"
      key: "openai-api-key"
    ANTHROPIC_API_KEY:
      secretName: "ai-service-keys"
      key: "anthropic-api-key"
```

***

## Using the SDK with Self-Hosted

Configure the xpander SDK to point to your Agent Controller endpoint.

<Note>
  When using self-hosted deployment, use the **Agent Controller API key** generated during Helm installation, not your xpander.ai cloud API key.
</Note>

```python theme={"dark"}
from xpander_sdk import Configuration

config = Configuration(
    api_key="your-agent-controller-api-key",  # From Helm installation
    organization_id="your-org-id",
    base_url="https://agent-controller.my-company.com"
)
```

### Using with Agno Framework

```python theme={"dark"}
from xpander_sdk import Backend, Configuration
from agno.agent import Agent

config = Configuration(
    api_key="your-agent-controller-api-key",  # From Helm installation
    organization_id="your-org-id",
    base_url="https://agent-controller.my-company.com"
)

backend = Backend(configuration=config)
agno_agent = Agent(**backend.get_args(agent_id="agent-123"))

result = await agno_agent.arun(input="What can you help me with?")
```

<Note>
  Make sure your `base_url` points to the Agent Controller endpoint (e.g., `https://agent-controller.{your-domain}`), not the root domain.

  For more SDK examples, see the [Self-Hosted SDK Configuration](/api-reference/configuration/self-hosted).
</Note>

***

## Upgrading

```bash theme={"dark"}
# Update to latest version
helm repo update
helm upgrade xpander xpander/xpander \
  --namespace xpander \
  --reuse-values

# Or with new configuration
helm upgrade xpander xpander/xpander \
  --namespace xpander \
  --values xpander-values.yaml
```

## Uninstalling

```bash theme={"dark"}
# Remove the Helm release
helm uninstall xpander --namespace xpander

# Clean up the namespace (optional)
kubectl delete namespace xpander
```
