apiVersion: apps/v1 kind: Deployment metadata: name: hermes namespace: platform-engineer 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: serviceAccountName: platform-engineer # No imagePullSecrets — using the public stock Hermes image from Docker Hub. # 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: # Download kubectl + helm into a shared emptyDir so the stock Hermes image # (which doesn't ship kubectl) can still drive the cluster. Avoids building # and pushing a custom image through a slow / size-capped registry. - name: install-tools image: curlimages/curl:8.12.1 command: ["sh", "-c"] args: - | set -e echo "Downloading kubectl v1.35.0..." curl -fsSL -o /tools/kubectl \ https://dl.k8s.io/release/v1.35.0/bin/linux/amd64/kubectl chmod +x /tools/kubectl echo "Downloading helm v3.16.3..." curl -fsSL https://get.helm.sh/helm-v3.16.3-linux-amd64.tar.gz \ | tar -xz -C /tools --strip-components=1 linux-amd64/helm chmod +x /tools/helm echo "Tools installed:"; ls -la /tools volumeMounts: - name: tools mountPath: /tools # Seed /opt/data with config.yaml + SOUL.md 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..." cp /seed/config.yaml /opt/data/config.yaml cp /seed/SOUL.md /opt/data/SOUL.md chmod 600 /opt/data/config.yaml else echo "/opt/data already initialized — leaving runtime state intact." fi mkdir -p /opt/data/home/.kube /opt/data/cron/output /opt/data/scripts /workspace 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. The image's CMD # (main-wrapper.sh) already routes `gateway run` through s6. args: ["gateway", "run"] ports: - name: gateway containerPort: 8642 - name: dashboard containerPort: 9119 envFrom: - secretRef: name: hermes-env env: # k3s injects KUBERNETES_SERVICE_HOST/PORT + the SA token automatically; # kubectl inside the pod authenticates as the platform-engineer SA. - name: HERMES_HOME value: /opt/data # Put the initContainer-installed kubectl/helm on PATH for the hermes user. - name: PATH value: /opt/hermes/bin:/opt/hermes/.venv/bin:/tools:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin volumeMounts: - name: data mountPath: /opt/data - name: workspace mountPath: /workspace - name: tools mountPath: /tools readOnly: true resources: requests: memory: "512Mi" cpu: "250m" limits: memory: "2Gi" cpu: "1000m" livenessProbe: httpGet: path: /health port: 8642 initialDelaySeconds: 60 periodSeconds: 30 failureThreshold: 3 securityContext: allowPrivilegeEscalation: false volumes: - name: data persistentVolumeClaim: claimName: hermes-data - name: workspace emptyDir: {} - name: tools emptyDir: {} - name: seed configMap: name: hermes-seed --- apiVersion: v1 kind: Service metadata: name: hermes namespace: platform-engineer spec: type: ClusterIP selector: app: hermes ports: - name: gateway port: 80 targetPort: 8642 - name: dashboard port: 9119 targetPort: 9119