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.
Relationship to OpenHands Enterprise
Agent Canvas is an unauthenticated, single-tenant application. This Helm chart runs exactly that: one shared instance where all agents are comingled on the same pod and PVC, with no built-in authentication, user-level role-based access control, or tenant isolation. It’s a good fit for a single team or individual running their own backend, and for the “internal vibecoding platform” pattern described above — where a small, trusted group shares one deployment. OpenHands Enterprise (OHE) is the productized upgrade path when you need a hardened, multi-user deployment. OHE adds:- Authentication (SSO / SAML / OIDC) so users must log in before they can run agents.
- Role-based access control over who can run agents and manage the deployment.
- Multi-tenancy so different teams get isolated spaces rather than one shared instance.
- Isolated agent sandboxes — each agent run executes in its own container instead of every agent sharing the pod’s filesystem and shell.
Prerequisites
- Kubernetes 1.24 or later (required by the chart’s
kubeVersionconstraint). - Helm 3.x.
- A working
kubectlcontext with permission to create resources in the target namespace. - A
StorageClassthat supportsReadWriteOncevolumes. On GKE this is usuallystandard-rwoon older node pools orhyperdisk-balancedonc4/n4node pools. On EKS it’sgp3. On DigitalOcean/Linode it’sdo-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 theOpenHands/agent-canvas repo. Clone it and install from the local path:
http://agent-canvas.agent-canvas.svc.cluster.local:8000. See Access It for how to reach it from a browser.
What Gets Deployed
Persistence
The chart provisions one PVC and mounts it at multiple well-known subdirectories of the openhands user’s HOME viasubPath. 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), theOH_SECRET_KEYand 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 root
persistence.mounts if you want other subtrees persisted (e.g. ~/.cache, ~/.config).
Defaults:
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.Bring Your Own PVC
If you already manage the volume out of band, point the chart at it and it will skip thevolumeClaimTemplates path:
Ingress
Ingress is off by default. Enable it and provide the standard knobs — the chart supportsclassName, annotations, multiple hosts with per-path routing, and 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:Skill: Deploying Apps Into the Cluster
To unlock the internal vibecoding platform described at the top of this page, give the agent a skill 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 (<namespace>, <domain>, 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 and a rbac.namespaces entry for the target namespace, so the pod’s ServiceAccount can kubectl apply there directly.
Common Configurations
Minimal (defaults + ingress)
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 viaconfig.extraEnv:
Agent That Manages a Sandbox Namespace
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:config.extraEnv rather than putting the password in values.yaml.
Install and Upgrade
appVersion):
Access It
The chart’s defaultService is ClusterIP. Three common ways to reach the UI:
-
Ingress — configure the
ingress:block as shown above. This is the production path. -
Port-forward — for quick access from your laptop without touching DNS or ingress:
Then open
http://localhost:8000. -
LoadBalancer — set
service.type: LoadBalancerif your cloud provisions cloud load balancers for you. Cheaper than ingress for one-off installs, but skips TLS and auth.
Uninstall
StatefulSet is retained on uninstall so a reinstall picks up where you left off. Delete it explicitly if you want a fully clean slate:
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:
volumeClaimTemplates on an existing StatefulSet are immutable, changing the StorageClass requires deleting the STS and PVC first:
ErrImagePull on ghcr.io/openhands/agent-canvas:<tag>
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-<short-sha>. See GHCR for the tag list.
WebSocket disconnects every minute
Your ingress is closing idle streams. Bump the timeout annotations on theIngress:
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 — single-container equivalent for laptops and single-host VMs.
- VM / Self-Hosted Installation — install directly on a Linux VM without Kubernetes.
- Connect and Manage Backends — point a local Agent Canvas UI at a remote backend.

