File size: 8,511 Bytes
3201ca6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
# agent-canvas Helm chart

Helm chart for running the [OpenHands agent-canvas](https://github.com/OpenHands/OpenHands)
all-in-one image (frontend + agent-server + automation) on Kubernetes as a
`StatefulSet` with persistent storage, an `Ingress`, and optional in-cluster
RBAC.

> [!WARNING]
> This Helm chart is experimental. Agent Canvas is an unauthenticated, single-tenant application. This helm chart doesn't address scale or zero-downtime deployments.

## When to use this

This chart turns a Kubernetes cluster into a shared, always-on Agent Canvas
backend: one pod running the UI, agent-server, and automation stack, with a
PVC that survives pod restarts and image upgrades. It's a good fit when you
want:

- **A self-hosted, persistent backend** instead of the laptop-friendly Docker
  setup β€” so conversations, secrets, and the automation SQLite DB persist
  across restarts without you managing the volume by hand.
- **An internal "vibecoding" platform.** With [RBAC enabled](#rbac), give the
  agent a skill that teaches it 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.

Put it behind an authenticated ingress before exposing it to the internet (see
the [Security](#security) notes).

## Relationship to OpenHands Enterprise

Agent Canvas is an **unauthenticated, single-tenant** application. This chart
runs exactly that: **one** shared instance where all agents are comingled on
the same pod and PVC, with no built-in auth, RBAC for users, or tenant
isolation. It's well suited to a single team or individual running their own
backend.

[OpenHands Enterprise (OHE)](https://www.all-hands.ai/enterprise) is the
productized upgrade path when you need a hardened, multi-user deployment. OHE
adds:

- **Authentication** (SSO / SAML / OIDC) so users must log in.
- **Role-based access control** over who can run agents and manage the
  deployment.
- **Multi-tenancy** so different teams get isolated spaces.
- **Isolated agent sandboxes** β€” each agent run gets its own container rather
  than every agent sharing the pod's filesystem.

Use this chart for self-hosted, single-tenant setups; reach for OHE when you
need authentication, multi-tenancy, or isolated agent execution.

## TL;DR

```bash
helm install agent-canvas ./helm/agent-canvas
```

## What gets deployed

| Resource                            | Purpose                                                                            |
| ----------------------------------- | ---------------------------------------------------------------------------------- |
| `StatefulSet`                       | Single-replica pod running the all-in-one image (frontend + agent-server + automation). |
| `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` by default)  | Cluster-internal endpoint on port 8000.                                            |
| `Service` (headless)                | Required by the `StatefulSet` for stable pod DNS.                                  |
| `ServiceAccount`                    | Stable identity the pod runs under. Bindings depend on `rbac.*`.                   |
| `Ingress` (optional)                | External HTTP(S) entry point with the usual class/annotations/TLS knobs.           |
| `RoleBinding` (per namespace)       | When `rbac.enabled=true`, one per entry in `rbac.namespaces`, bound to the built-in `admin` ClusterRole. |
| `ClusterRoleBinding` (optional)     | When `rbac.clusterAdmin=true`, binds the SA to `cluster-admin` cluster-wide.       |

## Persistence

The chart provisions **one** PVC and mounts it at multiple well-known
subdirectories of the openhands user's HOME (`/home/openhands`) via
`subPath`. That preserves the pristine home directory the base image
ships (dotfiles like `~/.bashrc` and `~/.profile`) while persisting the
directories that actually contain state:

- `~/.openhands` β€” agent-server settings, encrypted secrets,
  conversation history and event stores, automation SQLite database
  (unless `config.automationDbUrl` is set), auto-generated
  `OH_SECRET_KEY`, session API key
- `~/workspace` β€” the agent's default working directory: cloned repos,
  worktrees, anything the agent writes when it treats `~` as the
  workspace root

Both paths share the same underlying disk. Add more entries to
`persistence.mounts` if you want other subtrees persisted (e.g.
`~/.cache`, `~/.config`).

The pod runs as `openhands` (UID 10001) from the upstream image;
`podSecurityContext.fsGroup=10001` makes the kubelet chown the PVC
so that user can write to it.

Point at an existing PVC with `persistence.existingClaim=<name>` if you
manage the volume out of band; otherwise the chart uses the
`StatefulSet`'s `volumeClaimTemplates` (recommended β€” data survives pod
rescheduling and image upgrades).

## RBAC

RBAC is **off by default** β€” the pod runs under its own ServiceAccount
but has no in-cluster permissions.

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).
  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.namespaces` creates one `RoleBinding` per namespace, referencing
  the built-in `admin` ClusterRole. Those namespaces must already exist
  in the cluster.
- `rbac.clusterAdmin=true` creates an additional `ClusterRoleBinding` to
  `cluster-admin`. This overrides / supersedes any per-namespace grant.

## Ingress

Standard knobs:

```yaml
ingress:
  enabled: true
  className: nginx
  annotations:
    nginx.ingress.kubernetes.io/proxy-read-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 agent-server uses WebSockets on `/sockets`, so long
`proxy-read-timeout` / `proxy-send-timeout` values are recommended when
using `ingress-nginx`. File uploads honor `proxy-body-size`.

## Common overrides

Provide an LLM key via secret:

```yaml
config:
  extraEnv:
    - name: LLM_MODEL
      value: openhands/claude-sonnet-4-5-20250929
    - name: LLM_API_KEY
      valueFrom:
        secretKeyRef:
          name: my-llm-secret
          key: api-key
```

Bring your own `OH_SECRET_KEY` (must match the value used to encrypt any
existing PVC contents):

```yaml
secrets:
  ohSecretKey:
    existingSecret: agent-canvas-keys
    key: ohSecretKey
```

Point automation at an external Postgres:

```yaml
config:
  automationDbUrl: postgresql+asyncpg://user:pass@postgres/agentcanvas
```

## Security

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. There is no built-in authentication in Agent Canvas, so treat the release
namespace as trusted infrastructure:

- Put it behind an **authenticated** ingress (oauth2-proxy, Cloudflare Access,
  tailscale-serve, etc.) before exposing it to the internet.
- Avoid a bare `LoadBalancer` β€” the agent server accepts any request with the
  right `LOCAL_BACKEND_API_KEY`, so exposing it directly means anyone who can
  guess the key can drive the agent.
- Only turn on `rbac.clusterAdmin` when you truly need cluster-wide access;
  prefer scoping to specific namespaces via `rbac.namespaces`.

For authentication, role-based access control, multi-tenancy, and isolated
agent sandboxes, see [OpenHands Enterprise](#relationship-to-openhands-enterprise).

## Uninstall

```bash
helm uninstall agent-canvas
```

By default the PVC created by `volumeClaimTemplates` is **retained**
after uninstall β€” delete it manually if you want the data gone. Set
`statefulSet.persistentVolumeClaimRetentionPolicy` (Kubernetes 1.27+) if
you want the PVC lifecycle tied to the StatefulSet.