- Enhanced Dockerfile with updated image labels and source. - Revised README to reflect changes in DSH Web architecture and deployment instructions. - Added Kubernetes sandbox template for DSH Web with security context and environment variables. - Updated .env.sample and CODEOWNERS for consistency. - Introduced multi-arch Docker build workflow for improved CI/CD.
deepseek-kubernetes-sandbox based on hermes-agents-as-a-service
Original: https://github.com/kubernetes-sigs/agent-sandbox/tree/main/examples/hermes-agents-as-a-service
After analyzing the hermes-agents-as-a-service example, its core pattern maps directly onto your dsh-web container. The example's key mechanisms are: SandboxTemplate defines a reusable Pod blueprint, SandboxWarmPool pre-warms instances, SandboxClaim lets each user claim a persistent Sandbox, and service: true gives each Sandbox a stable network identity for gateway routing. Your DSH container is a strong fit for this model — it needs a persistent DSH_HOME (like Hermes needs a persistent data directory) and exposes a web port. The main differences are how secrets are injected and how the port is surfaced to end users.
🔑 Mechanism Mapping: Hermes → DSH
| Hermes example mechanism | Purpose | Adaptation for DSH |
|---|---|---|
SandboxTemplate |
Reusable Pod blueprint | Change image to dsh-web:0.2.1-rc.2, expose port 3080 |
volumeClaimTemplates |
Auto-create a PVC per user, persisting state | Mount point becomes /home/node/.dsh (DSH's state directory) |
SandboxWarmPool |
Pre-warmed instances for sub-second provisioning | Keep as-is; size the pool for expected concurrent users |
SandboxClaim + warmPoolRef |
User "registration" claims a lightweight object, adopts a pooled instance | Keep exactly as-is; one Claim per DSH user |
service: true |
Creates a headless Service per Sandbox with a stable DNS name | Enable it so a front-end gateway or Ingress can reliably route to each user's DSH Web |
envVarsInjectionPolicy |
Controls whether a Claim may inject/override env vars | DSH needs DEEPSEEK_API_KEY injected, so either allow it or reference a Secret from the template |
operatingMode |
Cost switch: Running / Suspended (deletes Pod, keeps PVC + Service) |
Reuse directly — suspend idle DSH instances to save cost |
📦 Equivalent Kubernetes Manifests
The manifests below mirror the Hermes example's organization. Apply them in order, all in a dedicated namespace (e.g. dsh-demo).
1. Secret: DSH API key
apiVersion: v1
kind: Secret
metadata:
name: dsh-api-secret
namespace: dsh-demo
type: Opaque
stringData:
DEEPSEEK_API_KEY: "sk-..." # replace with a real key
2. SandboxTemplate: DSH Web blueprint
This is the core resource, corresponding to 10-sandboxtemplate.yaml in the example. Key settings: service: true for stable DNS, volumeClaimTemplates to persist /home/node/.dsh, and a security context that runs as the non-root node user (UID 1000).
apiVersion: extensions.agents.x-k8s.io/v1beta1
kind: SandboxTemplate
metadata:
name: dsh-web-template
namespace: dsh-demo
spec:
service: true # headless Service per Sandbox → stable DNS
podTemplate:
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
containers:
- name: dsh-web
image: dsh-web:0.2.1-rc.2 # replace with your registry path
ports:
- containerPort: 3080
name: web
env:
- name: DEEPSEEK_API_KEY
valueFrom:
secretKeyRef:
name: dsh-api-secret
key: DEEPSEEK_API_KEY
- name: DSH_PUBLIC_URL
value: "http://localhost:3080" # change to the external URL when behind a gateway/Ingress
volumeMounts:
- name: dsh-home
mountPath: /home/node/.dsh
readinessProbe:
httpGet:
path: /
port: 3080
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /
port: 3080
initialDelaySeconds: 15
periodSeconds: 20
# If your cluster uses gVisor / Kata, set the runtime class here
# runtimeClassName: gvisor
volumeClaimTemplates:
- metadata:
name: dsh-home
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 5Gi # adjust to your DSH state size
3. SandboxWarmPool: pre-warmed pool
Corresponds to 20-sandboxwarmpool.yaml. Pooled Sandboxes pull the image and boot ahead of time; a user Claim adopts one instantly.
apiVersion: extensions.agents.x-k8s.io/v1beta1
kind: SandboxWarmPool
metadata:
name: dsh-web-pool
namespace: dsh-demo
spec:
replicas: 2 # number of pre-warmed instances
sandboxTemplateRef:
name: dsh-web-template
4. SandboxClaim: per-user claim
Corresponds to 40-claim.yaml. Each user creates one lightweight Claim and receives a pooled instance with its own PVC and stable DNS.
apiVersion: extensions.agents.x-k8s.io/v1beta1
kind: SandboxClaim
metadata:
name: user-alice-dsh
namespace: dsh-demo
spec:
sandboxTemplateRef:
name: dsh-web-template
warmPoolRef:
name: dsh-web-pool
# If the template allows env injection (envVarsInjectionPolicy: Allowed),
# you can override/add variables here:
# env:
# - name: DSH_PUBLIC_URL
# value: "https://alice.dsh.example.com"
🌐 Networking: How Users Reach DSH Web
Unlike Hermes agents, which are mainly consumed by an internal gateway, DSH Web is a user-facing web UI — each Sandbox's 3080 port must be reachable by end users. Two patterns:
Pattern A: Direct via headless Service (simple)
With service: true, each Sandbox gets a headless Service whose DNS name is exposed in the Sandbox's status.serviceFQDN. You can reach it from inside the cluster via that FQDN, or add an Ingress/upper-layer gateway that routes a path to the corresponding Service.
Pattern B: Gateway-proxied (recommended, mirrors Hermes's service layer)
The Hermes example ships a ~250-line Python gateway (gateway/gateway.py) that handles per-user routing, auth, and auto-waking suspended Sandboxes. You can build a simpler equivalent for DSH:
- Accept external requests (e.g.
/u/{user}/**). - Look up the
status.serviceFQDNof that user's Sandbox. - Reverse-proxy to
http://{serviceFQDN}:3080. - If the Sandbox is
Suspended, flip itsoperatingModeback toRunning, wait forReady, then proxy.
If you don't want a gateway yet, you can manually create one Ingress per user pointing at their Service — less automated, but functional.
⚙️ Key Differences and Caveats
-
Secret-injection policy. The Hermes example sets
envVarsInjectionPolicy: Disallowedto strictly limit what Claims can inject. DSH needsDEEPSEEK_API_KEYinjected, so you have two choices:- Set it to
Allowed, then reference the Secret fromSandboxClaim.env(more flexible, but requires trusting Claim creators). - Keep it
Disallowedand reference the Secret directly inside theSandboxTemplate'spodTemplate(stricter, but all users share the same key — fine for a single-tenant internal setup). Pick based on your multi-tenancy model.
- Set it to
-
DSH_PUBLIC_URLconfiguration. Your entrypoint defaults tohttp://localhost:3080. When accessed via a headless Service or gateway, setDSH_PUBLIC_URLto the real external URL (e.g.https://alice.dsh.example.com), otherwise links DSH generates may be unreachable. You can override it inSandboxClaim.env(if injection is allowed) or rewrite URLs in the gateway. -
Image pull and platform. As with the Hermes example, push
dsh-webto a registry reachable by the cluster. Your image is built foramd64; node architecture must match. -
PVC size. DSH state size (sessions, configs, etc.) depends on usage; Hermes allocated a PVC for
/opt/data. Adjust thestoragerequest involumeClaimTemplatesaccordingly. -
Auto-suspend via
operatingMode. Hermes's gateway includes an idle sweeper that suspends idle instances automatically. If your DSH gateway implements a similar sweeper, you get the same cost model:Runningwhile active,Suspendedwhen idle (PVC + Service retained), auto-woken when the user returns.
✅ Deployment Order
kubectl create namespace dsh-demo
kubectl apply -f 00-secret.yaml
kubectl apply -f 10-sandboxtemplate.yaml
kubectl apply -f 20-sandboxwarmpool.yaml
kubectl apply -f 40-claim.yaml # one per user
After applying, watch the warm pool spin up with kubectl -n dsh-demo get sandboxes -w, and inspect adoption and status.serviceFQDN with kubectl -n dsh-demo get sandboxclaim user-alice-dsh -o yaml. At that point you have a platform structurally identical to the Hermes example, but running DSH Web for multiple users.