๐Ÿฆž OpenClaw Helm Chart

February 5, 2026 ยท View on GitHub

Artifact Hub Helm 3 Kubernetes App Version Chart Version License: MIT

Helm chart for deploying OpenClaw on Kubernetes โ€” an AI assistant that connects to messaging platforms and executes tasks autonomously.

Built on bjw-s app-template. For a detailed walkthrough, see the blog post.


Architecture

OpenClaw runs as a single-instance deployment (cannot scale horizontally):

ComponentPortDescription
Gateway18789Main HTTP/WebSocket interface
Chromium9222Headless browser for automation (CDP, optional)

App Version: 2026.5.22


Installation

Prerequisites

  • Kubernetes >=1.26.0-0
  • Helm 3.0+
  • API key from a supported LLM provider (Anthropic, OpenAI, etc.)

Steps

  1. Add the repository:
helm repo add openclaw https://serhanekicii.github.io/openclaw-helm
helm repo update
  1. Create namespace and secret:
kubectl create namespace openclaw
kubectl create secret generic openclaw-env-secret -n openclaw \
  --from-literal=ANTHROPIC_API_KEY=sk-ant-xxx \
  --from-literal=OPENCLAW_GATEWAY_TOKEN=your-token
  1. Get default values:
helm show values openclaw/openclaw > values.yaml
  1. Reference your secret in values.yaml:
app-template:
  controllers:
    main:
      containers:
        main:
          envFrom:
            - secretRef:
                name: openclaw-env-secret
  1. Install:
helm install openclaw openclaw/openclaw -n openclaw -f values.yaml
  1. Pair your device:
# Access the web UI
kubectl port-forward -n openclaw svc/openclaw 18789:18789
# Open http://localhost:18789, enter your Gateway Token, click Connect

# Approve the pairing request
kubectl exec -n openclaw deployment/openclaw -c main -- node dist/index.js devices list
kubectl exec -n openclaw deployment/openclaw -c main -- node dist/index.js devices approve <REQUEST_ID>

Using a Fork or Local Image

If you maintain a fork of OpenClaw or build your own image, point to your container registry:

app-template:
  controllers:
    main:
      containers:
        main:
          image:
            repository: ghcr.io/your-org/openclaw-fork
            tag: "2026.5.22"

For images hosted in a private registry inside your cluster:

app-template:
  controllers:
    main:
      containers:
        main:
          image:
            repository: registry.internal/openclaw
            tag: "2026.5.22"
            pullPolicy: Always

Uninstall

helm uninstall openclaw -n openclaw
kubectl delete pvc -n openclaw -l app.kubernetes.io/name=openclaw  # optional: remove data

Configuration

All values are nested under app-template:. See values.yaml for full reference.

Values Table

Values

KeyTypeDefaultDescription
app-template.chromiumVersionstring"148.0.7778.97"Chromium sidecar image version
app-template.configMaps.config.data."openclaw.json"string"{\n // Gateway configuration\n \"gateway\": {\n \"port\": 18789,\n \"mode\": \"local\",\n // IMPORTANT: trustedProxies uses exact IP matching only\n // - CIDR notation is NOT supported - list each proxy IP individually\n // - IPv6 exact addresses may work but are untested\n // - Recommend single-stack IPv4 deployments for simplicity\n \"trustedProxies\": [\"10.0.0.1\"]\n },\n\n // Browser configuration (Chromium sidecar)\n \"browser\": {\n \"enabled\": true,\n \"defaultProfile\": \"default\",\n \"profiles\": {\n \"default\": {\n \"cdpUrl\": \"http://localhost:9222\",\n \"color\": \"#4285F4\"\n }\n }\n },\n\n // Agent configuration\n \"agents\": {\n \"defaults\": {\n \"workspace\": \"/home/node/.openclaw/workspace\",\n \"model\": {\n // Uses ANTHROPIC_API_KEY from environment\n \"primary\": \"anthropic/claude-opus-4-6\"\n },\n \"userTimezone\": \"UTC\",\n \"timeoutSeconds\": 600,\n \"maxConcurrent\": 1\n },\n \"list\": [\n {\n \"id\": \"main\",\n \"default\": true,\n \"identity\": {\n \"name\": \"OpenClaw\",\n \"emoji\": \"๐Ÿฆž\"\n }\n }\n ]\n },\n\n // Session management\n \"session\": {\n \"scope\": \"per-sender\",\n \"store\": \"/home/node/.openclaw/sessions\",\n \"reset\": {\n \"mode\": \"idle\",\n \"idleMinutes\": 60\n }\n },\n\n // Logging\n \"logging\": {\n \"level\": \"info\",\n \"consoleLevel\": \"info\",\n \"consoleStyle\": \"compact\",\n \"redactSensitive\": \"tools\"\n },\n\n // Tools configuration\n \"tools\": {\n \"profile\": \"full\",\n \"web\": {\n \"search\": {\n \"enabled\": false\n },\n \"fetch\": {\n \"enabled\": true\n }\n }\n }\n\n // Channel configuration can be added here:\n // \"channels\": {\n // \"telegram\": {\n // \"botToken\": \"${TELEGRAM_BOT_TOKEN}\",\n // \"enabled\": true\n // },\n // \"discord\": {\n // \"token\": \"${DISCORD_BOT_TOKEN}\"\n // },\n // \"slack\": {\n // \"botToken\": \"${SLACK_BOT_TOKEN}\",\n // \"appToken\": \"${SLACK_APP_TOKEN}\"\n // }\n // }\n}\n"
app-template.configMaps.config.data.bash_aliasesstring"alias openclaw='node /app/dist/index.js'\n"
app-template.configMaps.config.enabledbooltrue
app-template.configMaps.scripts.data."init-config.sh"string`"log() { echo "[(dateโˆ’Iseconds)][initโˆ’config](date -Iseconds)] [init-config] *"; }\n\nlog "Starting config initialization"\nmkdir -p /home/node/.openclaw\nCONFIG_MODE="{CONFIG_MODE:-merge}\"\n\nif [ \"CONFIG_MODE" = "merge" ] && [ -f /home/node/.openclaw/openclaw.json ]; then\n log "Mode: merge - merging Helm config with existing config"\n if node -e "\n const fs = require('fs');\n // Strip JSON5 single-line comments while preserving // inside strings (e.g. URLs)\n const stripComments = (s) => {\n let r = '', q = false, i = 0;\n while (i < s.length) {\n if (q) {\n if (s[i] === '\\\\') { r += s[i] + s[i+1]; i += 2; continue; }\n if (s[i] === '\"') q = false;\n r += s[i++];\n } else if (s[i] === '\"') {\n q = true; r += s[i++];\n } else if (s[i] === '/' && s[i+1] === '/') {\n while (i < s.length && s[i] !== '\n') i++;\n } else { r += s[i++]; }\n }\n return r;\n };\n let existing;\n try {\n existing = JSON.parse(stripComments(fs.readFileSync('/home/node/.openclaw/openclaw.json', 'utf8')));\n } catch (e) {\n console.error('[init-config] Warning: existing config is not valid JSON, will overwrite');\n process.exit(1);\n }\n const helm = JSON.parse(stripComments(fs.readFileSync('/config/openclaw.json', 'utf8')));\n const deepMerge = (target, source) => {\n for (const key of Object.keys(source)) {\n if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {\n target[key] = target[key]
app-template.configMaps.scripts.data."init-skills.sh"string`"log() { echo "[(dateโˆ’Iseconds)][initโˆ’skills](date -Iseconds)] [init-skills] *"; }\n\nlog "Starting skills initialization"\n\n# ============================================================\n# Runtime Dependencies\n# ============================================================\n# Some skills require additional runtimes (Python, Go, etc.)\n# Install them here so they persist across pod restarts.\n#\n# Example: Install uv (Python package manager) for Python skills\n# mkdir -p /home/node/.openclaw/bin\n# if [ ! -f /home/node/.openclaw/bin/uv ]; then\n# log "Installing uv..."\n# curl -LsSf https://astral.sh/uv/install.shenv UV_INSTALL_DIR=/home/node/.openclaw/bin sh\n# fi\n#\n# Example: Install pnpm and packages for interfaces (e.g., MS Teams)\n# The read-only filesystem and non-root UID prevent writing to default\n# pnpm paths (/usr/local/lib/node_modules, ~/.local/share/pnpm, etc.).\n# Redirect PNPM_HOME to the PVC so the binary persists across restarts.\n# The init container's HOME=/tmp ensures pnpm's cache, state, and config\n# writes land on /tmp (writable emptyDir). The store goes on the PVC so\n# hardlinks work (same filesystem as node_modules) and persist.\n# PNPM_HOME=/home/node/.openclaw/pnpm\n# mkdir -p "PNPM_HOME\"\n# if [ ! -f \"PNPM_HOME/pnpm" ]; then\n# log "Installing pnpm..."\n# curl -fsSL https://get.pnpm.io/install.sh
app-template.configMaps.scripts.enabledbooltrue
app-template.configModestring"merge"Config mode: merge preserves runtime changes, overwrite for strict GitOps
app-template.controllers.main.containers.chromiumobject{"enabled":true,"env":{"XDG_CACHE_HOME":"/tmp"},"image":{"repository":"chromedp/headless-shell","tag":"{{ .Values.chromiumVersion }}"},"probes":{"liveness":{"custom":true,"enabled":true,"spec":{"failureThreshold":6,"httpGet":{"path":"/json/version","port":9222},"initialDelaySeconds":10,"periodSeconds":30,"timeoutSeconds":5}},"readiness":{"custom":true,"enabled":true,"spec":{"httpGet":{"path":"/json/version","port":9222},"initialDelaySeconds":5,"periodSeconds":10}},"startup":{"custom":true,"enabled":true,"spec":{"failureThreshold":12,"httpGet":{"path":"/json/version","port":9222},"initialDelaySeconds":5,"periodSeconds":5,"timeoutSeconds":5}}},"resources":{"limits":{"cpu":"1000m","memory":"1Gi"},"requests":{"cpu":"100m","memory":"256Mi"}},"securityContext":{"allowPrivilegeEscalation":false,"capabilities":{"drop":["ALL"]},"readOnlyRootFilesystem":true,"runAsGroup":1000,"runAsNonRoot":true,"runAsUser":1000}}Chromium sidecar for browser automation (CDP on port 9222)
app-template.controllers.main.containers.chromium.enabledbooltrueEnable/disable the Chromium browser sidecar
app-template.controllers.main.containers.chromium.image.repositorystring"chromedp/headless-shell"Chromium image repository
app-template.controllers.main.containers.chromium.image.tagstring"{{ .Values.chromiumVersion }}"Chromium image tag
app-template.controllers.main.containers.mainobject{"args":["gateway","--bind","lan","--port","18789"],"command":["node","dist/index.js"],"env":{},"envFrom":[],"image":{"pullPolicy":"IfNotPresent","repository":"ghcr.io/openclaw/openclaw","tag":"{{ .Values.openclawVersion }}"},"probes":{"liveness":{"enabled":true,"spec":{"failureThreshold":3,"initialDelaySeconds":30,"periodSeconds":30,"tcpSocket":{"port":18789},"timeoutSeconds":5},"type":"TCP"},"readiness":{"enabled":true,"spec":{"failureThreshold":3,"initialDelaySeconds":10,"periodSeconds":10,"tcpSocket":{"port":18789},"timeoutSeconds":5},"type":"TCP"},"startup":{"enabled":true,"spec":{"failureThreshold":30,"initialDelaySeconds":5,"periodSeconds":5,"tcpSocket":{"port":18789},"timeoutSeconds":5},"type":"TCP"}},"resources":{"limits":{"cpu":"2000m","memory":"2Gi"},"requests":{"cpu":"200m","memory":"512Mi"}},"securityContext":{"allowPrivilegeEscalation":false,"capabilities":{"drop":["ALL"]},"readOnlyRootFilesystem":true,"runAsGroup":1000,"runAsNonRoot":true,"runAsUser":1000}}Main OpenClaw container
app-template.controllers.main.containers.main.image.pullPolicystring"IfNotPresent"Image pull policy
app-template.controllers.main.containers.main.image.repositorystring"ghcr.io/openclaw/openclaw"Container image repository
app-template.controllers.main.containers.main.image.tagstring"{{ .Values.openclawVersion }}"Container image tag
app-template.controllers.main.containers.main.resourcesobject{"limits":{"cpu":"2000m","memory":"2Gi"},"requests":{"cpu":"200m","memory":"512Mi"}}Resource requests and limits
app-template.controllers.main.initContainers.init-config.commandlist["sh","/scripts/init-config.sh"]Init-config startup script
app-template.controllers.main.initContainers.init-config.env.CONFIG_MODEstring`"{{ .Values.configModedefault "merge" }}"`
app-template.controllers.main.initContainers.init-config.image.repositorystring"ghcr.io/openclaw/openclaw"
app-template.controllers.main.initContainers.init-config.image.tagstring"{{ .Values.openclawVersion }}"
app-template.controllers.main.initContainers.init-config.securityContext.allowPrivilegeEscalationboolfalse
app-template.controllers.main.initContainers.init-config.securityContext.capabilities.drop[0]string"ALL"
app-template.controllers.main.initContainers.init-config.securityContext.readOnlyRootFilesystembooltrue
app-template.controllers.main.initContainers.init-config.securityContext.runAsGroupint1000
app-template.controllers.main.initContainers.init-config.securityContext.runAsNonRootbooltrue
app-template.controllers.main.initContainers.init-config.securityContext.runAsUserint1000
app-template.controllers.main.initContainers.init-skills.commandlist["sh","/scripts/init-skills.sh"]Init-skills startup script
app-template.controllers.main.initContainers.init-skills.env.HOMEstring"/tmp"
app-template.controllers.main.initContainers.init-skills.env.NPM_CONFIG_CACHEstring"/tmp/.npm"
app-template.controllers.main.initContainers.init-skills.env.SKILLS_TO_INSTALLstring`"{{ .Values.skillsjoin " " }}"`
app-template.controllers.main.initContainers.init-skills.image.repositorystring"ghcr.io/openclaw/openclaw"
app-template.controllers.main.initContainers.init-skills.image.tagstring"{{ .Values.openclawVersion }}"
app-template.controllers.main.initContainers.init-skills.securityContext.allowPrivilegeEscalationboolfalse
app-template.controllers.main.initContainers.init-skills.securityContext.capabilities.drop[0]string"ALL"
app-template.controllers.main.initContainers.init-skills.securityContext.readOnlyRootFilesystembooltrue
app-template.controllers.main.initContainers.init-skills.securityContext.runAsGroupint1000
app-template.controllers.main.initContainers.init-skills.securityContext.runAsNonRootbooltrue
app-template.controllers.main.initContainers.init-skills.securityContext.runAsUserint1000
app-template.controllers.main.replicasint1Number of replicas (must be 1, OpenClaw doesn't support horizontal scaling)
app-template.controllers.main.strategystring"Recreate"Deployment strategy
app-template.defaultPodOptions.securityContextobject{"fsGroup":1000,"fsGroupChangePolicy":"OnRootMismatch"}Pod security context
app-template.ingress.main.enabledboolfalseEnable ingress resource creation
app-template.networkpolicies.main.controllerstring"main"
app-template.networkpolicies.main.enabledboolfalseEnable network policy (default deny-all with explicit allow rules)
app-template.networkpolicies.main.policyTypes[0]string"Ingress"
app-template.networkpolicies.main.policyTypes[1]string"Egress"
app-template.networkpolicies.main.rules.egress[0].ports[0].portint53
app-template.networkpolicies.main.rules.egress[0].ports[0].protocolstring"UDP"
app-template.networkpolicies.main.rules.egress[0].ports[1].portint53
app-template.networkpolicies.main.rules.egress[0].ports[1].protocolstring"TCP"
app-template.networkpolicies.main.rules.egress[0].to[0].namespaceSelector.matchLabels."kubernetes.io/metadata.name"string"kube-system"
app-template.networkpolicies.main.rules.egress[0].to[0].podSelector.matchLabels.k8s-appstring"kube-dns"
app-template.networkpolicies.main.rules.egress[1].to[0].ipBlock.cidrstring"0.0.0.0/0"
app-template.networkpolicies.main.rules.egress[1].to[0].ipBlock.except[0]string"10.0.0.0/8"
app-template.networkpolicies.main.rules.egress[1].to[0].ipBlock.except[1]string"172.16.0.0/12"
app-template.networkpolicies.main.rules.egress[1].to[0].ipBlock.except[2]string"192.168.0.0/16"
app-template.networkpolicies.main.rules.egress[1].to[0].ipBlock.except[3]string"169.254.0.0/16"
app-template.networkpolicies.main.rules.egress[1].to[0].ipBlock.except[4]string"100.64.0.0/10"
app-template.networkpolicies.main.rules.ingress[0].from[0].namespaceSelector.matchLabels."kubernetes.io/metadata.name"string"gateway-system"
app-template.networkpolicies.main.rules.ingress[0].ports[0].portint18789
app-template.networkpolicies.main.rules.ingress[0].ports[0].protocolstring"TCP"
app-template.openclawVersionstring"2026.5.22"OpenClaw image version (used by all OpenClaw containers)
app-template.persistence.bash-aliases.advancedMounts.main.main[0].pathstring"/home/node/.bash_aliases"
app-template.persistence.bash-aliases.advancedMounts.main.main[0].readOnlybooltrue
app-template.persistence.bash-aliases.advancedMounts.main.main[0].subPathstring"bash_aliases"
app-template.persistence.bash-aliases.enabledbooltrue
app-template.persistence.bash-aliases.identifierstring"config"
app-template.persistence.bash-aliases.typestring"configMap"
app-template.persistence.config.advancedMounts.main.init-config[0].pathstring"/config"
app-template.persistence.config.advancedMounts.main.init-config[0].readOnlybooltrue
app-template.persistence.config.enabledbooltrue
app-template.persistence.config.identifierstring"config"
app-template.persistence.config.typestring"configMap"
app-template.persistence.data.accessModestring"ReadWriteOnce"PVC access mode
app-template.persistence.data.advancedMounts.main.init-config[0].pathstring"/home/node/.openclaw"
app-template.persistence.data.advancedMounts.main.init-skills[0].pathstring"/home/node/.openclaw"
app-template.persistence.data.advancedMounts.main.main[0].pathstring"/home/node/.openclaw"
app-template.persistence.data.enabledbooltrue
app-template.persistence.data.sizestring"5Gi"PVC storage size
app-template.persistence.data.typestring"persistentVolumeClaim"
app-template.persistence.scripts.advancedMounts.main.init-config[0].pathstring"/scripts"
app-template.persistence.scripts.advancedMounts.main.init-config[0].readOnlybooltrue
app-template.persistence.scripts.advancedMounts.main.init-skills[0].pathstring"/scripts"
app-template.persistence.scripts.advancedMounts.main.init-skills[0].readOnlybooltrue
app-template.persistence.scripts.enabledbooltrue
app-template.persistence.scripts.identifierstring"scripts"
app-template.persistence.scripts.typestring"configMap"
app-template.persistence.tmp.advancedMounts.main.chromium[0].pathstring"/tmp"
app-template.persistence.tmp.advancedMounts.main.init-config[0].pathstring"/tmp"
app-template.persistence.tmp.advancedMounts.main.init-skills[0].pathstring"/tmp"
app-template.persistence.tmp.advancedMounts.main.main[0].pathstring"/tmp"
app-template.persistence.tmp.enabledbooltrue
app-template.persistence.tmp.typestring"emptyDir"
app-template.service.main.controllerstring"main"
app-template.service.main.ipFamilies[0]string"IPv4"
app-template.service.main.ipFamilyPolicystring"SingleStack"IPv4-only (see trustedProxies note in gateway config)
app-template.service.main.ports.http.portint18789Gateway service port
app-template.skillslist["weather","gog"]ClawHub skills to install on startup. Set to [] to skip skill installation.

Config Mode

The configMode setting controls how Helm-managed config merges with runtime changes:

ModeBehavior
merge (default)Helm values are deep-merged with existing config. Runtime changes (e.g., paired devices, UI settings) are preserved.
overwriteHelm values completely replace existing config. Use for strict GitOps where config should match values.yaml exactly.
app-template:
  configMode: overwrite  # or "merge" (default)
ArgoCD with Config Merge

When using configMode: merge with ArgoCD, prevent ArgoCD from overwriting runtime config changes by ignoring the ConfigMap:

# Application manifest
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: openclaw
spec:
  ignoreDifferences:
    - group: ""
      kind: ConfigMap
      name: openclaw
      jsonPointers:
        - /data

This allows:

  • ArgoCD manages deployments, services, etc.
  • Runtime config changes (paired devices, UI settings) persist on PVC
  • Helm values still merge on pod restart

Security

The chart follows security best practices:

  • All containers run as non-root (UID 1000)
  • Read-only root filesystem on all containers
  • All capabilities dropped
  • Privilege escalation disabled
  • Network policies available for workload isolation

Important: OpenClaw has shell access and processes untrusted input. Use network policies and limit exposure. See the OpenClaw Security Guide for best practices.

Network Policy

Network policies isolate OpenClaw from internal cluster services, limiting blast radius if compromised:

app-template:
  networkpolicies:
    main:
      enabled: true

Default policy allows:

  • Ingress from gateway-system namespace on port 18789
  • Egress to kube-dns
  • Egress to public internet (blocks private/reserved ranges)

Requires a CNI with NetworkPolicy support (Calico, Cilium).

Allowing Internal Services

To allow OpenClaw to reach internal services (e.g., Vault, Ollama), add egress rules:

app-template:
  networkpolicies:
    main:
      enabled: true
      rules:
        egress:
          # DNS (required)
          - to:
              - namespaceSelector:
                  matchLabels:
                    kubernetes.io/metadata.name: kube-system
                podSelector:
                  matchLabels:
                    k8s-app: kube-dns
            ports:
              - protocol: UDP
                port: 53
          # Public internet (blocks RFC1918)
          - to:
              - ipBlock:
                  cidr: 0.0.0.0/0
                  except:
                    - 10.0.0.0/8
                    - 172.16.0.0/12
                    - 192.168.0.0/16
          # Vault
          - to:
              - namespaceSelector:
                  matchLabels:
                    kubernetes.io/metadata.name: vault
            ports:
              - protocol: TCP
                port: 8200
          # Ollama
          - to:
              - namespaceSelector:
                  matchLabels:
                    kubernetes.io/metadata.name: ollama
            ports:
              - protocol: TCP
                port: 11434

Browser Automation

Chromium sidecar provides headless browser via CDP on port 9222.

To disable:

app-template:
  controllers:
    main:
      containers:
        chromium:
          enabled: false

Skills

The init-skills container provides declarative skill management from ClawHub:

app-template:
  controllers:
    main:
      initContainers:
        init-skills:
          command:
            - sh
            - -c
            - |
              cd /home/node/.openclaw/workspace && mkdir -p skills
              for skill in weather; do
                if ! npx -y clawhub install "$skill" --no-input; then
                  echo "WARNING: Failed to install skill: $skill"
                fi
              done

Runtime Dependencies

Some features (interfaces, skills) require additional runtimes or packages not included in the base image. The init-skills init container handles this -- install extra tooling to the PVC at /home/node/.openclaw so it persists across pod restarts and is available at runtime.

This approach is necessary because all containers run with a read-only root filesystem as non-root (UID 1000). Default package manager paths (e.g., /usr/local/lib/node_modules) are not writable. Redirecting install paths to the PVC solves this.

pnpm (e.g., MS Teams interface)

Interfaces like MS Teams require pnpm packages. The read-only root filesystem prevents writing to default pnpm paths (/usr/local/lib/node_modules, ~/.local/share/pnpm, etc.). The fix is to install pnpm to the PVC and redirect its directories to writable mounts.

The init-skills container already sets HOME=/tmp, so pnpm's cache, state, and config writes land on /tmp (writable emptyDir). The content-addressable store goes on the PVC so that hardlinks work (same filesystem as node_modules) and persist across restarts.

1. Install pnpm and packages in init-skills:

app-template:
  controllers:
    main:
      initContainers:
        init-skills:
          command:
            - sh
            - -c
            - |
              PNPM_HOME=/home/node/.openclaw/pnpm
              mkdir -p "$PNPM_HOME"
              if [ ! -f "$PNPM_HOME/pnpm" ]; then
                echo "Installing pnpm..."
                curl -fsSL https://get.pnpm.io/install.sh | env PNPM_HOME="$PNPM_HOME" SHELL=/bin/sh sh -
              fi
              export PATH="$PNPM_HOME:$PATH"
              echo "Installing interface dependencies..."
              cd /home/node/.openclaw
              pnpm install <your-package> --store-dir /home/node/.openclaw/.pnpm-store

2. Expose pnpm to the main container:

app-template:
  controllers:
    main:
      containers:
        main:
          env:
            PATH: /home/node/.openclaw/pnpm:/home/node/.openclaw/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
            PNPM_HOME: /home/node/.openclaw/pnpm
            PNPM_STORE_DIR: /home/node/.openclaw/.pnpm-store
uv (Python package manager)

For skills that require Python:

1. Install uv in init-skills:

app-template:
  controllers:
    main:
      initContainers:
        init-skills:
          command:
            - sh
            - -c
            - |
              mkdir -p /home/node/.openclaw/bin
              if [ ! -f /home/node/.openclaw/bin/uv ]; then
                echo "Installing uv..."
                curl -LsSf https://astral.sh/uv/install.sh | env UV_INSTALL_DIR=/home/node/.openclaw/bin sh
              fi

2. Add to PATH in main container:

app-template:
  controllers:
    main:
      containers:
        main:
          env:
            PATH: /home/node/.openclaw/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

Automatic Rollouts on ConfigMap/Secret Changes

For automatic pod restarts when ConfigMap/Secret changes, use Stakater Reloader or ArgoCD. See the blog post for detailed setup.

app-template:
  defaultPodOptions:
    annotations:
      reloader.stakater.com/auto: "true"

Persistence

Persistent storage is enabled by default (5Gi).

To disable (data lost on restart):

app-template:
  persistence:
    data:
      enabled: false
Ingress
app-template:
  ingress:
    main:
      enabled: true
      className: your-ingress-class
      hosts:
        - host: openclaw.example.com
          paths:
            - path: /
              pathType: Prefix
              service:
                identifier: main
                port: http
      tls:
        - secretName: openclaw-tls
          hosts:
            - openclaw.example.com
Internal CA Trust

For HTTPS to internal services with private CAs:

app-template:
  persistence:
    ca-bundle:
      enabled: true
      type: configMap
      name: ca-bundle
      advancedMounts:
        main:
          main:
            - path: /etc/ssl/certs/ca-bundle.crt
              subPath: ca-bundle.crt
              readOnly: true
  controllers:
    main:
      containers:
        main:
          env:
            REQUESTS_CA_BUNDLE: /etc/ssl/certs/ca-bundle.crt
Resource Limits

Default resources for main container:

app-template:
  controllers:
    main:
      containers:
        main:
          resources:
            requests:
              cpu: 200m
              memory: 512Mi
            limits:
              cpu: 2000m
              memory: 2Gi

Troubleshooting

Debug Commands
# Pod status
kubectl get pods -n openclaw

# Logs
kubectl logs -n openclaw deployment/openclaw

# Port forward
kubectl port-forward -n openclaw svc/openclaw 18789:18789

Development

helm lint charts/openclaw
helm dependency update charts/openclaw
helm template test charts/openclaw --debug

Dependencies

RepositoryNameVersion
https://bjw-s-labs.github.io/helm-charts/app-template4.6.2

License

MIT