kubernetes changedmanaged-agents/self-hosted/kubernetes
## Prerequisites ## Install the stack ## Run a session ## Put TLS in front of the server ## What sandboxes can reach ## Other clusters ## Free a session’s volume ## Renew keys and certificates ### Renew the environment key that the charts created ### Renew the dispatcher’s certificate ### Replace an environment key for your own workers ## Uninstall the stack ## Next steps
The whole hunk
380 lines, new pageA whole new page. There's nothing to diff it against, so here is what it says.
---
title: Deploy on Kubernetes
url: https://platform.claude.com/docs/en/managed-agents/self-hosted/kubernetes
description: Install the self-hosted Claude Managed Agents server and its sandboxes on a Kubernetes cluster with the two Helm charts in the release archive, then run a first session in a sandbox pod.
---
<Note>
To request access to self-hosted Claude Managed Agents, contact your Anthropic account team.
</Note>
This page takes an empty Google Kubernetes Engine (GKE) cluster to a session that runs a tool in a sandbox pod. It uses the two Helm charts in the release archive, which run the whole stack on the cluster:
* **`cma-server`:** Applies the database schema and runs two servers behind a Service, in the namespace `cma-server`.
* **`cma-sandboxes`:** Runs a dispatcher and the sandbox pods in the namespace `cma`. The dispatcher polls the server for work and gives each session its own sandbox pod, where a worker runs the agent’s shell and file tools.
Your PostgreSQL database stays outside both. On another kind of cluster, the steps are the same, but the cluster prerequisites and a few chart values differ. See [Other clusters](https://platform.claude.com/docs/en/managed-agents/self-hosted/kubernetes#other-clusters).
## Prerequisites
* **A cluster:** Kubernetes 1.30 or later, with:
* The Agent Sandbox controller (on GKE, the [Agent Sandbox add-on](https://docs.cloud.google.com/kubernetes-engine/docs/how-to/how-install-agent-sandbox))
* Nodes that run gVisor (on GKE, a [GKE Sandbox node pool](https://docs.cloud.google.com/kubernetes-engine/docs/how-to/sandbox-pods))
* A network plugin that enforces NetworkPolicy (on GKE, Dataplane V2 or Calico)
* **A database:** PostgreSQL 16 or later that the cluster can reach.
* **A registry:** One that the cluster can pull from. You build the images yourself.
* **A Claude API key:** The server uses it for model calls. To use another provider, see [Inference provider](https://platform.claude.com/docs/en/managed-agents/self-hosted/inference-provider).
* **Permissions:** Cluster-admin rights or the equivalent. The install creates namespaces, Roles, and a ClusterRoleBinding.
* **Tools on your machine:** `helm`, `kubectl`, `docker buildx`, `openssl`, and the [`ant` CLI](https://platform.claude.com/docs/en/cli-sdks-libraries/cli/quickstart#installation).
* **The release archive:** To request access to the download, contact your Anthropic account team. Extract the archive. Run every command on this page from its top directory.
## Install the stack
<Steps>
<Step title="Build and push the images">
Set these variables to your own values. `PLATFORM` is your nodes’ architecture. `TAG` is the release’s version, which is in the archive’s name after the `v`. Both charts expect images that have this version as their tag.
```bash
REGISTRY=us-docker.pkg.dev/my-project/cma
TAG=1.2.3
PLATFORM=linux/amd64
```
Then build and push the three images:
```bash
docker buildx build --platform "$PLATFORM" -f kubernetes/server/Dockerfile -t "$REGISTRY/cma-server-image:$TAG" --push .
docker buildx build --platform "$PLATFORM" -f kubernetes/dispatcher/Dockerfile -t "$REGISTRY/cma-dispatcher:$TAG" --push .
docker buildx build --platform "$PLATFORM" -t "$REGISTRY/cma-sandbox:$TAG" --push kubernetes/sandbox
```
</Step>
<Step title="Create a secrets file and the database">
Create a file with a generated database password and a placeholder for your Claude API key:
```bash
cat > secrets.env <<EOF
PG_PASSWORD=$(openssl rand -hex 32)
UPSTREAM_KEY=sk-ant-...
EOF
chmod 600 secrets.env
```
Open `secrets.env` in an editor and replace `sk-ant-...` with your Claude API key. Then connect to your database server as an administrator, for example, with `psql`. Run this SQL, with the value of `PG_PASSWORD` in place of `...`:
```sql
CREATE ROLE cma_server LOGIN PASSWORD '...';
GRANT cma_server TO CURRENT_USER;
CREATE DATABASE cma_server OWNER cma_server;
```
</Step>
<Step title="Create the server’s namespace and Secrets">
Create the server’s namespace and its Secrets:
```bash
source secrets.env
ENCRYPTION_KEY=$(openssl rand -base64 32)
kubectl create namespace cma-server
kubectl label namespace cma-server pod-security.kubernetes.io/enforce=restricted pod-security.kubernetes.io/enforce-version=latest
kubectl -n cma-server create secret generic cma-server-db \
--from-literal=PGPASSWORD="$PG_PASSWORD"
kubectl -n cma-server create secret generic cma-server-keys \
--from-literal=CMA_SERVER__API_KEY="$(openssl rand -hex 32)" \
--from-literal=CMA_STORE__CREDENTIALS_ENCRYPTION_KEY="$ENCRYPTION_KEY"
kubectl -n cma-server create secret generic cma-server-upstream \
--from-literal=ANTHROPIC_API_KEY="$UPSTREAM_KEY"
```
Print the encryption key with `echo "$ENCRYPTION_KEY"`, and keep a copy outside the cluster. The server uses this key to encrypt the credentials that it stores, and a restored database needs it. Then delete `secrets.env`. The `cma-server-db` Secret keeps the database password.
</Step>
<Step title="Create the sandboxes’ namespace and certificate">
Create the sandboxes’ namespace, and a certificate that the dispatcher serves and the sandboxes trust:
```bash
kubectl create namespace cma
kubectl label namespace cma pod-security.kubernetes.io/enforce=restricted pod-security.kubernetes.io/enforce-version=latest
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -days 825 \
-subj "/CN=cma-dispatcher.cma.svc" \
-addext "subjectAltName=DNS:cma-dispatcher.cma.svc,DNS:cma-dispatcher.cma.svc.cluster.local" \
-keyout tls.key -out tls.crt
kubectl -n cma create secret tls cma-dispatcher-tls --cert=tls.crt --key=tls.key --dry-run=client -o yaml | kubectl apply -f -
kubectl -n cma create configmap cma-dispatcher-ca --from-file=ca.crt=tls.crt --dry-run=client -o yaml | kubectl apply -f -
```
</Step>
<Step title="Install the server">
Save this as `cma-server-values.yaml`, with your registry and your database’s address:
```yaml
earlierNames: false # a new install
image:
registry: us-docker.pkg.dev/my-project/cma # REGISTRY in the build commands
database:
url: host=10.20.0.5 port=5432 user=cma_server dbname=cma_server sslmode=require # your database's address
environments:
- name: default
namespace: cma
```
Then install the chart:
```bash
helm install cma-server kubernetes/server/chart --namespace cma-server -f cma-server-values.yaml --wait
kubectl -n cma-server logs job/cma-server-environment-default
```
The install creates the environment `default` and stores its key in the Secret `cma-environment` in the namespace `cma`. The second command prints one line that starts `filled Secret cma/cma-environment`. If a first `helm install` fails, run `helm uninstall` for that release before you install again.
</Step>
<Step title="Find the cluster addresses to block">
This install lets sandboxes reach the public internet. A cluster’s API endpoint usually has a public address, so sandboxes could reach it too. To block it, you list its address under `egress.clusterCIDRs` in the next step.
To print the address, run `gcloud container clusters describe my-cluster --format 'value(endpoint)'` with your cluster’s name.
The chart itself blocks the private ranges `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, and `100.64.0.0/10`. If your cluster has a pod, service, or node range outside those four, list that range as well.
</Step>
<Step title="Install the sandboxes">
Save this as `cma-values.yaml`, with your registry and the addresses from the previous step:
```yaml
platform: gke
image:
registry: us-docker.pkg.dev/my-project/cma # REGISTRY in the build commands
egress:
mode: publicInternet
clusterCIDRs:
- 203.0.113.10/32 # the cluster's API endpoint
cmaServer:
pods:
namespace: cma-server
matchLabels:
anthropic.com/role: server
port: 8400
```
`egress.cmaServer` opens the path to your servers. To compare `publicInternet` with the two other modes, see [What sandboxes can reach](https://platform.claude.com/docs/en/managed-agents/self-hosted/kubernetes#what-sandboxes-can-reach).
If your cluster uses one of these DNS setups, add a line to the file:
* **Cloud DNS for GKE, which Autopilot clusters use:** `gke: {dns: cloudDns}`
* **NodeLocal DNSCache:** `gke: {dns: nodeLocalCache, kubeDnsUpstreamIP: <address>}`. To print the address, run `kubectl -n kube-system get svc kube-dns-upstream -o jsonpath='{.spec.clusterIP}'`.
Then install the chart:
```bash
helm install cma kubernetes/chart --namespace cma -f cma-values.yaml --wait
```
</Step>
</Steps>
## Run a session
These steps use a new install’s names. If your server values file has `earlierNames: true`, find the names to use instead in [Kubernetes: the names of your objects](https://platform.claude.com/docs/en/managed-agents/self-hosted/upgrade#object-names).
<Steps>
<Step title="Forward the server’s port">
In one terminal, forward the server’s port to your machine:
```bash
kubectl -n cma-server port-forward svc/cma-server 8400:8400
```
</Step>
<Step title="Point the CLI at your server">
In a second terminal, point the `ant` CLI at the forwarded port and read the server API key from the `cma-server-keys` Secret:
```bash
export ANTHROPIC_BASE_URL=http://127.0.0.1:8400
export ANTHROPIC_API_KEY=$(kubectl -n cma-server get secret cma-server-keys -o jsonpath='{.data.CMA_SERVER__API_KEY}' | base64 -d)
```
</Step>
<Step title="Create an agent and ask it to run a command">
Follow three steps of the quickstart’s [Run your first session](https://platform.claude.com/docs/en/managed-agents/self-hosted/quickstart#run-your-first-session): Create an agent, Start a session, and Ask the agent to run a command. In Start a session, replace `compose-sandbox` with `default`, the name of the environment that the charts create.
</Step>
<Step title="Find the sandbox pod">
The agent replies with the hostname of the pod that ran the command. In a third terminal, `kubectl -n cma get pods` lists a pod of that name beside the dispatcher’s. By default, the pod goes away about 5 minutes after the agent’s turn ends. If it isn’t in the list, ask the agent to run `hostname` again.
To leave the chat, press Ctrl+C.
</Step>
</Steps>
## Put TLS in front of the server
The server speaks plain HTTP on port 8400, and requests from your applications carry the server API key. Before clients outside the cluster connect, put a load balancer or ingress that terminates TLS in front of the `cma-server` Service. It must:
* Pass request paths through unchanged.
* Accept request bodies up to 32 MiB.
* Pass responses through unbuffered.
* Set a default read timeout of 2 minutes. For Git requests, set a read timeout of 10 minutes for paths matching `^/cma-[a-z]+/git/`.
* Send traffic only to servers that are ready. The `cma-server` Service already does this. A load balancer that reaches the pods directly must probe `/cma-server/readyz` on each pod and skip one that returns `503`.
* Never resend a `POST` that a server already accepted.
* Never cache a response.
To check it from outside the cluster, run `curl -s https://<host>/cma-server/healthz`. The response is `{"status":"ok"}`.
## What sandboxes can reach
In `cma-values.yaml`, `egress.mode: publicInternet` lets sandboxes reach the public internet.
This table compares it with the two other modes:
| `egress.mode` | What sandboxes can reach |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `closed`, the default | Your servers and the dispatcher, and no DNS server |
| `publicInternet` | Your servers, the dispatcher, a DNS server, and the public internet |
| `gateway` | Your servers, the dispatcher, and, through the egress gateway, the hosts that you list. See [Turn on the gateway on Kubernetes](https://platform.claude.com/docs/en/managed-agents/self-hosted/agent-identity#turn-on-the-gateway-on-kubernetes). |
## Other clusters
Set `platform` in `cma-values.yaml` to `eks` for Amazon EKS, to `aks` for Azure Kubernetes Service (AKS), or to `generic` for any other cluster. All three need the upstream Agent Sandbox controller with its extensions, and a network plugin that enforces NetworkPolicy. They also need:
* **Amazon EKS:** A node group with gVisor’s `runsc` registered as a containerd handler named `runsc`. Label the nodes `cma.anthropic.com/sandbox-node=true`, and taint them `cma.anthropic.com/sandbox-node=true:NoSchedule`. The chart registers the `gvisor` RuntimeClass against that handler.
* **AKS:** A node pool with Pod Sandboxing, with the same label and taint as on Amazon EKS. Put the name of the cluster’s network policy engine (`cilium`, `calico`, or `azure`) in `aks.networkPolicy`. Keep `egress.mode: publicInternet`, the only mode that the chart accepts on AKS. Test the chart on your cluster before agents do real work on it.
* **Any other cluster:** A RuntimeClass for gVisor or another sandboxed runtime. Put its name in `runtimeClass.name`.
The folder `kubernetes/chart/examples/` in the archive has a complete values file for each kind of cluster.
## Free a session’s volume
By the chart’s defaults, each session has a 10 GiB volume for `/workspace`. The volume stays after the session’s pod goes away, so the session’s next message finds its files.
| To free | Do this |
| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| One session’s volume | Archive the session after its turn ends. The dispatcher usually deletes the volume within about 5 minutes. |
| The volumes of sessions that nobody archives | Set `dispatcher.retentionDays` in `cma-values.yaml`. The dispatcher then deletes a volume once its session has been idle for that many days, and the session’s next message starts with an empty `/workspace`. The chart’s default, `0`, keeps every volume. |
The commands in this section are for GKE. On another cluster, use `sandboxclaims` in place of `sandboxes`.
Deleting a session doesn’t free its volume. Archive the session first. Delete it only after `kubectl -n cma get sandboxes -l anthropic.com/session-id=<session-id>` lists nothing.
If you deleted a session without archiving it, free its volume yourself:
1. Find the session’s ID in the dispatcher’s log. This command prints a line for each session that appears to be deleted and still has a volume, with the ID after `session_id`:
```bash
kubectl -n cma logs deployment/cma-dispatcher --since=6m \
| grep "could not check the claim's session" | grep "403 Forbidden"
```
The dispatcher writes these lines every 5 minutes. If the session isn’t listed, wait 5 minutes and run the command again.
2. Confirm that the session is deleted. With the port forwarded and `ANTHROPIC_API_KEY` set as in [Run a session](https://platform.claude.com/docs/en/managed-agents/self-hosted/kubernetes#run-a-session), run `ant --base-url http://127.0.0.1:8400 beta:sessions retrieve --session-id <session-id>`. Go on only if the answer is `404 Not Found` with `Session not found`. If the command prints a session, stop: that session isn’t deleted.
3. Delete the Kubernetes object that owns the volume. The session’s files go with it:
```bash
kubectl -n cma delete sandboxes -l anthropic.com/session-id=<session-id>
```
## Renew keys and certificates
An environment key is valid for 365 days, and the dispatcher’s certificate is valid for 825 days. Renew each before it expires.
In an environment key’s last 30 days, the server logs a warning on each request that uses the key. The warning starts `environment key is within 30 days of expiry`.
### Renew the environment key that the charts created
To see when the key expires, run `kubectl -n cma describe secret cma-environment`.
Run these two commands at least once in the key’s last 60 days, from the top directory of the installed release’s archive:
```bash
helm upgrade cma-server kubernetes/server/chart --namespace cma-server -f cma-server-values.yaml --wait
kubectl -n cma rollout restart deployment/cma-dispatcher
```
Each time the server chart’s `helm upgrade` command runs, it stores a new key in the Secret if the key expires within 60 days. Nothing else renews the key. The second command restarts the dispatcher so that it reads the new key.
### Renew the dispatcher’s certificate
Cut at 300 lines. The page has the rest.
No line in this hunk matches that.