From 8e0e4f736a862a8c24e88a74276ce41ea1d9c27e Mon Sep 17 00:00:00 2001 From: Sally O'Malley Date: Thu, 12 Mar 2026 07:28:21 -0400 Subject: [PATCH] docs: add Kubernetes install guide, setup script, and manifests (#34492) * add docs and manifests for k8s install Signed-off-by: sallyom * changelog Signed-off-by: sallyom --------- Signed-off-by: sallyom --- CHANGELOG.md | 2 + docs/docs.json | 1 + docs/install/kubernetes.md | 191 +++++++++++++++++++ scripts/k8s/create-kind.sh | 209 ++++++++++++++++++++ scripts/k8s/deploy.sh | 231 +++++++++++++++++++++++ scripts/k8s/manifests/configmap.yaml | 38 ++++ scripts/k8s/manifests/deployment.yaml | 146 ++++++++++++++ scripts/k8s/manifests/kustomization.yaml | 7 + scripts/k8s/manifests/pvc.yaml | 12 ++ scripts/k8s/manifests/service.yaml | 15 ++ 10 files changed, 852 insertions(+) create mode 100644 docs/install/kubernetes.md create mode 100755 scripts/k8s/create-kind.sh create mode 100755 scripts/k8s/deploy.sh create mode 100644 scripts/k8s/manifests/configmap.yaml create mode 100644 scripts/k8s/manifests/deployment.yaml create mode 100644 scripts/k8s/manifests/kustomization.yaml create mode 100644 scripts/k8s/manifests/pvc.yaml create mode 100644 scripts/k8s/manifests/service.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index e7ed2833506..48513274800 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/docs/docs.json b/docs/docs.json index e6cf5ba382b..402d56aa380 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -876,6 +876,7 @@ "group": "Hosting and deployment", "pages": [ "vps", + "install/kubernetes", "install/fly", "install/hetzner", "install/gcp", diff --git a/docs/install/kubernetes.md b/docs/install/kubernetes.md new file mode 100644 index 00000000000..577ff9d2df5 --- /dev/null +++ b/docs/install/kubernetes.md @@ -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 _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 _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 _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":{"_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 +``` diff --git a/scripts/k8s/create-kind.sh b/scripts/k8s/create-kind.sh new file mode 100755 index 00000000000..688f576a70e --- /dev/null +++ b/scripts/k8s/create-kind.sh @@ -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 _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 </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 _API_KEY=\"...\" && ./scripts/k8s/deploy.sh" +echo "" diff --git a/scripts/k8s/deploy.sh b/scripts/k8s/deploy.sh new file mode 100755 index 00000000000..abd62dedf58 --- /dev/null +++ b/scripts/k8s/deploy.sh @@ -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 _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 _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" diff --git a/scripts/k8s/manifests/configmap.yaml b/scripts/k8s/manifests/configmap.yaml new file mode 100644 index 00000000000..2334b0370c8 --- /dev/null +++ b/scripts/k8s/manifests/configmap.yaml @@ -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. diff --git a/scripts/k8s/manifests/deployment.yaml b/scripts/k8s/manifests/deployment.yaml new file mode 100644 index 00000000000..f87c266930b --- /dev/null +++ b/scripts/k8s/manifests/deployment.yaml @@ -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: {} diff --git a/scripts/k8s/manifests/kustomization.yaml b/scripts/k8s/manifests/kustomization.yaml new file mode 100644 index 00000000000..7d1fa13e10c --- /dev/null +++ b/scripts/k8s/manifests/kustomization.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - pvc.yaml + - configmap.yaml + - deployment.yaml + - service.yaml diff --git a/scripts/k8s/manifests/pvc.yaml b/scripts/k8s/manifests/pvc.yaml new file mode 100644 index 00000000000..e834e788a0e --- /dev/null +++ b/scripts/k8s/manifests/pvc.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: openclaw-home-pvc + labels: + app: openclaw +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi diff --git a/scripts/k8s/manifests/service.yaml b/scripts/k8s/manifests/service.yaml new file mode 100644 index 00000000000..41df6219782 --- /dev/null +++ b/scripts/k8s/manifests/service.yaml @@ -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