On this page
On this page
Hosting
Kubernetes
A minimal starting point for running OpenClaw on Kubernetes, not a production-ready deployment. It covers the core resources and is meant to be adapted to your environment.
Why not Helm
OpenClaw is a single container with some config files. The interesting customization is in agent content (Markdown files, skills, config overrides), not infrastructure templating. Kustomize handles overlays without the overhead of a Helm chart. Layer a Helm chart on top of these manifests if your deployment grows more complex.
What you need
- A running Kubernetes cluster (AKS, EKS, GKE, k3s, kind, OpenShift, etc.)
kubectlconnected to your cluster- An API key for at least one model provider
Quick start
deploy.sh creates token auth by default. Retrieve the generated gateway token for the Control UI:
For local debugging, ./scripts/k8s/deploy.sh --show-token prints the token after deploy.
Local testing with Kind
If you do not have a cluster, create one locally with Kind:
Then deploy as usual with ./scripts/k8s/deploy.sh.
Step by step
1) Deploy
Option A: API key in environment (one step)
The script creates a Kubernetes Secret with the API key and an auto-generated gateway token, then deploys. If the Secret already exists, it preserves the current gateway token and any provider keys not being changed.
Option B: create the secret separately
Add --show-token to either command to print the token to stdout for local testing.
2) Access the gateway
What gets deployed
Keep replicas: 1 and the provided Recreate strategy so replacements stop the
old pod before starting another Gateway against the same state. The PVC must
use persistent local or block storage with a local filesystem, not an NFS, SMB,
or CIFS share. ReadWriteOnce alone guarantees neither that storage choice nor
one pod at a time. See SQLite storage requirements.
The Deployment checks /readyz for startup and traffic readiness with a five-minute startup budget, and /healthz for liveness. Every check asserts the JSON check contract rather than the status code alone, because the Control UI answers unknown paths with a catch-all 200; a status-only check would pass forever against an image whose check route does not exist yet.
/startupz is the better traffic-admission check because it ignores channel health, so one failing channel account cannot evict an otherwise healthy Gateway from Service endpoints. It requires an image built from 2026.8.1 or newer, the release that introduced it, which is newer than the tag pinned above. After pinning such an image, switch the startup and readiness checks to /startupz and keep /readyz for monitoring that should include channel-account health.
Customization
Agent instructions
Edit the AGENTS.md in scripts/k8s/manifests/configmap.yaml and redeploy:
Gateway config
Edit openclaw.json in scripts/k8s/manifests/configmap.yaml. See Gateway configuration for the full reference.
The init container seeds openclaw.json and workspace AGENTS.md only when each file is missing from the PVC. The persisted copy is the source of truth after first boot: changes made through OpenClaw (onboard, channels add, doctor --fix, Control UI) survive pod restarts, and updating the ConfigMap does not overwrite an existing PVC copy. To intentionally reseed a file from an updated ConfigMap, delete the persisted copy and restart:
Keep openclaw.json.last-good and the openclaw.json.bak* backups when reseeding.
A valid replacement does not need a meta block: openclaw doctor --fix keeps
it, and the Gateway promotes it to last-good after it starts successfully.
Truncated or invalid configs still use backup recovery. If recovery replaces a
file, the warning names the saved openclaw.json.clobbered.* original and explains
how to copy it back after correcting the reported problem.
Deployments created from the previous template applied ConfigMap edits on every pod start (and discarded any config changes made through OpenClaw). If you relied on that flow, use the reseed commands above after ConfigMap edits.
Temporary storage
The init container prepares a sticky-protected gateway-tmp directory (mode 1777) on the emptyDir volume and mounts it at /tmp with subPath. Keep this layout when customizing the temp volume: filesystem safety checks reject group- or world-writable ancestors without sticky protection, even when /tmp/openclaw itself is private. Setting fsGroup alone does not add the sticky bit.
The volume must be writable by the init container, which must share the Gateway's effective UID to create and chmod the directory. The recipe uses UID/GID and fsGroup 1000. On platforms that assign arbitrary UIDs, adapt these security contexts together. Preparation failures stop initialization and appear in kubectl logs -n openclaw deploy/openclaw -c init-config.
Private storage after a PVC remount
Kubernetes can widen existing volume permissions when applying fsGroup on
mount. The template requests fsGroupChangePolicy: OnRootMismatch so kubelet
can skip recursive permission changes when the volume root already matches.
This preserves private child modes only while that condition holds; it neither
repairs existing widened paths nor establishes that the contents are safe.
If the root stops matching, kubelet can walk the volume again. CSI drivers that
advertise VOLUME_MOUNT_GROUP handle permissions themselves, so this policy
does not apply to them. See Kubernetes volume permission policy.
Gateway startup and admitted Doctor repair restore the private modes of the
stores this installation owns before credential admission and Doctor's recovery
capture, so a remount
that widens them no longer blocks startup. The restore covers the client
credential store, the daemon runtime pin store, their SQLite sidecars, and the
sibling <stateDir>.update-captures directory.
The state directory itself is left as it is found. A shared, group-accessible state root is a supported layout — config publication preserves its mode while keeping the files inside private — so only the private stores under and beside it are restored.
The restore is deliberately narrow. It repairs an enumerated path list rather
than walking the state tree, changes no ownership, skips symlinks instead of
following them, leaves paths owned by another UID alone, and leaves any path
carrying world permission bits for its owner to refuse, because fsGroup never
sets those. Every downstream admission check stays as strict as before:
membership in the volume's group still does not grant private credential
authority.
Two cases still need the storage owner.
A kubelet-created mount point stays owned by another UID, and fsGroup leaves it
group-writable. Doctor's pre-repair recovery copy refuses a staging ancestor that
another user could replace, and it walks the whole ancestor chain, so it names the
mount point itself. Placing the state directory deeper under that mount does not
help. Restoring modes is not ownership transfer and an unprivileged container
cannot chown a foreign-owned mount point, so recovery capture needs a mount point
provisioned with the Gateway's ownership before startup, as described below. The
Gateway still starts and Doctor still completes; only the pre-repair recovery copy
is unavailable.
Sharing the same private state stores between workloads through group membership is not supported: the stores must remain private to their Gateway UID. A group-accessible PVC root can still contain separate UID-owned private subpaths for different workloads; sharing the PVC does not grant access to those stores.
If startup still reports Client credential directory is unsafe, keep the
Gateway stopped and have the storage owner inspect the state directory, its
client/credentials.sqlite store, and the sibling <stateDir>.update-captures
directory. Credential directories and files must remain private to the Gateway
UID (0700 and 0600, respectively). Recovery storage also needs trusted
ancestors that prevent other users from replacing its directories.
The shipped PVC mount covers /home/node/.openclaw, while retained update
captures resolve to /home/node/.openclaw.update-captures. That sibling needs a
writable, persistent location when adapting this template for recovery capture.
For a new or explicitly migrated layout, the storage owner can provision separate
private child directories on the PVC and expose them through subPath mounts at
/home/node/.openclaw and /home/node/.openclaw.update-captures. This keeps the
PVC root out of the Gateway's path ancestry. Both children must be owned by the
Gateway UID, with trusted ancestors at their container mount points. Their source
entries on the volume also need protection from replacement before they are
mounted. subPath alone does not establish ownership or protect private modes
from volume-wide permission changes.
This is an operator-provisioned layout, not an automatic upgrade of the supplied
mount. Deployments using the supplied mount store state at the PVC root.
Switching them to an empty child would hide that state and can initialize a different installation.
Before such a cutover, the storage owner must stop all state users, retain a
verified backup, and preserve the complete state and matching recovery data in
the selected children. Include config, credentials, databases and their sidecars,
and retained migration/backup artifacts. Preserve the old data until the new
mounts and recovery have been verified. Do not infer that the existing state/
directory is a suitable child: it contains the shared database, not the complete
OpenClaw state tree.
If a customized deployment instead mounts its PVC at /home/node and already
has complete .openclaw and .openclaw.update-captures children, those existing
directories can be selected as the subpaths without relocating their data. The
storage owner must still verify ownership, private modes, source-entry protection,
and the target mount ancestry before changing the mounts. Do not automatically
create missing children or reseed config as a substitute for existing data.
The Gateway performs this restore for its own enumerated paths. A
deployment-specific preparation step is still what establishes ownership of a
foreign-owned mount root, and can restore modes on exact, authorized, UID-owned
private paths after mounting and before Gateway or Doctor admission.
It must refuse symlinks, hardlinked files, foreign owners, unsafe ancestors, and
unsupported recovery states, and leave source/schema/backup validation intact.
Pathname chmod after a separate check is not a validated implementation of
these requirements. The unprivileged init container cannot make a foreign-owned
mount root private. Avoid recursive chmod or chown over the state tree.
Verify both the initial boot and a subsequent PVC remount with the selected
storage driver before relying on restart recovery.
Add providers
Re-run with additional keys exported:
Existing provider keys stay in the Secret unless you overwrite them.
Or patch the Secret directly:
Custom namespace
Custom image
Edit the image field in scripts/k8s/manifests/deployment.yaml:
Images from 2026.8.1 onward need a writable cache location; the shipped manifest provides it through XDG_CACHE_HOME. When upgrading an older deployment, apply that environment entry along with the image change.
The official image entrypoint runs Doctor before the Gateway, but this manifest's
explicit command bypasses it. Before upgrading an existing PVC, back up its
state, scale the Deployment to zero, wait for its pods to terminate, and run the
new image's openclaw doctor --fix --non-interactive in a one-off pod or Job with
the same state/config mounts. Restore one replica only after Doctor succeeds.
See container upgrade and recovery.
Expose beyond port-forward
The default manifests bind the gateway to loopback inside the pod. That works with kubectl port-forward, but not with a Kubernetes Service or Ingress path that needs to reach the pod IP directly.
To expose the gateway through an Ingress or load balancer:
- Change the gateway bind in
scripts/k8s/manifests/configmap.yamlfromloopbackto a non-loopback bind that matches your deployment model. - Keep gateway auth enabled and use a proper TLS-terminated entrypoint.
- Configure the Control UI for remote access using the supported web security model (for example HTTPS/Tailscale Serve and explicit allowed origins when needed).
Re-deploy
This applies all manifests and restarts the pod to pick up any config or secret changes.
Teardown
For the default openclaw namespace, this deletes the namespace and everything in it, including the PVC.
For a custom namespace, --delete removes only OpenClaw resources and preserves the namespace and unrelated workloads:
Use --delete-resources to request this scoped teardown explicitly in any namespace. Both scoped modes delete the OpenClaw Deployment, Service, PVC, ConfigMap, and generated Secret. Deleting the PVC removes OpenClaw's claim and access to its persisted data; whether the backing volume and data are deleted depends on the PersistentVolume or StorageClass reclaim policy (Delete or Retain).
To delete a custom namespace and every workload in it, explicitly opt in:
This also deletes unrelated workloads and the PVC.
Architecture notes
- The gateway binds to loopback inside the pod by default, so the included setup is for
kubectl port-forward. - No cluster-scoped resources; everything lives in a single namespace.
- Security hardening:
readOnlyRootFilesystem,drop: ALLcapabilities, non-root user (UID 1000). - Writable cache: the Gateway stages private SQLite snapshots under
XDG_CACHE_HOME(default~/.cache). The manifest points it at/home/node/.openclaw/.cacheon the PVC because the root filesystem is read-only. Keep an equivalent writable cache location if you customize the mounts. - The default config keeps the Control UI on the safer local-access path: loopback bind plus
kubectl port-forwardtohttp://127.0.0.1:18789. - If you move beyond localhost access, use the supported remote model: HTTPS/Tailscale plus the appropriate gateway bind and Control UI origin settings.
- Secrets are generated in a temp directory and applied directly to the cluster; no secret material is written to the repo checkout.