From 2c6b7e04c1fcde588793ea68cd50ffd316054b82 Mon Sep 17 00:00:00 2001 From: OpenHands Date: Thu, 9 Jul 2026 15:45:55 +0000 Subject: [PATCH 1/4] docs(agent-canvas): add Kubernetes (Helm) backend guide Add a new backend-setup page documenting the official Helm chart for running Agent Canvas on Kubernetes. Covers prerequisites, what the chart deploys, persistence (including the GKE c4/n4 hyperdisk gotcha), ingress with WebSocket-friendly timeouts, RBAC toggles (namespaced + cluster-admin), common values.yaml recipes (LLM key from a Secret, sandbox-namespace RBAC, external Postgres), install/upgrade commands, three ways to reach the UI, uninstall + PVC cleanup, and a troubleshooting section for the most common install failures. Wires the page into docs.json under Agent Canvas > Backend Setup and adds reciprocal Related-Guides links from the Docker and VM pages. --- docs.json | 1 + .../agent-canvas/backend-setup/docker.mdx | 1 + .../agent-canvas/backend-setup/kubernetes.mdx | 299 ++++++++++++++++++ .../usage/agent-canvas/backend-setup/vm.mdx | 1 + 4 files changed, 302 insertions(+) create mode 100644 openhands/usage/agent-canvas/backend-setup/kubernetes.mdx diff --git a/docs.json b/docs.json index b5b7db02..812aba5d 100644 --- a/docs.json +++ b/docs.json @@ -221,6 +221,7 @@ "openhands/usage/agent-canvas/backend-setup/local", "openhands/usage/agent-canvas/backend-setup/vm", "openhands/usage/agent-canvas/backend-setup/docker", + "openhands/usage/agent-canvas/backend-setup/kubernetes", "openhands/usage/agent-canvas/backend-setup/cloud", "openhands/usage/agent-canvas/backend-setup/modal" ] diff --git a/openhands/usage/agent-canvas/backend-setup/docker.mdx b/openhands/usage/agent-canvas/backend-setup/docker.mdx index 99454964..85768fe6 100644 --- a/openhands/usage/agent-canvas/backend-setup/docker.mdx +++ b/openhands/usage/agent-canvas/backend-setup/docker.mdx @@ -81,3 +81,4 @@ Then add the Docker backend: - [Connect and Manage Backends](/openhands/usage/agent-canvas/backends) - [Local Backend](/openhands/usage/agent-canvas/backend-setup/local) - [VM / Self-Hosted Installation](/openhands/usage/agent-canvas/backend-setup/vm) +- [Kubernetes (Helm)](/openhands/usage/agent-canvas/backend-setup/kubernetes) diff --git a/openhands/usage/agent-canvas/backend-setup/kubernetes.mdx b/openhands/usage/agent-canvas/backend-setup/kubernetes.mdx new file mode 100644 index 00000000..a3a4d21a --- /dev/null +++ b/openhands/usage/agent-canvas/backend-setup/kubernetes.mdx @@ -0,0 +1,299 @@ +--- +title: Kubernetes (Helm) +description: Install Agent Canvas into a Kubernetes cluster with the official Helm chart. +--- + +The official Helm chart deploys the all-in-one Agent Canvas image (frontend + agent-server + automation) on Kubernetes as a `StatefulSet` with a `PersistentVolumeClaim` for durable state, a `Service`, and an optional `Ingress` and RBAC layer. It's the recommended way to run Agent Canvas as a shared, always-on backend that survives pod restarts and image upgrades. + + + The agent server can read and write the pod filesystem, execute shell commands, and — when RBAC is enabled — mutate the Kubernetes cluster it runs in. Treat the release namespace as trusted infrastructure, put it behind an authenticated ingress before exposing it to the internet, and only turn on `rbac.clusterAdmin` when you truly need cluster-wide access. + + +## Prerequisites + +- Kubernetes **1.24 or later** (required by the chart's `kubeVersion` constraint). +- [Helm **3.x**](https://helm.sh/docs/intro/install/). +- A working `kubectl` context with permission to create resources in the target namespace. +- A `StorageClass` that supports `ReadWriteOnce` volumes. On GKE this is usually `standard-rwo` on older node pools or `hyperdisk-balanced` on `c4` / `n4` node pools. On EKS it's `gp3`. On DigitalOcean/Linode it's `do-block-storage` / `linode-block-storage`. +- An ingress controller (nginx, Traefik, cloud-provider ingress, etc.) if you want to reach Agent Canvas from outside the cluster. + +## Get the Chart + +The chart lives alongside the source in the `OpenHands/agent-canvas` repo. Clone it and install from the local path: + +```bash +git clone https://github.com/OpenHands/agent-canvas.git +cd agent-canvas +helm install agent-canvas ./helm/agent-canvas \ + --namespace agent-canvas --create-namespace +``` + +That single command deploys everything below. Agent Canvas is now reachable inside the cluster at `http://agent-canvas.agent-canvas.svc.cluster.local:8000`. See [Access It](#access-it) for how to reach it from a browser. + +## What Gets Deployed + +| Resource | Purpose | +|---|---| +| `StatefulSet` | Single-replica pod running the all-in-one image. | +| `PersistentVolumeClaim` (per pod) | Backs `$HOME/.openhands` — settings, encrypted secrets, conversation history, automation SQLite DB, workspaces, generated keys. | +| `Service` (`ClusterIP`) | Cluster-internal endpoint on port 8000. | +| `Service` (headless) | Required by the `StatefulSet` for stable pod DNS. | +| `ServiceAccount` | Stable identity the pod runs under. | +| `Ingress` (optional) | External HTTP(S) entry point. | +| `RoleBinding` (per namespace) | Created when `rbac.enabled=true`, one per entry in `rbac.namespaces`. | +| `ClusterRoleBinding` (optional) | Created when `rbac.clusterAdmin=true`. | + +## Persistence + +Everything durable that Agent Canvas writes lives under `$HOME/.openhands` inside the container. The chart mounts a PVC there so state survives pod restarts and image upgrades. That includes: + +- agent-server settings and encrypted secrets +- conversation history and event stores +- automation SQLite database (unless you point at external Postgres — see [External Database](#external-database)) +- user workspaces (cloned repos, worktrees, generated files) +- the `OH_SECRET_KEY` and session API key that the entrypoint auto-generates on first boot + +Defaults: + +```yaml +persistence: + enabled: true + mountPath: /home/openhands/.openhands # matches HOME for UID 1000 in the image + size: 20Gi + # storageClassName: "" # empty → cluster default + accessModes: + - ReadWriteOnce +``` + + + On GKE clusters using `c4` or `n4` node pools, the default `standard-rwo` StorageClass will fail to attach because those machine types require `hyperdisk-balanced`. Set `persistence.storageClassName: hyperdisk-balanced` explicitly. + + +### Bring Your Own PVC + +If you already manage the volume out of band, point the chart at it and it will skip the `volumeClaimTemplates` path: + +```yaml +persistence: + enabled: true + existingClaim: my-agent-canvas-pvc +``` + +## Ingress + +Ingress is off by default. Enable it and provide the standard knobs — the chart supports `className`, `annotations`, multiple `hosts` with per-path routing, and TLS. + +```yaml +ingress: + enabled: true + className: nginx + annotations: + nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" + nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" + nginx.ingress.kubernetes.io/proxy-body-size: "50m" + cert-manager.io/cluster-issuer: letsencrypt-prod + hosts: + - host: agent-canvas.example.com + paths: + - path: / + pathType: Prefix + tls: + - hosts: + - agent-canvas.example.com + secretName: agent-canvas-tls +``` + + + The read/send timeout annotations matter — Agent Canvas holds long-lived WebSocket connections for streaming agent events. Without generous timeouts, the ingress controller will close idle streams and the UI will drop reconnects mid-turn. Nginx defaults to 60 seconds. + + +## RBAC + +RBAC is **off by default**. The pod runs under its own ServiceAccount but has no in-cluster permissions. Turn it on when the agent needs to inspect or mutate Kubernetes resources (e.g. to deploy things it builds). + +Two independent switches: + +```yaml +rbac: + enabled: true + # Full access to all resources in these namespaces (bound to the + # built-in `admin` ClusterRole via one RoleBinding per namespace). + # Each namespace must already exist in the cluster. + namespaces: + - default + - agent-sandbox + # Optionally grant cluster-admin. OFF by default. Very broad — enable + # only when the agent truly needs to manage the whole cluster. + clusterAdmin: false +``` + + + `rbac.clusterAdmin: true` grants `cluster-admin` — the highest privilege level in Kubernetes. An agent with a compromised prompt (or a bad LLM response) could delete every resource in the cluster. Prefer scoping to specific namespaces with `rbac.namespaces` whenever possible. + + +## Common Configurations + +### Minimal (defaults + ingress) + +```yaml +# values.yaml +ingress: + enabled: true + className: nginx + hosts: + - host: agent-canvas.example.com + paths: + - path: / + pathType: Prefix + tls: + - hosts: [agent-canvas.example.com] + secretName: agent-canvas-tls +``` + +### With LLM Credentials from a Secret + +Rather than typing your LLM key into the UI on every reinstall, pass it in through the chart. Create the secret separately, then reference it via `config.extraEnv`: + +```bash +kubectl -n agent-canvas create secret generic llm \ + --from-literal=api-key=sk-... +``` + +```yaml +# values.yaml +config: + extraEnv: + - name: LLM_MODEL + value: "openhands/claude-sonnet-4-5-20250929" + - name: LLM_API_KEY + valueFrom: + secretKeyRef: + name: llm + key: api-key +``` + +### Agent That Manages a Sandbox Namespace + +```yaml +# values.yaml +rbac: + enabled: true + namespaces: + - agent-sandbox +``` + +Create the sandbox namespace before installing (`kubectl create namespace agent-sandbox`). Then the pod can `kubectl apply` / `kubectl delete` anything inside `agent-sandbox` but nothing else. + +### External Database + +The automation subsystem uses a SQLite database on the PVC by default. For higher-volume deployments, point it at Postgres: + +```yaml +# values.yaml +config: + automationDbUrl: "postgresql+asyncpg://user:pass@postgres.databases.svc.cluster.local/agent_canvas" +``` + +Store the actual credentials in a Kubernetes Secret and reference them via `config.extraEnv` rather than putting the password in `values.yaml`. + +## Install and Upgrade + +```bash +# First install +helm install agent-canvas ./helm/agent-canvas \ + --namespace agent-canvas --create-namespace \ + -f values.yaml + +# Later upgrades +helm upgrade agent-canvas ./helm/agent-canvas \ + -n agent-canvas -f values.yaml + +# Check rollout +kubectl -n agent-canvas rollout status statefulset/agent-canvas +kubectl -n agent-canvas get pvc,pod,svc,ingress +``` + +To pin a specific image (e.g. a PR preview or a build newer than the chart's `appVersion`): + +```bash +helm upgrade agent-canvas ./helm/agent-canvas \ + -n agent-canvas -f values.yaml \ + --set image.tag=sha- +``` + +## Access It + +The chart's default `Service` is `ClusterIP`. Three common ways to reach the UI: + +1. **Ingress** — configure the `ingress:` block as shown above. This is the production path. +2. **Port-forward** — for quick access from your laptop without touching DNS or ingress: + + ```bash + kubectl -n agent-canvas port-forward svc/agent-canvas 8000:8000 + ``` + + Then open `http://localhost:8000`. +3. **LoadBalancer** — set `service.type: LoadBalancer` if your cloud provisions cloud load balancers for you. Cheaper than ingress for one-off installs, but skips TLS and auth. + + + The agent server accepts any request with the right `LOCAL_BACKEND_API_KEY`, so exposing it via a bare LoadBalancer means anyone on the internet who can guess the key can drive the agent. Prefer the Ingress path with an authenticated proxy (oauth2-proxy, Cloudflare Access, tailscale-serve, ngrok OAuth, etc.) in front of it. + + +## Uninstall + +```bash +helm uninstall agent-canvas -n agent-canvas +``` + +The PVC created by the `StatefulSet` is **retained** on uninstall so a reinstall picks up where you left off. Delete it explicitly if you want a fully clean slate: + +```bash +kubectl -n agent-canvas delete pvc -l app.kubernetes.io/instance=agent-canvas +``` + +## Troubleshooting + +### `FailedAttachVolume: pd-balanced disk type cannot be used by c4-standard-8 machine type` + +The default StorageClass on your cluster is provisioning a disk type your nodes can't attach. On GKE `c4` / `n4` node pools, use `hyperdisk-balanced`: + +```yaml +persistence: + storageClassName: hyperdisk-balanced +``` + +Because `volumeClaimTemplates` on an existing `StatefulSet` are immutable, changing the StorageClass requires deleting the STS and PVC first: + +```bash +kubectl -n agent-canvas delete statefulset agent-canvas +kubectl -n agent-canvas delete pvc -l app.kubernetes.io/instance=agent-canvas +helm upgrade agent-canvas ./helm/agent-canvas -n agent-canvas -f values.yaml +``` + +### `ErrImagePull` on `ghcr.io/openhands/agent-canvas:` + +Verify the tag exists on GHCR — the chart's `appVersion` pins the default. To pull an image built from a specific commit, use `--set image.tag=sha-`. See [GHCR](https://github.com/OpenHands/agent-canvas/pkgs/container/agent-canvas) for the tag list. + +### WebSocket disconnects every minute + +Your ingress is closing idle streams. Bump the timeout annotations on the `Ingress`: + +```yaml +ingress: + annotations: + # nginx + nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" + nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" + # Traefik + traefik.ingress.kubernetes.io/router.middlewares: "" # keep this in mind if you also add auth middlewares +``` + +### Pod stuck in `Pending` — `no persistent volumes available` + +Either no `StorageClass` exists on the cluster, or the one you set doesn't provision on demand. Run `kubectl get storageclass` and set `persistence.storageClassName` to one that shows `VOLUMEBINDINGMODE=WaitForFirstConsumer` (Immediate is fine too). + +## Related Guides + +- [Docker Backend](/openhands/usage/agent-canvas/backend-setup/docker) — single-container equivalent for laptops and single-host VMs. +- [VM / Self-Hosted Installation](/openhands/usage/agent-canvas/backend-setup/vm) — install directly on a Linux VM without Kubernetes. +- [Connect and Manage Backends](/openhands/usage/agent-canvas/backends) — point a local Agent Canvas UI at a remote backend. diff --git a/openhands/usage/agent-canvas/backend-setup/vm.mdx b/openhands/usage/agent-canvas/backend-setup/vm.mdx index ff22fae2..51819907 100644 --- a/openhands/usage/agent-canvas/backend-setup/vm.mdx +++ b/openhands/usage/agent-canvas/backend-setup/vm.mdx @@ -336,4 +336,5 @@ Before exposing Agent Canvas beyond an SSH tunnel: - [Connect and Manage Backends](/openhands/usage/agent-canvas/backends) - [Local Backend](/openhands/usage/agent-canvas/backend-setup/local) - [Docker Backend](/openhands/usage/agent-canvas/backend-setup/docker) +- [Kubernetes (Helm)](/openhands/usage/agent-canvas/backend-setup/kubernetes) - [Cloud Backend](/openhands/usage/agent-canvas/backend-setup/cloud) From e10660b1128a73caa429cdb20b5c0342c5e8068a Mon Sep 17 00:00:00 2001 From: OpenHands Date: Thu, 9 Jul 2026 15:56:14 +0000 Subject: [PATCH 2/4] =?UTF-8?q?docs(agent-canvas):=20follow=20chart=20chan?= =?UTF-8?q?ge=20=E2=80=94=20PVC=20now=20backs=20whole=20$HOME?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirror the chart update that moves persistence.mountPath from /home/openhands/.openhands up to /home/openhands so ~/workspace and other agent-generated state also survives pod restarts. --- .../agent-canvas/backend-setup/kubernetes.mdx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/openhands/usage/agent-canvas/backend-setup/kubernetes.mdx b/openhands/usage/agent-canvas/backend-setup/kubernetes.mdx index a3a4d21a..a11aba9f 100644 --- a/openhands/usage/agent-canvas/backend-setup/kubernetes.mdx +++ b/openhands/usage/agent-canvas/backend-setup/kubernetes.mdx @@ -35,7 +35,7 @@ That single command deploys everything below. Agent Canvas is now reachable insi | Resource | Purpose | |---|---| | `StatefulSet` | Single-replica pod running the all-in-one image. | -| `PersistentVolumeClaim` (per pod) | Backs `$HOME/.openhands` — settings, encrypted secrets, conversation history, automation SQLite DB, workspaces, generated keys. | +| `PersistentVolumeClaim` (per pod) | Backs the openhands user's `$HOME` — settings, encrypted secrets, conversation history, automation SQLite DB, cloned repos under `~/workspace`, generated keys, dotfiles. | | `Service` (`ClusterIP`) | Cluster-internal endpoint on port 8000. | | `Service` (headless) | Required by the `StatefulSet` for stable pod DNS. | | `ServiceAccount` | Stable identity the pod runs under. | @@ -45,22 +45,22 @@ That single command deploys everything below. Agent Canvas is now reachable insi ## Persistence -Everything durable that Agent Canvas writes lives under `$HOME/.openhands` inside the container. The chart mounts a PVC there so state survives pod restarts and image upgrades. That includes: +The chart mounts the PVC at the openhands user's HOME (`/home/openhands`) so every piece of durable state the container writes survives pod restarts, rescheduling, and image upgrades. That covers: -- agent-server settings and encrypted secrets -- conversation history and event stores -- automation SQLite database (unless you point at external Postgres — see [External Database](#external-database)) -- user workspaces (cloned repos, worktrees, generated files) -- the `OH_SECRET_KEY` and session API key that the entrypoint auto-generates on first boot +- `~/.openhands` — agent-server settings and encrypted secrets, conversation history and event stores, automation SQLite database (unless you point at external Postgres — see [External Database](#external-database)), the `OH_SECRET_KEY` and session API key auto-generated on first boot +- `~/workspace` — the agent's default working directory: cloned repos, worktrees, anything the agent writes when it treats `~` as the workspace +- any other dotfiles the running user creates (`~/.gitconfig`, `~/.cache`, `~/.local`, etc.) + +Mounting the whole HOME (rather than just `~/.openhands`) is deliberate: the upstream image doesn't ship any dotfiles or Python venvs under `/home/openhands` — everything is `--system`-installed under `/opt/agent-canvas` — so an empty PVC on first boot doesn't shadow anything, and the entrypoint re-creates the `.openhands` subtree it needs. Defaults: ```yaml persistence: enabled: true - mountPath: /home/openhands/.openhands # matches HOME for UID 1000 in the image + mountPath: /home/openhands # HOME for UID 1000 in the image size: 20Gi - # storageClassName: "" # empty → cluster default + # storageClassName: "" # empty → cluster default accessModes: - ReadWriteOnce ``` From 2148ebb9c4bb493284983939fb576a4a21b6851c Mon Sep 17 00:00:00 2001 From: OpenHands Date: Thu, 9 Jul 2026 16:02:47 +0000 Subject: [PATCH 3/4] =?UTF-8?q?docs(agent-canvas):=20match=20chart=20fix?= =?UTF-8?q?=20=E2=80=94=20subPath=20mounts=20+=20UID=2010001?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The chart now runs as UID 10001 (upstream image's `openhands` user; UID 1000 was a leftover `pn` account in the base layer) and mounts a single PVC at ~/.openhands and ~/workspace via subPath instead of shadowing the whole HOME (which would hide the image's .bashrc et al.). --- .../agent-canvas/backend-setup/kubernetes.mdx | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/openhands/usage/agent-canvas/backend-setup/kubernetes.mdx b/openhands/usage/agent-canvas/backend-setup/kubernetes.mdx index a11aba9f..5b5218d2 100644 --- a/openhands/usage/agent-canvas/backend-setup/kubernetes.mdx +++ b/openhands/usage/agent-canvas/backend-setup/kubernetes.mdx @@ -35,7 +35,7 @@ That single command deploys everything below. Agent Canvas is now reachable insi | Resource | Purpose | |---|---| | `StatefulSet` | Single-replica pod running the all-in-one image. | -| `PersistentVolumeClaim` (per pod) | Backs the openhands user's `$HOME` — settings, encrypted secrets, conversation history, automation SQLite DB, cloned repos under `~/workspace`, generated keys, dotfiles. | +| `PersistentVolumeClaim` (per pod) | Backs `~/.openhands` and `~/workspace` (both mounted from the same PVC via `subPath`): settings, encrypted secrets, conversation history, automation SQLite DB, cloned repos, generated files. | | `Service` (`ClusterIP`) | Cluster-internal endpoint on port 8000. | | `Service` (headless) | Required by the `StatefulSet` for stable pod DNS. | | `ServiceAccount` | Stable identity the pod runs under. | @@ -45,26 +45,33 @@ That single command deploys everything below. Agent Canvas is now reachable insi ## Persistence -The chart mounts the PVC at the openhands user's HOME (`/home/openhands`) so every piece of durable state the container writes survives pod restarts, rescheduling, and image upgrades. That covers: +The chart provisions **one** PVC and mounts it at multiple well-known subdirectories of the openhands user's HOME via `subPath`. That preserves the pristine `/home/openhands` the base image ships (dotfiles like `~/.bashrc` and `~/.profile`) while persisting the directories that actually contain state: - `~/.openhands` — agent-server settings and encrypted secrets, conversation history and event stores, automation SQLite database (unless you point at external Postgres — see [External Database](#external-database)), the `OH_SECRET_KEY` and session API key auto-generated on first boot -- `~/workspace` — the agent's default working directory: cloned repos, worktrees, anything the agent writes when it treats `~` as the workspace -- any other dotfiles the running user creates (`~/.gitconfig`, `~/.cache`, `~/.local`, etc.) +- `~/workspace` — the agent's default working directory: cloned repos, worktrees, anything the agent writes when it treats `~` as the workspace root -Mounting the whole HOME (rather than just `~/.openhands`) is deliberate: the upstream image doesn't ship any dotfiles or Python venvs under `/home/openhands` — everything is `--system`-installed under `/opt/agent-canvas` — so an empty PVC on first boot doesn't shadow anything, and the entrypoint re-creates the `.openhands` subtree it needs. +Both paths share the same underlying disk. Add more entries to `persistence.mounts` if you want other subtrees persisted (e.g. `~/.cache`, `~/.config`). Defaults: ```yaml persistence: enabled: true - mountPath: /home/openhands # HOME for UID 1000 in the image + mounts: + - mountPath: /home/openhands/.openhands + subPath: openhands + - mountPath: /home/openhands/workspace + subPath: workspace size: 20Gi # storageClassName: "" # empty → cluster default accessModes: - ReadWriteOnce ``` + + The pod runs as `openhands` (UID 10001) from the upstream image. The chart sets `podSecurityContext.fsGroup: 10001` so the kubelet chowns the PVC on mount and the process can write to it. If you override `securityContext` or `podSecurityContext`, make sure UID/GID/fsGroup all point at the same user or `openhands` won't be able to write to the volume. + + On GKE clusters using `c4` or `n4` node pools, the default `standard-rwo` StorageClass will fail to attach because those machine types require `hyperdisk-balanced`. Set `persistence.storageClassName: hyperdisk-balanced` explicitly. From 28d5b7073288ae48921534b901e52dc02ff33a98 Mon Sep 17 00:00:00 2001 From: openhands Date: Thu, 9 Jul 2026 16:50:50 +0000 Subject: [PATCH 4/4] docs(agent-canvas): add internal vibecoding platform intro + generic app-deploy skill Explain that the Kubernetes backend can be used as an internal vibecoding platform where anyone with Agent Canvas UI access can build, ship, and push code to GitHub from a prompt (GitHub MCP required for the push half). Include a generic, cluster-agnostic version of the internal app-deployment skill as a copy-pasteable code block. Co-authored-by: openhands --- .../agent-canvas/backend-setup/kubernetes.mdx | 209 ++++++++++++++++++ 1 file changed, 209 insertions(+) diff --git a/openhands/usage/agent-canvas/backend-setup/kubernetes.mdx b/openhands/usage/agent-canvas/backend-setup/kubernetes.mdx index 5b5218d2..5f152a39 100644 --- a/openhands/usage/agent-canvas/backend-setup/kubernetes.mdx +++ b/openhands/usage/agent-canvas/backend-setup/kubernetes.mdx @@ -5,6 +5,12 @@ description: Install Agent Canvas into a Kubernetes cluster with the official He The official Helm chart deploys the all-in-one Agent Canvas image (frontend + agent-server + automation) on Kubernetes as a `StatefulSet` with a `PersistentVolumeClaim` for durable state, a `Service`, and an optional `Ingress` and RBAC layer. It's the recommended way to run Agent Canvas as a shared, always-on backend that survives pod restarts and image upgrades. + + **Turn this into an internal vibecoding platform.** Once Agent Canvas runs in your cluster with [RBAC enabled](#rbac), you can give it a skill that teaches the agent how to deploy the small web apps it builds straight into the cluster. From that point on, **anyone with access to the Agent Canvas UI can build and ship code into the cluster — and save it to GitHub — with just a prompt.** No pipelines, no manual `kubectl`, no hand-written manifests: the agent scaffolds the app, applies the manifests, and gives back a live URL. + + The "save to GitHub" half of that loop requires the **GitHub MCP server** to be enabled so the agent can create repos and push commits on the user's behalf. See the [generic app-deployment skill](#skill-deploying-apps-into-the-cluster) below for a ready-to-adapt version, and [RBAC](#rbac) for the permissions it needs. + + The agent server can read and write the pod filesystem, execute shell commands, and — when RBAC is enabled — mutate the Kubernetes cluster it runs in. Treat the release namespace as trusted infrastructure, put it behind an authenticated ingress before exposing it to the internet, and only turn on `rbac.clusterAdmin` when you truly need cluster-wide access. @@ -138,6 +144,209 @@ rbac: `rbac.clusterAdmin: true` grants `cluster-admin` — the highest privilege level in Kubernetes. An agent with a compromised prompt (or a bad LLM response) could delete every resource in the cluster. Prefer scoping to specific namespaces with `rbac.namespaces` whenever possible. +## Skill: Deploying Apps Into the Cluster + +To unlock the internal vibecoding platform described at the top of this page, give the agent a [skill](/overview/skills/creating) that teaches it the conventions for shipping the apps it builds into a namespace of your cluster. Drop the markdown below into `.openhands/skills/deploy-app/SKILL.md` (or your workspace's skills directory), adjust the placeholders (``, ``, GitHub org), and the agent will scaffold, deploy, and — with the **GitHub MCP server enabled** — push each app to its own repo on request. + +This is a generic version of the skill the OpenHands team runs internally. It assumes the backend was installed with [`rbac.enabled=true`](#rbac) and a `rbac.namespaces` entry for the target namespace, so the pod's ServiceAccount can `kubectl apply` there directly. + +````markdown +# Deploy apps into the cluster + +Use this skill to create and manage the small web apps you build, serving each +one at `https://.` from the `` namespace of the +cluster Agent Canvas runs in. + +## Platform conventions + +Every app follows the same pattern: + +- **Namespace:** ``. The agent runs under a ServiceAccount that has + admin in this namespace (granted via the Helm chart's `rbac.namespaces`), so + `kubectl apply` works directly with no extra credentials. +- **Content:** static files (HTML/JS/CSS) served by an `nginx:*-alpine` pod. + The files live in a **ConfigMap** (`-web`) mounted at + `/usr/share/nginx/html`. Apps that need a backend add their own container. +- **Objects per app:** `Deployment` + `Service` (ClusterIP, port 80) + + `Ingress`. Apps that need scheduled work add a `CronJob`. +- **Host:** `.`. +- **TLS:** if cert-manager is installed, add the + `cert-manager.io/cluster-issuer: ` annotation and a `tls` block with + `secretName: -tls`; the cert is issued automatically. +- **Auth:** put shared apps behind your ingress's authentication (oauth2-proxy, + a forward-auth middleware, Cloudflare Access, etc.) so they aren't exposed + unauthenticated. Reference your cluster's auth middleware/annotation here. +- **Resources:** keep them tiny (requests `10m`/`16Mi`, limits `100m`/`64Mi`). + +### Ingress template + +```yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: + namespace: + labels: + app: + annotations: + cert-manager.io/cluster-issuer: + # Add your cluster's auth middleware/annotation here so the app is + # not exposed unauthenticated. +spec: + ingressClassName: # e.g. nginx or traefik + rules: + - host: . + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: + port: + number: 80 + tls: + - hosts: + - . + secretName: -tls +``` + +### Deployment + Service template + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: + namespace: + labels: + app: +spec: + replicas: 1 + selector: + matchLabels: + app: + template: + metadata: + labels: + app: + spec: + containers: + - name: web + image: nginx:1.27-alpine + ports: + - containerPort: 80 + resources: + requests: + cpu: 10m + memory: 16Mi + limits: + cpu: 100m + memory: 64Mi + volumeMounts: + - name: web + mountPath: /usr/share/nginx/html + volumes: + - name: web + configMap: + name: -web +--- +apiVersion: v1 +kind: Service +metadata: + name: + namespace: + labels: + app: +spec: + selector: + app: + ports: + - port: 80 + targetPort: 80 +``` + +## Secrets (never commit them) + +If an app needs credentials at runtime, store them in a Kubernetes `Secret` +created out of band — never in a ConfigMap, the git repo, or the manifest, and +never print their values. Create the Secret from environment variables so the +plaintext never appears in a command line or file: + +```bash +kubectl create secret generic - -n \ + --from-literal=="$SOME_ENV_VAR" \ + --dry-run=client -o yaml | kubectl apply -f - +``` + +Consume it in the Deployment via `env` + `secretKeyRef`. Document the one-off +`kubectl create secret ...` command in the app's `README.md` — keep it out of +`deploy.sh` and the committed manifest. + +## Source layout & GitHub + +- Keep each app's source under `~/workspace/`. +- Give each app its own GitHub repo (e.g. `/app-`). + **This requires the GitHub MCP server to be enabled** so the agent can create + the repo and push commits. Create it if it doesn't exist, then push. +- Standard repo layout: + - `README.md` — what the app is, its URL, how to deploy, any one-off secrets. + - `k8s/.yaml` — all Kubernetes objects (Deployment+Service+Ingress). + - `web/` — static assets served via the ConfigMap. + - `deploy.sh` — regenerates the ConfigMap from `web/`, applies `k8s/`, and + rolls the Deployment. + +### Typical `deploy.sh` + +```bash +#!/usr/bin/env bash +set -euo pipefail +NS= +DIR="$(cd "$(dirname "$0")" && pwd)" + +kubectl create configmap -web --namespace "$NS" \ + --from-file="$DIR/web/" \ + --dry-run=client -o yaml | kubectl apply -f - + +kubectl apply -f "$DIR/k8s/.yaml" +kubectl rollout restart deployment/ -n "$NS" +kubectl rollout status deployment/ -n "$NS" +``` + +## Creating a new app + +1. `mkdir -p ~/workspace//{k8s,web}` and add `web/index.html`, + `k8s/.yaml` (from the templates above), `deploy.sh`, and a + `README.md`. +2. If GitHub MCP is enabled, create/verify `/app-`, + commit, and push. +3. Deploy: `./deploy.sh`. +4. If cert-manager is used, wait for the cert: + `kubectl get certificate -tls -n ` should become + `READY=True`. Confirm the Ingress has an address. +5. Report the live URL back to the user. + +## Updating an app + +Edit files under `~/workspace/`, commit + push (via GitHub MCP), then +re-run `./deploy.sh` (which restarts the Deployment so nginx reloads the +ConfigMap). + +## Deleting an app + +1. `kubectl delete -f ~/workspace//k8s/.yaml` and delete the + `-web` ConfigMap. Deleting the Ingress lets cert-manager clean up the + TLS secret; delete any credential secrets explicitly. +2. Optionally archive/delete the GitHub repo and remove + `~/workspace/`. + +## Verifying access + +```bash +kubectl get deploy,svc,ingress,certificate -n -l app= +``` +```` + ## Common Configurations ### Minimal (defaults + ingress)