Deploy OpenClaw Gateway to Kubernetes with Kustomize
Learn how to deploy OpenClaw Gateway to a Kubernetes cluster using a lightweight Kustomize template. Includes prerequisites, quick start steps, and local testing with Kind.
Read this when
- You want to run OpenClaw on a Kubernetes cluster
- You want to test OpenClaw in a Kubernetes environment
A lightweight starting template for running OpenClaw on Kubernetes, not something intended for production use. The core resources are included, and you are expected to tailor them to fit your own setup.
Why not Helm
OpenClaw ships as one container plus a handful of configuration files. The real customization happens in agent content, such as Markdown files, skills, and config overrides, rather than in infrastructure templating. Kustomize provides overlay support without the extra weight of a Helm chart. Should your deployment become more involved, you can add a Helm chart on top of these manifests.
What you need
- A Kubernetes cluster that is already up and running (AKS, EKS, GKE, k3s, kind, OpenShift, and similar)
kubectlpointed at your cluster- An API key for at least one model provider
Quick start
# Replace with your provider: ANTHROPIC, GEMINI, OPENAI, or OPENROUTER
export <PROVIDER>_API_KEY="..."
./scripts/k8s/deploy.sh
kubectl port-forward svc/openclaw 18789:18789 -n openclaw
open http://127.0.0.1:18789
Token authentication is enabled by default via deploy.sh. To get the generated gateway token for the Control UI:
kubectl get secret openclaw-secrets -n openclaw -o jsonpath='{.data.OPENCLAW_GATEWAY_TOKEN}' | base64 -d
When debugging locally, ./scripts/k8s/deploy.sh --show-token outputs the token right after deployment.
Local testing with Kind
No cluster available? Spin one up locally with Kind:
./scripts/k8s/create-kind.sh # auto-detects docker or podman
./scripts/k8s/create-kind.sh --delete # tear down
After that, deploy as usual using ./scripts/k8s/deploy.sh.
Step by step
1) Deploy
Option A: API key passed through the environment (single step)
# Replace with your provider: ANTHROPIC, GEMINI, OPENAI, or OPENROUTER
export <PROVIDER>_API_KEY="..."
./scripts/k8s/deploy.sh
This script builds a Kubernetes Secret holding the API key plus a gateway token that is generated on the fly, then runs the deployment. If the Secret is already present, the current gateway token is kept, and any provider keys that are not being modified are left alone.
Option B: secret creation done separately
export <PROVIDER>_API_KEY="..."
./scripts/k8s/deploy.sh --create-secret
./scripts/k8s/deploy.sh
Append --show-token to either command so the token is printed to stdout, which helps with local testing.
2) Access the gateway
kubectl port-forward svc/openclaw 18789:18789 -n openclaw
open http://127.0.0.1:18789
What gets deployed
Namespace: openclaw (configurable via OPENCLAW_NAMESPACE)
├── Deployment/openclaw # Single pod, init container + gateway
├── Service/openclaw # ClusterIP on port 18789
├── PersistentVolumeClaim # 10Gi for agent state and config
├── ConfigMap/openclaw-config # openclaw.json + AGENTS.md
└── Secret/openclaw-secrets # Gateway token + API keys
For startup and traffic readiness, the Deployment probes /readyz with a five-minute window allowed for startup, while /healthz handles liveness. Each probe checks the JSON probe contract rather than just the HTTP status, since the Control UI responds to unknown paths with a catch-all 200; a check that only looked at status would keep passing indefinitely against an image where the probe route is missing.
For traffic admission, /startupz is the preferred probe because it does not factor in channel health, meaning one failing channel account cannot remove a healthy Gateway from Service endpoints. This probe needs an image built from the release where it was introduced, which is newer than the tag pinned above. Once such an image is pinned, move the startup and readiness probes over to /startupz, and keep /readyz for monitoring that ought to cover channel-account health.
Customization
Agent instructions
Modify the AGENTS.md inside scripts/k8s/manifests/configmap.yaml and then redeploy:
./scripts/k8s/deploy.sh
Gateway config
Adjust openclaw.json in scripts/k8s/manifests/configmap.yaml. The complete reference is available in Gateway configuration.
The init container populates openclaw.json and workspace AGENTS.md only when those files are absent from the PVC. After the first boot, whatever is persisted on the PVC becomes authoritative: edits made through OpenClaw, such as onboard, channels add, doctor --fix, or the Control UI, survive pod restarts, and updating the ConfigMap will not overwrite what is already on the PVC. To deliberately reseed a file from a refreshed ConfigMap, remove the persisted copy and restart:
kubectl exec -n openclaw deploy/openclaw -- rm /home/node/.openclaw/openclaw.json
kubectl rollout restart -n openclaw deploy/openclaw
Deployments generated from the earlier template reapplied ConfigMap changes on every pod start, and any config adjustments made through OpenClaw were discarded in the process. If that workflow is what you depended on, run the reseed commands above after each ConfigMap change.
Add providers
Run it again with extra keys exported:
export ANTHROPIC_API_KEY="..."
export OPENAI_API_KEY="..."
./scripts/k8s/deploy.sh --create-secret
./scripts/k8s/deploy.sh
Provider keys that already exist in the Secret remain untouched unless you explicitly overwrite them.
Alternatively, patch the Secret directly:
kubectl patch secret openclaw-secrets -n openclaw \
-p '{"stringData":{"<PROVIDER>_API_KEY":"..."}}'
kubectl rollout restart deployment/openclaw -n openclaw
Custom namespace
OPENCLAW_NAMESPACE=my-namespace ./scripts/k8s/deploy.sh
Custom image
Change the image field located in scripts/k8s/manifests/deployment.yaml:
# Bump this immutable versioned tag when upgrading OpenClaw.
image: ghcr.io/openclaw/openclaw:2026.7.1-2-slim
Expose beyond port-forward
Out of the box, the manifests bind the gateway to loopback within the pod. That setup works fine with kubectl port-forward, but it will not work with a Kubernetes Service or an Ingress path that has to reach the pod IP directly.
To make the gateway reachable through an Ingress or load balancer:
- In
scripts/k8s/manifests/configmap.yaml, switch the gateway bind fromloopbackto a non-loopback address that fits your deployment model. - Leave gateway auth on and route traffic through a proper TLS-terminated entrypoint.
- Set up the Control UI for remote access following the supported web security model, for example HTTPS/Tailscale Serve with explicit allowed origins where applicable.
Re-deploy
./scripts/k8s/deploy.sh
This applies every manifest and restarts the pod so that any config or secret changes take effect.
Teardown
./scripts/k8s/deploy.sh --delete
For the default openclaw namespace, this action removes the namespace along with all of its contents, the PVC included.
When working with a custom namespace, --delete limits cleanup to OpenClaw resources only, leaving the namespace itself and any unrelated workloads untouched:
OPENCLAW_NAMESPACE=my-namespace ./scripts/k8s/deploy.sh --delete
To request this targeted removal explicitly in any namespace, call --delete-resources. In both scoped modes, the OpenClaw Deployment, Service, PVC, ConfigMap, and generated Secret are all removed. Once the PVC is gone, OpenClaw loses its claim and access to stored data; what happens to the underlying volume and its contents depends on the reclaim policy of the PersistentVolume or StorageClass (Delete or Retain).
If you want to wipe out a custom namespace along with every workload it holds, you must opt in explicitly:
OPENCLAW_NAMESPACE=my-namespace ./scripts/k8s/deploy.sh --delete-namespace
That command also removes unrelated workloads and the PVC.
Architecture notes
- Since the gateway binds to loopback within the pod by default, the bundled setup targets
kubectl port-forward. - No cluster-scoped resources exist; everything resides in one namespace.
- Hardening measures include
readOnlyRootFilesystem,drop: ALLcapabilities, and a non-root user (UID 1000). - Out of the box, the Control UI stays on the safer local-access route: loopback binding combined with
kubectl port-forwardto reachhttp://127.0.0.1:18789. - For anything beyond localhost, use the supported remote approach: HTTPS/Tailscale with the proper gateway bind and Control UI origin settings.
- Secrets get generated in a temporary directory and pushed straight to the cluster, so no secret material ever lands in the repo checkout.
File structure
scripts/k8s/
├── deploy.sh # Creates namespace + secret, deploys via kustomize
├── create-kind.sh # Local Kind cluster (auto-detects docker/podman)
└── manifests/
├── kustomization.yaml # Kustomize base
├── configmap.yaml # openclaw.json + AGENTS.md
├── deployment.yaml # Pod spec with security hardening
├── pvc.yaml # 10Gi persistent storage
└── service.yaml # ClusterIP on 18789