Claude Platform Docs
Managed AgentsSelf-hosted Managed Agents

Deploy on Kubernetes

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.

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.

Prerequisites

  • A cluster: Kubernetes 1.30 or later, with:
  • 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.
  • 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.
  • 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

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

    REGISTRY=us-docker.pkg.dev/my-project/cma
    TAG=1.2.3
    PLATFORM=linux/amd64

    Then build and push the three images:

    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
  2. Create a secrets file and the database

    Create a file with a generated database password and a placeholder for your Claude API key:

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

    CREATE ROLE cma_server LOGIN PASSWORD '...';
    GRANT cma_server TO CURRENT_USER;
    CREATE DATABASE cma_server OWNER cma_server;
  3. Create the server’s namespace and Secrets

    Create the server’s namespace and its Secrets:

    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.

  4. Create the sandboxes’ namespace and certificate

    Create the sandboxes’ namespace, and a certificate that the dispatcher serves and the sandboxes trust:

    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 -
  5. Install the server

    Save this as cma-server-values.yaml, with your registry and your database’s address:

    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:

    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.

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

  7. Install the sandboxes

    Save this as cma-values.yaml, with your registry and the addresses from the previous step:

    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.

    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:

    helm install cma kubernetes/chart --namespace cma -f cma-values.yaml --wait

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.

  1. Forward the server’s port

    In one terminal, forward the server’s port to your machine:

    kubectl -n cma-server port-forward svc/cma-server 8400:8400
  2. 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:

    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)
  3. Create an agent and ask it to run a command

    Follow three steps of the quickstart’s 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.

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

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.modeWhat sandboxes can reach
closed, the defaultYour servers and the dispatcher, and no DNS server
publicInternetYour servers, the dispatcher, a DNS server, and the public internet
gatewayYour servers, the dispatcher, and, through the egress gateway, the hosts that you list. See 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 freeDo this
One session’s volumeArchive the session after its turn ends. The dispatcher usually deletes the volume within about 5 minutes.
The volumes of sessions that nobody archivesSet 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:

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

    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:

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

Go back to the step “Create the sandboxes’ namespace and certificate.” Repeat its openssl command and the two kubectl commands after it. Then restart the dispatcher with kubectl -n cma rollout restart deployment/cma-dispatcher.

Replace an environment key for your own workers

The charts renew only their own key. If you created a key yourself, as in Create an environment and its key, replace it with the admin CLI. The CLI is at the top of the release archive.

Run the commands in the second terminal from Run a session, where ANTHROPIC_API_KEY holds the server API key. Keep the port-forward running in the first terminal. If you reach your server at another address, pass that address as -base-url, and you need no port-forward.

  1. Create a new key for the same environment

    For -env, pass the environment’s ID:

    echo "$ANTHROPIC_API_KEY" | ./cma-server-admin mint-key \
      -base-url http://127.0.0.1:8400 -env env_01595EKxaaTTGwwY3kyXdtbs -key-file -

    The command prints the new key, and it prints the key’s ID and expiry on standard error.

  2. Move your workers to the new key

    Set the new key as ANTHROPIC_ENVIRONMENT_KEY for each process that polls, and restart it.

  3. Revoke the old key

    A worker that still uses a revoked key gets a 401 error and gives up the session it was serving. To find the old key’s ID, run list-keys with the same -base-url, -env, and -key-file flags. When no worker uses the old key anymore, revoke it by that ID:

    echo "$ANTHROPIC_API_KEY" | ./cma-server-admin revoke-key \
      -base-url http://127.0.0.1:8400 -env env_01595EKxaaTTGwwY3kyXdtbs \
      -key-id etok_01AbCdEfGhIjKlMnOpQrStUv -key-file -

Uninstall the stack

Delete the sandboxes first. helm uninstall deletes the network policies that confine sandboxes, but the sandboxes that the dispatcher made for sessions keep running.

These steps don’t touch your database, but they delete the Secrets. To use the database again, you need your copy of the encryption key.

  1. Stop the dispatcher and delete the sandboxes

    kubectl -n cma scale deployment/cma-dispatcher --replicas=0
    kubectl -n cma delete sandboxwarmpool cma-sandbox
    kubectl -n cma delete sandboxclaims --all

    On GKE, also run this command:

    kubectl -n cma delete sandboxes --all
  2. Wait until the sandboxes are gone

    Repeat this command until the sandboxes, their pods, and their volumes are gone:

    kubectl -n cma get sandboxes,pods,pvc
  3. Uninstall the charts and delete the namespaces

    helm uninstall cma --namespace cma
    helm uninstall cma-server --namespace cma-server
    kubectl delete namespace cma cma-server

Next steps

Was this page helpful?