docs: add Kubernetes install guide, setup script, and manifests (#34492)

* add docs and manifests for k8s install

Signed-off-by: sallyom <somalley@redhat.com>

* changelog

Signed-off-by: sallyom <somalley@redhat.com>

---------

Signed-off-by: sallyom <somalley@redhat.com>
This commit is contained in:
Sally O'Malley 2026-03-12 07:28:21 -04:00 committed by GitHub
parent 4f620bebe5
commit 8e0e4f736a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 852 additions and 0 deletions

View File

@ -18,6 +18,8 @@ Docs: https://docs.openclaw.ai
### Changes
- Docs/Kubernetes: Add a starter K8s install path with raw manifests, Kind setup, and deployment docs. Thanks @sallyom @dzianisv @egkristi
### Fixes
- Cron/proactive delivery: keep isolated direct cron sends out of the write-ahead resend queue so transient-send retries do not replay duplicate proactive messages after restart. (#40646) Thanks @openperf and @vincentkoc.

View File

@ -876,6 +876,7 @@
"group": "Hosting and deployment",
"pages": [
"vps",
"install/kubernetes",
"install/fly",
"install/hetzner",
"install/gcp",

191
docs/install/kubernetes.md Normal file
View File

@ -0,0 +1,191 @@
---
summary: "Deploy OpenClaw Gateway to a Kubernetes cluster with Kustomize"
read_when:
- You want to run OpenClaw on a Kubernetes cluster
- You want to test OpenClaw in a Kubernetes environment
title: "Kubernetes"
---
# OpenClaw on 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. If your deployment grows more complex, a Helm chart can be layered on top of these manifests.
## What you need
- A running Kubernetes cluster (AKS, EKS, GKE, k3s, kind, OpenShift, etc.)
- `kubectl` connected to your cluster
- An API key for at least one model provider
## Quick start
```bash
# 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://localhost:18789
```
Retrieve the gateway token and paste it into the Control UI:
```bash
kubectl get secret openclaw-secrets -n openclaw -o jsonpath='{.data.OPENCLAW_GATEWAY_TOKEN}' | base64 -d
```
For local debugging, `./scripts/k8s/deploy.sh --show-token` prints the token after deploy.
## Local testing with Kind
If you don't have a cluster, create one locally with [Kind](https://kind.sigs.k8s.io/):
```bash
./scripts/k8s/create-kind.sh # auto-detects docker or podman
./scripts/k8s/create-kind.sh --delete # tear down
```
Then deploy as usual with `./scripts/k8s/deploy.sh`.
## Step by step
### 1) Deploy
**Option A** — API key in environment (one step):
```bash
# Replace with your provider: ANTHROPIC, GEMINI, OPENAI, or OPENROUTER
export <PROVIDER>_API_KEY="..."
./scripts/k8s/deploy.sh
```
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:
```bash
export <PROVIDER>_API_KEY="..."
./scripts/k8s/deploy.sh --create-secret
./scripts/k8s/deploy.sh
```
Use `--show-token` with either command if you want the token printed to stdout for local testing.
### 2) Access the gateway
```bash
kubectl port-forward svc/openclaw 18789:18789 -n openclaw
open http://localhost: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
```
## Customization
### Agent instructions
Edit the `AGENTS.md` in `scripts/k8s/manifests/configmap.yaml` and redeploy:
```bash
./scripts/k8s/deploy.sh
```
### Gateway config
Edit `openclaw.json` in `scripts/k8s/manifests/configmap.yaml`. See [Gateway configuration](/gateway/configuration) for the full reference.
### Add providers
Re-run with additional keys exported:
```bash
export ANTHROPIC_API_KEY="..."
export OPENAI_API_KEY="..."
./scripts/k8s/deploy.sh --create-secret
./scripts/k8s/deploy.sh
```
Existing provider keys stay in the Secret unless you overwrite them.
Or patch the Secret directly:
```bash
kubectl patch secret openclaw-secrets -n openclaw \
-p '{"stringData":{"<PROVIDER>_API_KEY":"..."}}'
kubectl rollout restart deployment/openclaw -n openclaw
```
### Custom namespace
```bash
OPENCLAW_NAMESPACE=my-namespace ./scripts/k8s/deploy.sh
```
### Custom image
Edit the `image` field in `scripts/k8s/manifests/deployment.yaml`:
```yaml
image: ghcr.io/openclaw/openclaw:2026.3.1
```
### Expose beyond port-forward
The default manifests bind the gateway to loopback inside the pod. That works with `kubectl port-forward`, but it does not work with a Kubernetes `Service` or Ingress path that needs to reach the pod IP.
If you want to expose the gateway through an Ingress or load balancer:
- Change the gateway bind in `scripts/k8s/manifests/configmap.yaml` from `loopback` to 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
```bash
./scripts/k8s/deploy.sh
```
This applies all manifests and restarts the pod to pick up any config or secret changes.
## Teardown
```bash
./scripts/k8s/deploy.sh --delete
```
This deletes the namespace and all resources in it, including 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: `readOnlyRootFilesystem`, `drop: ALL` capabilities, non-root user (UID 1000)
- The default config keeps the Control UI on the safer local-access path: loopback bind plus `kubectl port-forward` to `http://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
## 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
```

209
scripts/k8s/create-kind.sh Executable file
View File

@ -0,0 +1,209 @@
#!/usr/bin/env bash
# ============================================================================
# KIND CLUSTER BOOTSTRAP SCRIPT
# ============================================================================
#
# Usage:
# ./scripts/k8s/create-kind.sh # Create with auto-detected engine
# ./scripts/k8s/create-kind.sh --name mycluster
# ./scripts/k8s/create-kind.sh --delete
#
# After creation, deploy with:
# export <AI_PROVIDER>_API_KEY="..." && ./scripts/k8s/deploy.sh
# ============================================================================
set -euo pipefail
# Defaults
CLUSTER_NAME="openclaw"
CONTAINER_CMD=""
DELETE=false
# Colors
GREEN='\033[0;32m'
BLUE='\033[0;34m'
YELLOW='\033[0;33m'
RED='\033[0;31m'
NC='\033[0m'
info() { echo -e "${BLUE}[INFO]${NC} $1"; }
success() { echo -e "${GREEN}[OK]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
fail() { echo -e "${RED}[ERROR]${NC} $1" >&2; exit 1; }
usage() {
cat <<EOF
Usage: $(basename "$0") [OPTIONS]
Options:
--name NAME Cluster name (default: openclaw)
--delete Delete the cluster instead of creating it
-h, --help Show this help message
Examples:
$(basename "$0") # Create cluster (auto-detect engine)
$(basename "$0") --delete # Delete the cluster
$(basename "$0") --name dev --delete # Delete a cluster named "dev"
EOF
exit 0
}
# ---------------------------------------------------------------------------
# Argument parsing
# ---------------------------------------------------------------------------
while [[ $# -gt 0 ]]; do
case "$1" in
--name)
[[ -z "${2:-}" ]] && fail "--name requires a value"
CLUSTER_NAME="$2"; shift 2 ;;
--delete)
DELETE=true; shift ;;
-h|--help)
usage ;;
*)
fail "Unknown option: $1 (see --help)" ;;
esac
done
# ---------------------------------------------------------------------------
# Container engine detection
# ---------------------------------------------------------------------------
provider_installed() {
command -v "$1" &>/dev/null
}
provider_responsive() {
case "$1" in
docker)
docker info &>/dev/null
;;
podman)
podman info &>/dev/null
;;
*)
return 1
;;
esac
}
detect_provider() {
local candidate
for candidate in podman docker; do
if provider_installed "$candidate" && provider_responsive "$candidate"; then
echo "$candidate"
return 0
fi
done
for candidate in podman docker; do
if provider_installed "$candidate"; then
case "$candidate" in
podman)
fail "Podman is installed but not responding, and no responsive Docker daemon was found. Ensure the podman machine is running (podman machine start) or start Docker."
;;
docker)
fail "Docker is installed but not running, and no responsive Podman machine was found. Start Docker or start Podman."
;;
esac
fi
done
fail "Neither podman nor docker found. Install one to use Kind."
}
CONTAINER_CMD=$(detect_provider)
info "Auto-detected container engine: $CONTAINER_CMD"
# ---------------------------------------------------------------------------
# Prerequisites
# ---------------------------------------------------------------------------
if ! command -v kind &>/dev/null; then
fail "kind is not installed. Install it from https://kind.sigs.k8s.io/"
fi
if ! command -v kubectl &>/dev/null; then
fail "kubectl is not installed. Install it before creating or managing a Kind cluster."
fi
# Verify the container engine is responsive
if ! provider_responsive "$CONTAINER_CMD"; then
if [[ "$CONTAINER_CMD" == "docker" ]]; then
fail "Docker daemon is not running. Start it and try again."
elif [[ "$CONTAINER_CMD" == "podman" ]]; then
fail "Podman is not responding. Ensure the podman machine is running (podman machine start)."
fi
fi
# ---------------------------------------------------------------------------
# Delete mode
# ---------------------------------------------------------------------------
if $DELETE; then
info "Deleting Kind cluster '$CLUSTER_NAME'..."
if KIND_EXPERIMENTAL_PROVIDER="$CONTAINER_CMD" kind get clusters 2>/dev/null | grep -qx "$CLUSTER_NAME"; then
KIND_EXPERIMENTAL_PROVIDER="$CONTAINER_CMD" kind delete cluster --name "$CLUSTER_NAME"
success "Cluster '$CLUSTER_NAME' deleted."
else
warn "Cluster '$CLUSTER_NAME' does not exist."
fi
exit 0
fi
# ---------------------------------------------------------------------------
# Check if cluster already exists
# ---------------------------------------------------------------------------
if KIND_EXPERIMENTAL_PROVIDER="$CONTAINER_CMD" kind get clusters 2>/dev/null | grep -qx "$CLUSTER_NAME"; then
warn "Cluster '$CLUSTER_NAME' already exists."
info "To recreate it, run: $0 --name \"$CLUSTER_NAME\" --delete && $0 --name \"$CLUSTER_NAME\""
info "Switching kubectl context to kind-$CLUSTER_NAME..."
kubectl config use-context "kind-$CLUSTER_NAME" &>/dev/null && success "Context set." || warn "Could not switch context."
exit 0
fi
# ---------------------------------------------------------------------------
# Create cluster
# ---------------------------------------------------------------------------
info "Creating Kind cluster '$CLUSTER_NAME' (provider: $CONTAINER_CMD)..."
KIND_EXPERIMENTAL_PROVIDER="$CONTAINER_CMD" kind create cluster \
--name "$CLUSTER_NAME" \
--config - <<'KINDCFG'
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
labels:
openclaw.dev/role: control-plane
# Uncomment to expose services on host ports:
# extraPortMappings:
# - containerPort: 30080
# hostPort: 8080
# protocol: TCP
# - containerPort: 30443
# hostPort: 8443
# protocol: TCP
KINDCFG
success "Kind cluster '$CLUSTER_NAME' created."
# ---------------------------------------------------------------------------
# Wait for readiness
# ---------------------------------------------------------------------------
info "Waiting for cluster to be ready..."
kubectl --context "kind-$CLUSTER_NAME" wait --for=condition=Ready nodes --all --timeout=120s >/dev/null
success "All nodes are Ready."
# ---------------------------------------------------------------------------
# Summary
# ---------------------------------------------------------------------------
echo ""
echo "---------------------------------------------------------------"
echo " Kind cluster '$CLUSTER_NAME' is ready"
echo "---------------------------------------------------------------"
echo ""
echo " kubectl cluster-info --context kind-$CLUSTER_NAME"
echo ""
echo ""
echo " export <AI_PROVIDER>_API_KEY=\"...\" && ./scripts/k8s/deploy.sh"
echo ""

231
scripts/k8s/deploy.sh Executable file
View File

@ -0,0 +1,231 @@
#!/usr/bin/env bash
# Deploy OpenClaw to Kubernetes.
#
# Secrets are generated in a temp directory and applied server-side.
# No secret material is ever written to the repo checkout.
#
# Usage:
# ./scripts/k8s/deploy.sh # Deploy (requires API key in env or secret already in cluster)
# ./scripts/k8s/deploy.sh --create-secret # Create or update the K8s Secret from env vars
# ./scripts/k8s/deploy.sh --show-token # Print the gateway token after deploy
# ./scripts/k8s/deploy.sh --delete # Tear down
#
# Environment:
# OPENCLAW_NAMESPACE Kubernetes namespace (default: openclaw)
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
MANIFESTS="$SCRIPT_DIR/manifests"
NS="${OPENCLAW_NAMESPACE:-openclaw}"
# Check prerequisites
for cmd in kubectl openssl; do
command -v "$cmd" &>/dev/null || { echo "Missing: $cmd" >&2; exit 1; }
done
kubectl cluster-info &>/dev/null || { echo "Cannot connect to cluster. Check kubeconfig." >&2; exit 1; }
# ---------------------------------------------------------------------------
# -h / --help
# ---------------------------------------------------------------------------
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
cat <<'HELP'
Usage: ./scripts/k8s/deploy.sh [OPTION]
(no args) Deploy OpenClaw (creates secret from env if needed)
--create-secret Create or update the K8s Secret from env vars without deploying
--show-token Print the gateway token after deploy or secret creation
--delete Delete the namespace and all resources
-h, --help Show this help
Environment:
Export at least one provider API key:
ANTHROPIC_API_KEY, GEMINI_API_KEY, OPENAI_API_KEY, OPENROUTER_API_KEY
OPENCLAW_NAMESPACE Kubernetes namespace (default: openclaw)
HELP
exit 0
fi
SHOW_TOKEN=false
MODE="deploy"
while [[ $# -gt 0 ]]; do
case "$1" in
--create-secret)
MODE="create-secret"
;;
--delete)
MODE="delete"
;;
--show-token)
SHOW_TOKEN=true
;;
*)
echo "Unknown option: $1" >&2
echo "Run ./scripts/k8s/deploy.sh --help for usage." >&2
exit 1
;;
esac
shift
done
# ---------------------------------------------------------------------------
# --delete
# ---------------------------------------------------------------------------
if [[ "$MODE" == "delete" ]]; then
echo "Deleting namespace '$NS' and all resources..."
kubectl delete namespace "$NS" --ignore-not-found
echo "Done."
exit 0
fi
# ---------------------------------------------------------------------------
# Create and apply Secret to the cluster
# ---------------------------------------------------------------------------
_apply_secret() {
local TMP_DIR
local EXISTING_SECRET=false
local EXISTING_TOKEN=""
local ANTHROPIC_VALUE=""
local OPENAI_VALUE=""
local GEMINI_VALUE=""
local OPENROUTER_VALUE=""
local TOKEN
local SECRET_MANIFEST
TMP_DIR="$(mktemp -d)"
chmod 700 "$TMP_DIR"
trap 'rm -rf "$TMP_DIR"' EXIT
if kubectl get secret openclaw-secrets -n "$NS" &>/dev/null; then
EXISTING_SECRET=true
EXISTING_TOKEN="$(kubectl get secret openclaw-secrets -n "$NS" -o jsonpath='{.data.OPENCLAW_GATEWAY_TOKEN}' | base64 -d)"
ANTHROPIC_VALUE="$(kubectl get secret openclaw-secrets -n "$NS" -o jsonpath='{.data.ANTHROPIC_API_KEY}' 2>/dev/null | base64 -d)"
OPENAI_VALUE="$(kubectl get secret openclaw-secrets -n "$NS" -o jsonpath='{.data.OPENAI_API_KEY}' 2>/dev/null | base64 -d)"
GEMINI_VALUE="$(kubectl get secret openclaw-secrets -n "$NS" -o jsonpath='{.data.GEMINI_API_KEY}' 2>/dev/null | base64 -d)"
OPENROUTER_VALUE="$(kubectl get secret openclaw-secrets -n "$NS" -o jsonpath='{.data.OPENROUTER_API_KEY}' 2>/dev/null | base64 -d)"
fi
TOKEN="${EXISTING_TOKEN:-$(openssl rand -hex 32)}"
ANTHROPIC_VALUE="${ANTHROPIC_API_KEY:-$ANTHROPIC_VALUE}"
OPENAI_VALUE="${OPENAI_API_KEY:-$OPENAI_VALUE}"
GEMINI_VALUE="${GEMINI_API_KEY:-$GEMINI_VALUE}"
OPENROUTER_VALUE="${OPENROUTER_API_KEY:-$OPENROUTER_VALUE}"
SECRET_MANIFEST="$TMP_DIR/secrets.yaml"
# Write secret material to temp files so kubectl handles encoding safely.
printf '%s' "$TOKEN" > "$TMP_DIR/OPENCLAW_GATEWAY_TOKEN"
printf '%s' "$ANTHROPIC_VALUE" > "$TMP_DIR/ANTHROPIC_API_KEY"
printf '%s' "$OPENAI_VALUE" > "$TMP_DIR/OPENAI_API_KEY"
printf '%s' "$GEMINI_VALUE" > "$TMP_DIR/GEMINI_API_KEY"
printf '%s' "$OPENROUTER_VALUE" > "$TMP_DIR/OPENROUTER_API_KEY"
chmod 600 \
"$TMP_DIR/OPENCLAW_GATEWAY_TOKEN" \
"$TMP_DIR/ANTHROPIC_API_KEY" \
"$TMP_DIR/OPENAI_API_KEY" \
"$TMP_DIR/GEMINI_API_KEY" \
"$TMP_DIR/OPENROUTER_API_KEY"
kubectl create secret generic openclaw-secrets \
-n "$NS" \
--from-file=OPENCLAW_GATEWAY_TOKEN="$TMP_DIR/OPENCLAW_GATEWAY_TOKEN" \
--from-file=ANTHROPIC_API_KEY="$TMP_DIR/ANTHROPIC_API_KEY" \
--from-file=OPENAI_API_KEY="$TMP_DIR/OPENAI_API_KEY" \
--from-file=GEMINI_API_KEY="$TMP_DIR/GEMINI_API_KEY" \
--from-file=OPENROUTER_API_KEY="$TMP_DIR/OPENROUTER_API_KEY" \
--dry-run=client \
-o yaml > "$SECRET_MANIFEST"
chmod 600 "$SECRET_MANIFEST"
kubectl create namespace "$NS" --dry-run=client -o yaml | kubectl apply -f - >/dev/null
kubectl apply --server-side --field-manager=openclaw -f "$SECRET_MANIFEST" >/dev/null
# Clean up any annotation left by older client-side apply runs.
kubectl annotate secret openclaw-secrets -n "$NS" kubectl.kubernetes.io/last-applied-configuration- >/dev/null 2>&1 || true
rm -rf "$TMP_DIR"
trap - EXIT
if $EXISTING_SECRET; then
echo "Secret updated in namespace '$NS'. Existing gateway token preserved."
else
echo "Secret created in namespace '$NS'."
fi
if $SHOW_TOKEN; then
echo "Gateway token: $TOKEN"
else
echo "Gateway token stored in Secret only."
echo "Retrieve it with:"
echo " kubectl get secret openclaw-secrets -n $NS -o jsonpath='{.data.OPENCLAW_GATEWAY_TOKEN}' | base64 -d && echo"
fi
}
# ---------------------------------------------------------------------------
# --create-secret
# ---------------------------------------------------------------------------
if [[ "$MODE" == "create-secret" ]]; then
HAS_KEY=false
for key in ANTHROPIC_API_KEY OPENAI_API_KEY GEMINI_API_KEY OPENROUTER_API_KEY; do
if [[ -n "${!key:-}" ]]; then
HAS_KEY=true
echo " Found $key in environment"
fi
done
if ! $HAS_KEY; then
echo "No API keys found in environment. Export at least one and re-run:"
echo " export <PROVIDER>_API_KEY=\"...\" (ANTHROPIC, GEMINI, OPENAI, or OPENROUTER)"
echo " ./scripts/k8s/deploy.sh --create-secret"
exit 1
fi
_apply_secret
echo ""
echo "Now run:"
echo " ./scripts/k8s/deploy.sh"
exit 0
fi
# ---------------------------------------------------------------------------
# Check that the secret exists in the cluster
# ---------------------------------------------------------------------------
if ! kubectl get secret openclaw-secrets -n "$NS" &>/dev/null; then
HAS_KEY=false
for key in ANTHROPIC_API_KEY OPENAI_API_KEY GEMINI_API_KEY OPENROUTER_API_KEY; do
[[ -n "${!key:-}" ]] && HAS_KEY=true
done
if $HAS_KEY; then
echo "Creating secret from environment..."
_apply_secret
echo ""
else
echo "No secret found and no API keys in environment."
echo ""
echo "Export at least one provider API key and re-run:"
echo " export <PROVIDER>_API_KEY=\"...\" (ANTHROPIC, GEMINI, OPENAI, or OPENROUTER)"
echo " ./scripts/k8s/deploy.sh"
exit 1
fi
fi
# ---------------------------------------------------------------------------
# Deploy
# ---------------------------------------------------------------------------
echo "Deploying to namespace '$NS'..."
kubectl create namespace "$NS" --dry-run=client -o yaml | kubectl apply -f - >/dev/null
kubectl apply -k "$MANIFESTS" -n "$NS"
kubectl rollout restart deployment/openclaw -n "$NS" 2>/dev/null || true
echo ""
echo "Waiting for rollout..."
kubectl rollout status deployment/openclaw -n "$NS" --timeout=300s
echo ""
echo "Done. Access the gateway:"
echo " kubectl port-forward svc/openclaw 18789:18789 -n $NS"
echo " open http://localhost:18789"
echo ""
if $SHOW_TOKEN; then
echo "Gateway token (paste into Control UI):"
echo " $(kubectl get secret openclaw-secrets -n "$NS" -o jsonpath='{.data.OPENCLAW_GATEWAY_TOKEN}' | base64 -d)"
echo ""
fi
echo "Retrieve the gateway token with:"
echo " kubectl get secret openclaw-secrets -n $NS -o jsonpath='{.data.OPENCLAW_GATEWAY_TOKEN}' | base64 -d && echo"

View File

@ -0,0 +1,38 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: openclaw-config
labels:
app: openclaw
data:
openclaw.json: |
{
"gateway": {
"mode": "local",
"bind": "loopback",
"port": 18789,
"auth": {
"mode": "token"
},
"controlUi": {
"enabled": true
}
},
"agents": {
"defaults": {
"workspace": "~/.openclaw/workspace"
},
"list": [
{
"id": "default",
"name": "OpenClaw Assistant",
"workspace": "~/.openclaw/workspace"
}
]
},
"cron": { "enabled": false }
}
AGENTS.md: |
# OpenClaw Assistant
You are a helpful AI assistant running in Kubernetes.

View File

@ -0,0 +1,146 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: openclaw
labels:
app: openclaw
spec:
replicas: 1
selector:
matchLabels:
app: openclaw
strategy:
type: Recreate
template:
metadata:
labels:
app: openclaw
spec:
automountServiceAccountToken: false
securityContext:
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
initContainers:
- name: init-config
image: busybox:1.37
imagePullPolicy: IfNotPresent
command:
- sh
- -c
- |
cp /config/openclaw.json /home/node/.openclaw/openclaw.json
mkdir -p /home/node/.openclaw/workspace
cp /config/AGENTS.md /home/node/.openclaw/workspace/AGENTS.md
securityContext:
runAsUser: 1000
runAsGroup: 1000
resources:
requests:
memory: 32Mi
cpu: 50m
limits:
memory: 64Mi
cpu: 100m
volumeMounts:
- name: openclaw-home
mountPath: /home/node/.openclaw
- name: config
mountPath: /config
containers:
- name: gateway
image: ghcr.io/openclaw/openclaw:slim
imagePullPolicy: IfNotPresent
command:
- node
- /app/dist/index.js
- gateway
- run
ports:
- name: gateway
containerPort: 18789
protocol: TCP
env:
- name: HOME
value: /home/node
- name: OPENCLAW_CONFIG_DIR
value: /home/node/.openclaw
- name: NODE_ENV
value: production
- name: OPENCLAW_GATEWAY_TOKEN
valueFrom:
secretKeyRef:
name: openclaw-secrets
key: OPENCLAW_GATEWAY_TOKEN
- name: ANTHROPIC_API_KEY
valueFrom:
secretKeyRef:
name: openclaw-secrets
key: ANTHROPIC_API_KEY
optional: true
- name: OPENAI_API_KEY
valueFrom:
secretKeyRef:
name: openclaw-secrets
key: OPENAI_API_KEY
optional: true
- name: GEMINI_API_KEY
valueFrom:
secretKeyRef:
name: openclaw-secrets
key: GEMINI_API_KEY
optional: true
- name: OPENROUTER_API_KEY
valueFrom:
secretKeyRef:
name: openclaw-secrets
key: OPENROUTER_API_KEY
optional: true
resources:
requests:
memory: 512Mi
cpu: 250m
limits:
memory: 2Gi
cpu: "1"
livenessProbe:
exec:
command:
- node
- -e
- "require('http').get('http://127.0.0.1:18789/healthz', r => process.exit(r.statusCode < 400 ? 0 : 1)).on('error', () => process.exit(1))"
initialDelaySeconds: 60
periodSeconds: 30
timeoutSeconds: 10
readinessProbe:
exec:
command:
- node
- -e
- "require('http').get('http://127.0.0.1:18789/readyz', r => process.exit(r.statusCode < 400 ? 0 : 1)).on('error', () => process.exit(1))"
initialDelaySeconds: 15
periodSeconds: 10
timeoutSeconds: 5
volumeMounts:
- name: openclaw-home
mountPath: /home/node/.openclaw
- name: tmp-volume
mountPath: /tmp
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
volumes:
- name: openclaw-home
persistentVolumeClaim:
claimName: openclaw-home-pvc
- name: config
configMap:
name: openclaw-config
- name: tmp-volume
emptyDir: {}

View File

@ -0,0 +1,7 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- pvc.yaml
- configmap.yaml
- deployment.yaml
- service.yaml

View File

@ -0,0 +1,12 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: openclaw-home-pvc
labels:
app: openclaw
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi

View File

@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: openclaw
labels:
app: openclaw
spec:
type: ClusterIP
selector:
app: openclaw
ports:
- name: gateway
port: 18789
targetPort: 18789
protocol: TCP