Skip to main content

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.
Before you start, ensure you have:
  • A running Kubernetes cluster (see EKS Cluster Setup if you need one)
  • Decided on your deployment manager connectivity — public TLS or PrivateLink
  • Your organizationId, environmentId, and deploymentManagerApiKey from xpander
  • A domain managed in Route 53 (or ability to create DNS records)

1. Create a Self-Hosted Location

  1. Go to 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.
Already have an ingress controller? Skip to step 4 (Install the Helm Chart) and set ingress.enabled=true.

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.

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.
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.
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>:
aws acm describe-certificate --certificate-arn <CERT_ARN> \
  --region <REGION> --profile <PROFILE> \
  --query 'Certificate.DomainValidationOptions[0].ResourceRecord'
Wait for validation:
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:
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):
kubectl get svc -n ingress-nginx ingress-nginx-controller \
  -o jsonpath='{.status.loadBalancer.ingress[0].hostname}'

4. Install the Helm Chart

Add the Repository

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)
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:
ParameterDescription
global.organizationIdYour xpander.ai organization ID
global.environmentIdYour xpander.ai environment ID
secrets.static.deploymentManagerApiKeyDeployment manager API key
domainBase domain for ingress hosts
urls.deploymentManagerDeployment manager URL (public or PrivateLink)
Optional API keys — add during install or later:
  --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:
xpander-values.yaml
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"
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>.
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:
ServiceURL
APIhttps://api.<DOMAIN>
Chathttps://chat.<DOMAIN>
AI Gatewayhttps://ai-gateway.<DOMAIN>
Agent Controllerhttps://agent-controller.<DOMAIN>
Agent Workerhttps://agent-worker.<DOMAIN>
MCPhttps://mcp.<DOMAIN>
Code Runnerhttps://code-runner.<DOMAIN>
AWS Operatorhttps://aws-operator.<DOMAIN>

6. Verification

Check All Pods Are Running

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
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

curl -sk https://api.<DOMAIN>/health

Test via Port Forward

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
  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:
helm upgrade xpander xpander/xpander \
  --namespace xpander --reuse-values \
  --set secrets.static.anthropicApiKey=<KEY> \
  --timeout 5m
After the upgrade, pods that use the updated secrets will automatically restart. Verify with:
kubectl get pods -n xpander
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:
# 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 ValueSecret Field
secrets.static.anthropicApiKeyANTHROPIC_API_KEY
secrets.static.agentsOpenaiApiKeyAGENTS_OPENAI_API_KEY
secrets.static.fireworksApiKeyFIREWORKS_API_KEY
secrets.static.awsBedrockApiKeyAWS_BEARER_TOKEN_BEDROCK
secrets.static.googleApiKeyGOOGLE_API_KEY
secrets.static.nebiusApiKeyNEBIUS_API_KEY
secrets.static.heliconeApiKeyHELICONE_API_KEY
secrets.static.openrouterApiKeyOPENROUTER_API_KEY
secrets.static.deploymentManagerApiKeyDEPLOYMENT_MANAGER_API_KEY
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
# 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.
When using self-hosted deployment, use the Agent Controller API key generated during Helm installation, not your xpander.ai cloud API key.
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

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?")
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.

Upgrading

# 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

# Remove the Helm release
helm uninstall xpander --namespace xpander

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