From faf4e3eee280d0dbe845d687b8d7010a968208b2 Mon Sep 17 00:00:00 2001 From: sHa Date: Sun, 27 Sep 2026 13:19:14 +0300 Subject: [PATCH] Add openclaw Helm chart (openclaw/openclaw Gateway) --- README.md | 1 + openclaw/Chart.yaml | 14 +++ openclaw/templates/configmap.yaml | 12 +++ openclaw/templates/deployment.yaml | 163 +++++++++++++++++++++++++++++ openclaw/templates/ingress.yaml | 27 +++++ openclaw/templates/pvc.yaml | 18 ++++ openclaw/templates/service.yaml | 16 +++ openclaw/values.yaml | 102 ++++++++++++++++++ 8 files changed, 353 insertions(+) create mode 100644 openclaw/Chart.yaml create mode 100644 openclaw/templates/configmap.yaml create mode 100644 openclaw/templates/deployment.yaml create mode 100644 openclaw/templates/ingress.yaml create mode 100644 openclaw/templates/pvc.yaml create mode 100644 openclaw/templates/service.yaml create mode 100644 openclaw/values.yaml diff --git a/README.md b/README.md index 71c92a0..63d786a 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ cd helm-charts | [Home Assistant](docs/home-assistant.md) | Home automation with PostgreSQL recorder and managed secrets | Ready | | [ESPHome](docs/esphome.md) | ESPHome dashboard with host network for device discovery | Ready | | Frigate | NVR with Rockchip RK3588 hardware acceleration (rkmpp + rknn) | Ready | +| OpenClaw | OpenClaw Gateway (AI assistant server), seed-if-missing config, PVC state | Ready | ### 3. Usage with FluxCD diff --git a/openclaw/Chart.yaml b/openclaw/Chart.yaml new file mode 100644 index 0000000..b51d468 --- /dev/null +++ b/openclaw/Chart.yaml @@ -0,0 +1,14 @@ +apiVersion: v2 +name: openclaw +description: >- + OpenClaw Gateway (personal AI assistant server). Own chart (not a wrapper): + upstream ships only raw Kustomize manifests (scripts/k8s/manifests), and the + Gateway is a single Deployment + Service + PVC + seed ConfigMap, so the image + is tracked directly off openclaw/openclaw releases via the annotation below + and scripts/update-charts.sh. +type: application +version: 0.1.0 +appVersion: "2026.9.6" +annotations: + version-source: github-release:openclaw/openclaw + version-pattern: "s|^v||" diff --git a/openclaw/templates/configmap.yaml b/openclaw/templates/configmap.yaml new file mode 100644 index 0000000..61865f3 --- /dev/null +++ b/openclaw/templates/configmap.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Release.Name }}-seed + labels: + app.kubernetes.io/name: {{ .Chart.Name }} + app.kubernetes.io/instance: {{ .Release.Name }} +data: + openclaw.json: | + {{- toPrettyJson .Values.config | nindent 4 }} + AGENTS.md: | + {{- .Values.agentsMd | nindent 4 }} diff --git a/openclaw/templates/deployment.yaml b/openclaw/templates/deployment.yaml new file mode 100644 index 0000000..16161fa --- /dev/null +++ b/openclaw/templates/deployment.yaml @@ -0,0 +1,163 @@ +{{- $tag := .Chart.AppVersion -}} +{{- with .Values.image.variant }}{{ $tag = printf "%s-%s" $tag . }}{{ end -}} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Release.Name }} + labels: + app.kubernetes.io/name: {{ .Chart.Name }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} + app.kubernetes.io/component: gateway + annotations: + version-source: {{ index .Chart.Annotations "version-source" }} +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app: {{ .Release.Name }} + template: + metadata: + labels: + app: {{ .Release.Name }} + annotations: + checksum/seed: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + spec: + automountServiceAccountToken: false + {{- with .Values.securityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + initContainers: + # Seed-if-missing: the PVC copy owns config after first boot + - name: seed-config + image: busybox:1.38 + imagePullPolicy: IfNotPresent + command: + - sh + - -c + - | + [ -f /home/node/.openclaw/openclaw.json ] || cp /seed/openclaw.json /home/node/.openclaw/openclaw.json + mkdir -p /home/node/.openclaw/workspace + [ -f /home/node/.openclaw/workspace/AGENTS.md ] || cp /seed/AGENTS.md /home/node/.openclaw/workspace/AGENTS.md + {{- with .Values.containerSecurityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + resources: + requests: + cpu: 10m + memory: 16Mi + limits: + cpu: 100m + memory: 64Mi + volumeMounts: + - name: data + mountPath: /home/node/.openclaw + - name: seed + mountPath: /seed + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.image.repository }}:{{ $tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: + - node + - /app/dist/index.js + - gateway + - run + {{- with .Values.containerSecurityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + ports: + - containerPort: {{ .Values.httpPort }} + name: http + env: + - name: TZ + value: {{ .Values.timezone | quote }} + - name: HOME + value: /home/node + - name: OPENCLAW_CONFIG_DIR + value: /home/node/.openclaw + - name: NODE_ENV + value: production + {{- range $key, $value := .Values.env }} + - name: {{ $key }} + value: {{ $value | quote }} + {{- end }} + {{- with .Values.existingSecret }} + envFrom: + - secretRef: + name: {{ . }} + {{- end }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + # Probes assert the JSON probe contract, not just the status code: the + # Control UI answers unknown paths with a catch-all 200. /startupz + # ignores channel health, so one failing channel account can't pull + # the Gateway out of Service endpoints (/readyz is channel-aware). + startupProbe: + exec: + command: + - node + - -e + - "require('http').get('http://127.0.0.1:{{ .Values.httpPort }}/startupz', r => { let b = ''; r.on('data', d => b += d); r.on('end', () => { try { process.exit(r.statusCode < 400 && JSON.parse(b).status === 'started' ? 0 : 1); } catch { process.exit(1); } }); }).on('error', () => process.exit(1))" + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 30 + readinessProbe: + exec: + command: + - node + - -e + - "require('http').get('http://127.0.0.1:{{ .Values.httpPort }}/startupz', r => { let b = ''; r.on('data', d => b += d); r.on('end', () => { try { process.exit(r.statusCode < 400 && JSON.parse(b).status === 'started' ? 0 : 1); } catch { process.exit(1); } }); }).on('error', () => process.exit(1))" + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + livenessProbe: + exec: + command: + - node + - -e + - "require('http').get('http://127.0.0.1:{{ .Values.httpPort }}/healthz', r => { let b = ''; r.on('data', d => b += d); r.on('end', () => { try { process.exit(r.statusCode < 400 && JSON.parse(b).ok === true ? 0 : 1); } catch { process.exit(1); } }); }).on('error', () => process.exit(1))" + periodSeconds: 30 + timeoutSeconds: 10 + failureThreshold: 3 + volumeMounts: + - name: data + mountPath: /home/node/.openclaw + - name: tmp + mountPath: /tmp + - name: shm + mountPath: /dev/shm + volumes: + - name: data + {{- if .Values.persistence.enabled }} + persistentVolumeClaim: + claimName: {{ .Values.persistence.existingClaim | default (printf "%s-data" .Release.Name) }} + {{- else }} + emptyDir: {} + {{- end }} + - name: seed + configMap: + name: {{ .Release.Name }}-seed + - name: tmp + emptyDir: {} + - name: shm + emptyDir: + medium: Memory + sizeLimit: {{ .Values.shmSize }} diff --git a/openclaw/templates/ingress.yaml b/openclaw/templates/ingress.yaml new file mode 100644 index 0000000..9eca732 --- /dev/null +++ b/openclaw/templates/ingress.yaml @@ -0,0 +1,27 @@ +{{- range $name, $config := .Values.ingresses }} +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ $.Release.Name }}-{{ $name }} + {{- with $config.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + rules: + - host: {{ $config.host }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: {{ $.Release.Name }} + port: + number: {{ $.Values.service.port }} + {{- with $config.tls }} + tls: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/openclaw/templates/pvc.yaml b/openclaw/templates/pvc.yaml new file mode 100644 index 0000000..c50ea39 --- /dev/null +++ b/openclaw/templates/pvc.yaml @@ -0,0 +1,18 @@ +{{- if and .Values.persistence.enabled (not .Values.persistence.existingClaim) }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ .Release.Name }}-data + labels: + app.kubernetes.io/name: {{ .Chart.Name }} + app.kubernetes.io/instance: {{ .Release.Name }} +spec: + accessModes: + {{- toYaml .Values.persistence.accessModes | nindent 4 }} + {{- with .Values.persistence.storageClass }} + storageClassName: {{ . }} + {{- end }} + resources: + requests: + storage: {{ .Values.persistence.size }} +{{- end }} diff --git a/openclaw/templates/service.yaml b/openclaw/templates/service.yaml new file mode 100644 index 0000000..ef6cea2 --- /dev/null +++ b/openclaw/templates/service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Release.Name }} + labels: + app.kubernetes.io/name: {{ .Chart.Name }} + app.kubernetes.io/instance: {{ .Release.Name }} +spec: + type: {{ .Values.service.type }} + selector: + app: {{ .Release.Name }} + ports: + - name: http + port: {{ .Values.service.port }} + targetPort: http + protocol: TCP diff --git a/openclaw/values.yaml b/openclaw/values.yaml new file mode 100644 index 0000000..6621769 --- /dev/null +++ b/openclaw/values.yaml @@ -0,0 +1,102 @@ +# Default values for OpenClaw Gateway Helm Chart +image: + repository: ghcr.io/openclaw/openclaw + # Tag is "" or "-". Variants: "" (default, + # bundles codex + diagnostics-otel), "slim", "browser" (Chromium baked in for + # the Gateway-controlled browser). + variant: slim + pullPolicy: IfNotPresent + +timezone: "Europe/Kyiv" + +# Gateway port (WS + HTTP multiplexed: Control UI, probes, API) +httpPort: 18789 + +service: + type: ClusterIP + port: 18789 + +# Secret with Gateway/provider credentials, loaded whole via envFrom. Keys +# become env vars, e.g. OPENCLAW_GATEWAY_TOKEN (required: non-loopback binds +# refuse to start without gateway auth), ANTHROPIC_OAUTH_TOKEN (Claude +# setup-token), ANTHROPIC_API_KEY, OPENAI_API_KEY, ... The chart never creates it. +existingSecret: "" + +# Extra plain environment variables (rendered as name/value pairs) +env: {} + +# Seed config. Copied into the PVC only when missing (first boot); after that +# the PVC copy is the source of truth, so edits made through OpenClaw (Control +# UI, `openclaw config set`, doctor --fix) survive restarts. To reseed from +# here: delete ~/.openclaw/openclaw.json in the pod and restart. +config: + gateway: + mode: local + # Listen on the pod IP so the Service/Ingress can reach it (upstream + # default "loopback" only works with kubectl port-forward) + bind: lan + port: 18789 + auth: + mode: token + controlUi: + enabled: true + agents: + defaults: + workspace: ~/.openclaw/workspace + +# Seeded as ~/.openclaw/workspace/AGENTS.md when missing +agentsMd: | + # OpenClaw Assistant + + You are a helpful AI assistant running in Kubernetes. + +# Upstream images run as uid/gid 1000 (user "node"); fsGroup makes the PVC writable +securityContext: + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + +containerSecurityContext: + allowPrivilegeEscalation: false + # Upstream manifests use a read-only root; off here because agent exec tools, + # npm/plugin installs and Chromium write outside ~/.openclaw (only /tmp and + # the PVC are writable when enabled). + readOnlyRootFilesystem: false + capabilities: + drop: + - ALL + +resources: + requests: + cpu: "250m" + memory: "512Mi" + limits: + cpu: "2" + memory: "2Gi" + +# In-memory /dev/shm; Chromium (browser variant) crashes on the 64Mi default +shmSize: 512Mi + +# Persistent ~/.openclaw (config, credentials, agent state DBs, workspace) +persistence: + enabled: true + existingClaim: "" + storageClass: local-path + accessModes: + - ReadWriteOnce + size: 10Gi + +nodeSelector: {} +tolerations: [] +affinity: {} + +ingresses: {} + # https: + # host: openclaw.example.com + # annotations: {} + # tls: + # - hosts: + # - openclaw.example.com