apiVersion: apps/v1 kind: Deployment metadata: name: hermes namespace: home-manager labels: app: hermes spec: replicas: 1 # MUST be 1 — Hermes' /opt/data is single-writer. strategy: type: Recreate # never run two pods against the same PVC selector: matchLabels: app: hermes template: metadata: labels: app: hermes spec: # No serviceAccountName — the agent has NO k8s API access. It manages the # home via the Home Assistant REST API and notifies via Discord. # Pin to the powerful amd64 node (image is linux/amd64; the NUC has 24 GiB). nodeSelector: kubernetes.io/arch: amd64 affinity: nodeAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 preference: matchExpressions: - key: hardware operator: In values: ["high-memory"] podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchLabels: app: hermes topologyKey: kubernetes.io/hostname initContainers: # Seed /opt/data with config.yaml + SOUL.md + .env on first boot only. # ArgoCD owns the manifests; the PVC is runtime state and is NOT reconciled. - name: seed-data image: busybox:1.36 command: ["sh", "-c"] args: - | set -e if [ ! -f /opt/data/config.yaml ]; then echo "First boot: seeding /opt/data from ConfigMap + env..." cp /seed/config.yaml /opt/data/config.yaml cp /seed/SOUL.md /opt/data/SOUL.md chmod 600 /opt/data/config.yaml # Write .env from the injected Secret env vars so the s6 gateway # finds API keys (the hermes container reads keys from /opt/data/.env). : > /opt/data/.env chmod 600 /opt/data/.env for k in OPENAI_API_KEY OPENAI_BASE_URL HOMEASSISTANT_TOKEN \ DISCORD_BOT_TOKEN DISCORD_HOME_CHANNEL \ GATEWAY_ALLOW_ALL_USERS DISCORD_FREE_RESPONSE_CHANNELS \ HERMES_DASHBOARD HERMES_DASHBOARD_BASIC_AUTH_USERNAME \ HERMES_DASHBOARD_BASIC_AUTH_PASSWORD HERMES_DASHBOARD_BASIC_AUTH_SECRET \ API_SERVER_KEY; do eval "v=\${$k:-}" [ -n "$v" ] && echo "$k=$v" >> /opt/data/.env done else echo "/opt/data already initialized — leaving runtime state intact." fi mkdir -p /opt/data/home/.kube /opt/data/cron/output /opt/data/scripts envFrom: - secretRef: name: hermes-env volumeMounts: - name: data mountPath: /opt/data - name: seed mountPath: /seed containers: - name: hermes image: nousresearch/hermes-agent:latest imagePullPolicy: Always # IMPORTANT: do NOT set `command:` — it would override the image's # ENTRYPOINT (/init, s6-overlay), which sets up the hermes user, seeds # config on first boot, and supervises the gateway. args: ["gateway", "run"] ports: - name: gateway containerPort: 8642 - name: dashboard containerPort: 9119 envFrom: - secretRef: name: hermes-env env: - name: HERMES_HOME value: /opt/data # Home Assistant REST API base URL (in-cluster service). Non-secret, so # it lives here rather than in the Secret. The SOUL.md uses $HOMEASSISTANT_URL. - name: HOMEASSISTANT_URL value: "http://home-assistant.home-assistant:80" # Expose the Hermes gateway HTTP API on port 8642 (the Service routes # 80 → 8642). Used by Open WebUI and other in-cluster clients to talk # to the agent as an OpenAI-compatible endpoint. - name: API_SERVER_ENABLED value: "true" # Bind the gateway API on all interfaces so other pods can reach it # via the Service (default is 127.0.0.1/loopback, which is unreachable # cross-pod). Auth is still enforced via API_SERVER_KEY. - name: API_SERVER_HOST value: "0.0.0.0" # Distinct model id advertised on GET /v1/models. Without this both # agents report "hermes-agent" and Open WebUI dedupes them, hiding one. - name: API_SERVER_MODEL_NAME value: "home-manager" # NOTE: API_SERVER_KEY comes from the hermes-env Secret (via envFrom) # and is also seeded into /opt/data/.env by the init container. Clients # (e.g. Open WebUI) authenticate with `Authorization: Bearer `. # HERMES_WRITE_SAFE_ROOT is intentionally unset → defaults to HERMES_HOME # (/opt/data). The home agent has no git workspace to write to, so the # tighter default is correct (memory/skills/scripts live under /opt/data). volumeMounts: - name: data mountPath: /opt/data resources: requests: memory: "512Mi" cpu: "250m" limits: memory: "2Gi" cpu: "1000m" livenessProbe: # Probe the dashboard port (9119, always enabled via HERMES_DASHBOARD=1 # and binds 0.0.0.0). The gateway API on 8642 is enabled via # API_SERVER_ENABLED=true above. tcpSocket: port: 9119 initialDelaySeconds: 90 periodSeconds: 30 timeoutSeconds: 5 failureThreshold: 5 securityContext: allowPrivilegeEscalation: false volumes: - name: data persistentVolumeClaim: claimName: hermes-data - name: seed configMap: name: hermes-seed --- apiVersion: v1 kind: Service metadata: name: home-manager namespace: home-manager spec: type: ClusterIP selector: app: hermes ports: - name: gateway port: 80 targetPort: 8642 - name: dashboard port: 9119 targetPort: 9119