Files
openclaw/scripts/k8s/manifests/deployment.yaml
T
Peter Steinberger 2c3e537cb8 fix(deploy): repair probes that silently pass and a blueprint that cannot boot (#122963)
Three defects proven by running the shipped configs:

Kubernetes probes checked only the status code against /startupz, but the
pinned image predates that route and the Control UI answers unknown paths
with a catch-all 200. A wedged pod was therefore marked Ready forever.
Probes now assert the JSON probe contract and target routes the pinned
image actually serves; verified in a kind cluster where the old command
exits 0 on the missing route and the new one exits 1.

render.yaml set no dockerCommand, so the image CMD ran without
--allow-unconfigured and a fresh Render disk exited 78 with 'Missing
config' before binding. Reproduced locally with Render's exact env.

The Cloudflare Container readiness poll had the same route mismatch
against operator-supplied official image digests; it now polls /healthz,
which every published image serves.

Also replaces an R2 verification step that could never fail: wrangler
cannot list object keys, so the documented command 404'd into || true.
2026-08-12 20:49:54 -07:00

167 lines
6.0 KiB
YAML

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
- |
# Seed-if-missing: the PVC copy owns config after first boot so edits made
# through OpenClaw (onboard, channels add, doctor --fix, Control UI) survive
# restarts. ConfigMap edits need an explicit reseed (see docs/install/kubernetes.md).
[ -f /home/node/.openclaw/openclaw.json ] || cp /config/openclaw.json /home/node/.openclaw/openclaw.json
mkdir -p /home/node/.openclaw/workspace
[ -f /home/node/.openclaw/workspace/AGENTS.md ] || 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
# Bump this immutable versioned tag when upgrading OpenClaw.
image: ghcr.io/openclaw/openclaw:2026.7.1-2-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"
# Probes assert the JSON probe contract, not just the status code. The
# Control UI serves a catch-all 200 for unknown paths, so a status-only
# check would pass forever against an image whose probe route does not
# exist yet. Failing closed makes that mismatch loud instead of silent.
# /startupz (startup admission, ignores channel health) needs an image
# built from the release that added it; switch the startup and
# readiness probes to it after pinning such an image.
startupProbe:
exec:
command:
- node
- -e
- "require('http').get('http://127.0.0.1:18789/readyz', r => { let b = ''; r.on('data', d => b += d); r.on('end', () => { try { process.exit(r.statusCode < 400 && JSON.parse(b).ready === true ? 0 : 1); } catch { process.exit(1); } }); }).on('error', () => process.exit(1))"
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 30
livenessProbe:
exec:
command:
- node
- -e
- "require('http').get('http://127.0.0.1:18789/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))"
initialDelaySeconds: 60
periodSeconds: 30
timeoutSeconds: 10
readinessProbe:
exec:
command:
- node
- -e
- "require('http').get('http://127.0.0.1:18789/readyz', r => { let b = ''; r.on('data', d => b += d); r.on('end', () => { try { process.exit(r.statusCode < 400 && JSON.parse(b).ready === true ? 0 : 1); } catch { process.exit(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: {}