1
0
mirror of https://github.com/sredevopsorg/deepseek-kubernetes-sandbox.git synced 2026-11-03 01:13:24 +00:00

Update Dockerfile, README, and Kubernetes manifests for DSH Web deployment (#1)

- 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.
This commit is contained in:
Nicolás Georger
2026-10-21 03:46:48 -03:00
committed by GitHub
parent f3d4312089
commit 0d66cbaca1
6 changed files with 480 additions and 138 deletions

View File

@@ -1,3 +1,4 @@
DEEPSEEK_API_KEY= DEEPSEEK_API_KEY=
DSH_VERSION=dsh-v0.2.1-rc.2 DSH_VERSION=dsh-v0.2.1-rc.2
PNPM_VERSION=11.28.5 PNPM_VERSION=11.28.5
DSH_PORT=3080

1
.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1 @@
* @ngeorger

283
.github/workflows/multi-build.yaml vendored Normal file
View File

@@ -0,0 +1,283 @@
# This workflow builds a multi-arch Docker image using GitHub Actions and separated Github Runners with native support for ARM64 and AMD64 architectures, without using QEMU emulation.
# It uses Docker Buildx to build and push the image to GitHub Container Registry (GHCR).
name: Build multi arch Docker Image with separate Github Runners
on:
workflow_dispatch:
push:
branches:
- main
paths:
- 'Dockerfile'
- '.github/workflows/multi-build.yaml'
env:
# The name of the Docker image to be built and pushed to GHCR
# The image name is derived from the GitHub repository name and the GitHub Container Registry (GHCR) URL.
# The image name will be in the format: ghcr.io/<owner>/<repo>
GHCR_IMAGE: ghcr.io/${{ github.repository }}
DSH_VERSION: dsh-v0.2.1-rc.2 #TODO: Make this configurable from workflow inputs and/or an custom Github Action
permissions:
# Global permissions for the workflow, which can be overridden at the job level
contents: read
concurrency:
# This concurrency group ensures that only one job in the group runs at a time.
# If a new job is triggered, the previous one will be canceled.
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
# The build job builds the Docker image for each platform specified in the matrix.
build:
strategy:
fail-fast: false
matrix:
platform:
- linux/amd64
- linux/arm64
# The matrix includes two platforms: linux/amd64 and linux/arm64.
# The build job will run for each platform in the matrix.
permissions:
# Permissions for the build job, which can be overridden at the step level
# The permissions are set to allow the job to write to the GitHub Container Registry (GHCR) and read from the repository.
attestations: write
actions: read
checks: write
contents: write
deployments: none
id-token: write
issues: read
discussions: read
packages: write
pages: none
pull-requests: read
repository-projects: read
security-events: read
statuses: read
runs-on: ${{ matrix.platform == 'linux/amd64' && 'ubuntu-latest' || matrix.platform == 'linux/arm64' && 'ubuntu-24.04-arm' }}
# The job runs on different runners based on the platform.
# For linux/amd64, it runs on the latest Ubuntu runner.
# For linux/arm64, it runs on an Ubuntu 24.04 ARM runner.
# The runner is selected based on the platform specified in the matrix.
name: Build Docker image for ${{ matrix.platform }}
steps:
-
name: Prepare environment for current platform
# This step sets up the environment for the current platform being built.
# It replaces the '/' character in the platform name with '-' and sets it as an environment variable.
# This is useful for naming artifacts and other resources that cannot contain '/'.
# The environment variable PLATFORMS_PAIR will be used later in the workflow.
id: prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# This step checks out the code from the repository.
# It uses the actions/checkout action to clone the repository into the runner's workspace.
- name: Docker meta default
# This step generates metadata for the Docker image.
# It uses the docker/metadata-action to create metadata based on the repository information.
# The metadata includes information such as the image name, tags, and labels.
# The metadata will be used later in the workflow to build and push the Docker image.
id: meta
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
images: ${{ env.GHCR_IMAGE }}
- name: Set up Docker Context for Buildx
# This step sets up a Docker context for Buildx.
# It creates a new context named "builders" that will be used for building the Docker image.
# The context allows Buildx to use the Docker daemon for building images.
id: buildx-context
run: |
docker context create builders
- name: Set up Docker Buildx
# This step sets up Docker Buildx, which is a Docker CLI plugin for extended build capabilities with BuildKit.
# It uses the docker/setup-buildx-action to configure Buildx with the specified context and platforms.
# The platforms are specified in the matrix and will be used for building the Docker image.
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
with:
endpoint: builders
platforms: ${{ matrix.platform }}
- name: Login to GitHub Container Registry
# This step logs in to the GitHub Container Registry (GHCR) using the docker/login-action.
# It uses the GitHub actor's username and the GITHUB_TOKEN secret for authentication.
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push by digest
# This step builds and pushes the Docker image using Buildx.
# It uses the docker/build-push-action to build the image with the specified context and platforms.
# The image is built with the labels and annotations generated in the previous steps.
# The outputs are configured to push the image by digest, which allows for better caching and versioning.
# The cache-from and cache-to options are used to enable caching for the build process.
# The cache is stored in GitHub Actions cache and is scoped to the repository, branch, and platform.
id: build
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
env:
DOCKER_BUILDKIT: 1
DSH_VERSION: dsh-v0.2.1-rc.2 #TODO: Make this configurable from workflow inputs and/or an custom Github Action
PNPM_VERSION: 11.28.5
DSH_PORT: 3080
with:
context: .
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
build-args: DSH_VERSION=${{ env.DSH_VERSION }}
annotations: ${{ steps.meta.outputs.annotations }}
outputs: type=image,name=${{ env.GHCR_IMAGE }},push-by-digest=true,name-canonical=true,push=true,oci-mediatypes=true
cache-from: type=gha,scope=${{ github.repository }}-${{ github.ref_name }}-${{ matrix.platform }}
cache-to: type=gha,scope=${{ github.repository }}-${{ github.ref_name }}-${{ matrix.platform }}
- name: Export digest
# This step exports the digest of the built image to a file.
# It creates a directory in /tmp/digests and saves the digest of the image to a file.
# The digest is obtained from the output of the build step.
# The digest is used to uniquely identify the built image and can be used for further processing or verification.
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
# This step uploads the digest file to the GitHub Actions artifact storage.
# It uses the actions/upload-artifact action to upload the file created in the previous step.
# The artifact is named digests-${{ env.PLATFORM_PAIR }}, where PLATFORM_PAIR is the platform name with '/' replaced by '-'.
# The artifact is retained for 1 day, and if no files are found, it will throw an error.
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: digests-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
merge:
# This job merges the Docker manifests for the different platforms built in the previous job.
name: Merge Docker manifests
runs-on: ubuntu-latest
permissions:
attestations: write
actions: read
checks: read
contents: read
deployments: none
id-token: write
issues: read
discussions: read
packages: write
pages: none
pull-requests: read
repository-projects: read
security-events: read
statuses: read
needs:
- build
# This job depends on the build job to complete before it starts.
# It ensures that the Docker images for all platforms are built before merging the manifests.
steps:
- name: Download digests
# This step downloads the digest files uploaded in the build job.
# It uses the actions/download-artifact action to download the artifacts with the pattern digests-*.
# The downloaded files are merged into the /tmp/digests directory.
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true
- name: Docker meta
# This step generates metadata for the Docker image.
# It uses the docker/metadata-action to create metadata based on the repository information.
# The metadata includes information such as the image name, tags, and labels.
id: meta
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
env:
DSH_VERSION: ${{ env.DSH_VERSION }}
with:
images: ${{ env.GHCR_IMAGE }}
annotations: |
type=org.opencontainers.image.description,value=${{ github.event.repository.description || 'No description provided' }}
tags: |
type=raw,value=main,enable=${{ github.ref_name == 'main' }}
type=raw,value=latest,enable=${{ github.ref_name == 'main' }}
type=raw,value=${{ env.DSH_VERSION }},enable=${{ github.ref_name == 'main' }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
# This step sets up Docker Buildx, which is a Docker CLI plugin for extended build capabilities with BuildKit.
with:
driver-opts: |
network=host
- name: Login to GitHub Container Registry
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
# This step logs in to the GitHub Container Registry (GHCR) using the docker/login-action.
# It uses the GitHub actor's username and the GITHUB_TOKEN secret for authentication.
# The login is necessary to push the merged manifest list to GHCR.
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get execution timestamp with RFC3339 format
# This step gets the current execution timestamp in RFC3339 format.
# It uses the date command to get the current UTC time and formats it as a string.
# The timestamp is used for annotating the Docker manifest list.
id: timestamp
run: |
echo "timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" >> $GITHUB_OUTPUT
- name: Create manifest list and pushs
# This step creates a manifest list for the Docker images built for different platforms.
# It uses the docker buildx imagetools create command to create the manifest list.
# The manifest list is annotated with metadata such as description, creation timestamp, and source URL.
# The annotations are obtained from the metadata generated in the previous steps.
# The manifest list is pushed to the GitHub Container Registry (GHCR) with the specified tags.
working-directory: /tmp/digests
id: manifest-annotate
continue-on-error: true
run: |
docker buildx imagetools create \
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
--annotation='index:org.opencontainers.image.description=${{ github.event.repository.description }}' \
--annotation='index:org.opencontainers.image.created=${{ steps.timestamp.outputs.timestamp }}' \
--annotation='index:org.opencontainers.image.url=${{ github.event.repository.url }}' \
--annotation='index:org.opencontainers.image.source=${{ github.event.repository.url }}' \
$(printf '${{ env.GHCR_IMAGE }}@sha256:%s ' *)
- name: Create manifest list and push without annotations
# This step creates a manifest list for the Docker images built for different platforms.
# It uses the docker buildx imagetools create command to create the manifest list.
# The manifest list is created without annotations if the previous step fails.
# The manifest list is pushed to the GitHub Container Registry (GHCR) with the specified tags.
if: steps.manifest-annotate.outcome == 'failure'
working-directory: /tmp/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.GHCR_IMAGE }}@sha256:%s ' *)
- name: Inspect image
# This step inspects the created manifest list to verify its contents.
# It uses the docker buildx imagetools inspect command to display information about the manifest list.
# The inspection output will show the platforms and tags associated with the manifest list.
id: inspect
run: |
docker buildx imagetools inspect '${{ env.GHCR_IMAGE }}:${{ steps.meta.outputs.version }}'

View File

@@ -48,9 +48,9 @@ FROM ${NODE_RUNTIME_IMAGE} AS runtime
ARG DSH_VERSION ARG DSH_VERSION
LABEL org.opencontainers.image.title="DeepSeek Harness Web" \ LABEL org.opencontainers.image.title="DeepSeek Harness Web packaged by SREDevOps.org" \
org.opencontainers.image.version="${DSH_VERSION}" \ org.opencontainers.image.version="${DSH_VERSION}" \
org.opencontainers.image.source="https://github.com/deepseek-ai/deepseek-harness" org.opencontainers.image.source="https://github.com/sredevopsorg/deepseek-kubernetes-sandbox"
ENV NODE_ENV=production \ ENV NODE_ENV=production \
HOME=/home/node \ HOME=/home/node \

297
README.md
View File

@@ -1,151 +1,178 @@
Here is the complete updated Dockerfile incorporating the hoisted deploy fix and the dependency patch. The key changes versus the last version are in the deploy block: `--legacy` is dropped, `node-linker=hoisted` and `inject-workspace-packages` are enabled, and the missing workspace peer is added via the `workspace:*` protocol. # deepseek-kubernetes-sandbox based on hermes-agents-as-a-service
```dockerfile Original: https://github.com/kubernetes-sigs/agent-sandbox/tree/main/examples/hermes-agents-as-a-service
# syntax=docker/dockerfile:1.7
ARG NODE_BUILD_IMAGE=docker.io/node:24-trixie 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.
ARG NODE_RUNTIME_IMAGE=docker.io/node:trixie-slim
# --------------------------------------------------------------------------- ### 🔑 Mechanism Mapping: Hermes → DSH
# Build stage
# ---------------------------------------------------------------------------
FROM ${NODE_BUILD_IMAGE} AS build
ARG DSH_REPO=https://github.com/deepseek-ai/deepseek-harness.git | Hermes example mechanism | Purpose | Adaptation for DSH |
ARG DSH_VERSION=dsh-v0.2.1-rc.2 | :--- | :--- | :--- |
ARG PNPM_VERSION=11.28.5 | **`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 |
# CI=true skips the repository's lefthook postinstall in the container. ### 📦 Equivalent Kubernetes Manifests
ENV CI=true \
NODE_OPTIONS=--max-old-space-size=4096
RUN corepack enable \ The manifests below mirror the Hermes example's organization. Apply them in order, all in a dedicated namespace (e.g. `dsh-demo`).
&& corepack prepare "pnpm@${PNPM_VERSION}" --activate
WORKDIR /src #### 1. Secret: DSH API key
# Exact upstream tag, e.g. dsh-v0.2.1-rc.2. ```yaml
RUN git clone --depth 1 --branch "${DSH_VERSION}" "${DSH_REPO}" . apiVersion: v1
kind: Secret
RUN pnpm install --frozen-lockfile metadata:
name: dsh-api-secret
# ── Workaround for upstream packaging defect ────────────────────────────── namespace: dsh-demo
# dsh-app-boot statically imports @deepseek-ai/cordis-plugin-group, which is type: Opaque
# only a peer/workspace dependency and is omitted by `pnpm deploy --prod`. stringData:
# Promote it to a real dependency of @deepseek-ai/dsh so it is always part of DEEPSEEK_API_KEY: "sk-..." # replace with a real key
# the deploy closure.
RUN pnpm --filter @deepseek-ai/dsh add @deepseek-ai/cordis-plugin-group@workspace:*
# ──────────────────────────────────────────────────────────────────────────
# Builds native addon, Host/Client bundles, and the Web frontend.
RUN pnpm run build
# ── Production deploy closure ────────────────────────────────────────────
# A hoisted (npm-style) node_modules is required: the default isolated pnpm
# layout drops peer dependencies such as @deepseek-ai/cosmokit from the
# deployed tree, causing ERR_MODULE_NOT_FOUND at runtime. `--legacy` is
# intentionally NOT used, so pnpm's modern deploy binds peers correctly.
RUN pnpm --config.node-linker=hoisted \
--config.inject-workspace-packages=true \
deploy --prod /opt/dsh \
&& test -f /opt/dsh/lib/bin.js \
&& test -d /opt/dsh/node_modules/@deepseek-ai/cosmokit \
&& test -d /opt/dsh/node_modules/@deepseek-ai/cordis-plugin-group \
&& node /opt/dsh/lib/bin.js web --help >/dev/null
# ──────────────────────────────────────────────────────────────────────────
# ---------------------------------------------------------------------------
# Runtime stage
# ---------------------------------------------------------------------------
FROM ${NODE_RUNTIME_IMAGE} AS runtime
ARG DSH_VERSION
LABEL org.opencontainers.image.title="DeepSeek Harness Web" \
org.opencontainers.image.version="${DSH_VERSION}" \
org.opencontainers.image.source="https://github.com/deepseek-ai/deepseek-harness"
ENV NODE_ENV=production \
HOME=/home/node \
DSH_HOME=/home/node/.dsh \
DSH_PORT=3080 \
PATH="/app/node_modules/.bin:${PATH}"
# git/ripgrep are useful for DSH agent tools; tini handles signals.
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ca-certificates \
git \
ripgrep \
tini \
&& rm -rf /var/lib/apt/lists/* \
&& install -d -o node -g node -m 0755 /workspace "${DSH_HOME}"
COPY --from=build --chown=node:node /opt/dsh /app
COPY <<'EOF' /usr/local/bin/dsh-entrypoint
#!/bin/sh
set -eu
: "${DSH_PORT:=3080}"
: "${DSH_HOST:=}"
DSH_PUBLIC_URL="${DSH_PUBLIC_URL-http://localhost:${DSH_PORT}}"
# DSH rejects wildcard hosts. Pick the container/pod's first non-loopback IPv4.
if [ -z "$DSH_HOST" ]; then
DSH_HOST="$(node -e 'const os=require("os"); const ifaces=Object.values(os.networkInterfaces()).flat(); const addr=ifaces.find(v=>v&&v.family==="IPv4"&&!v.internal); if(!addr){process.exit(1)} process.stdout.write(addr.address)')"
fi
if [ -n "$DSH_PUBLIC_URL" ]; then
set -- --public-url "$DSH_PUBLIC_URL" "$@"
fi
exec node /app/lib/bin.js web \
--no-open \
--host "$DSH_HOST" \
--port "$DSH_PORT" \
--trusted-host localhost \
--trusted-host 127.0.0.1 \
"$@"
EOF
RUN chmod 0755 /usr/local/bin/dsh-entrypoint
WORKDIR /workspace
USER node
EXPOSE 3080
VOLUME ["/home/node/.dsh"]
STOPSIGNAL SIGTERM
ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/dsh-entrypoint"]
``` ```
### What changed and why #### 2. SandboxTemplate: DSH Web blueprint
| Area | Before | Now | Reason | 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).
| ------ | -------- | ----- | -------- |
| Deploy linker | default isolated `.pnpm` store | `--config.node-linker=hoisted` | Produces a flat npm-style tree so peer deps like `@deepseek-ai/cosmokit` are physically present. |
| Deploy flag | `--legacy` | removed | Legacy deployer does not reliably bind peer dependencies; the modern deployer does. |
| Workspace binding | implicit | `--config.inject-workspace-packages=true` | Guarantees workspace packages and their peers are bound into the closure deterministically. |
| Missing peer patch | `@deepseek-ai/cordis-plugin-group@^1.0.1` | `@deepseek-ai/cordis-plugin-group@workspace:*` | Correct protocol for a monorepo-internal package; no dependency on a published npm version. |
| Verification | only `bin.js` exists + `web --help` | also asserts `cosmokit` and `cordis-plugin-group` directories exist | Fails the build early if the closure is still incomplete, instead of failing at container start. |
### Build and run ```yaml
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.
```yaml
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.
```yaml
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:
1. Accept external requests (e.g. `/u/{user}/**`).
2. Look up the `status.serviceFQDN` of that user's Sandbox.
3. Reverse-proxy to `http://{serviceFQDN}:3080`.
4. If the Sandbox is `Suspended`, flip its `operatingMode` back to `Running`, wait for `Ready`, 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
1. **Secret-injection policy.** The Hermes example sets `envVarsInjectionPolicy: Disallowed` to strictly limit what Claims can inject. DSH needs `DEEPSEEK_API_KEY` injected, so you have two choices:
- **Set it to `Allowed`**, then reference the Secret from `SandboxClaim.env` (more flexible, but requires trusting Claim creators).
- **Keep it `Disallowed`** and reference the Secret directly inside the `SandboxTemplate`'s `podTemplate` (stricter, but all users share the same key — fine for a single-tenant internal setup).
Pick based on your multi-tenancy model.
2. **`DSH_PUBLIC_URL` configuration.** Your entrypoint defaults to `http://localhost:3080`. When accessed via a headless Service or gateway, set `DSH_PUBLIC_URL` to the real external URL (e.g. `https://alice.dsh.example.com`), otherwise links DSH generates may be unreachable. You can override it in `SandboxClaim.env` (if injection is allowed) or rewrite URLs in the gateway.
3. **Image pull and platform.** As with the Hermes example, push `dsh-web` to a registry reachable by the cluster. Your image is built for `amd64`; node architecture must match.
4. **PVC size.** DSH state size (sessions, configs, etc.) depends on usage; Hermes allocated a PVC for `/opt/data`. Adjust the `storage` request in `volumeClaimTemplates` accordingly.
5. **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: `Running` while active, `Suspended` when idle (PVC + Service retained), auto-woken when the user returns.
### ✅ Deployment Order
```bash ```bash
docker build \ kubectl create namespace dsh-demo
--build-arg DSH_VERSION=dsh-v0.2.1-rc.2 \ kubectl apply -f 00-secret.yaml
-t dsh-web:0.2.1-rc.2 . kubectl apply -f 10-sandboxtemplate.yaml
kubectl apply -f 20-sandboxwarmpool.yaml
docker run --rm -it \ kubectl apply -f 40-claim.yaml # one per user
-p 3080:3080 \
-v dsh-home:/home/node/.dsh \
-e DEEPSEEK_API_KEY \
dsh-web:0.2.1-rc.2
``` ```
Then open the `dsh web:` URL printed in the logs (it carries a per-process access token). 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.
### If the `pnpm add …@workspace:*` step ever fails
That would mean `@deepseek-ai/cordis-plugin-group` is not a workspace package in the chosen tag. In that case remove the two `pnpm --filter … add` lines; the hoisted linker alone is usually sufficient to include peer dependencies. You can confirm by checking that the `test -d /opt/dsh/node_modules/@deepseek-ai/cordis-plugin-group` assertion still passes.

View File

@@ -0,0 +1,30 @@
apiVersion: extensions.agents.x-k8s.io/v1alpha1
kind: SandboxTemplate
metadata:
name: dsh-web-template
namespace: default
spec:
podTemplate:
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000 # Matches the UID of the 'node' user
runAsGroup: 1000
containers:
- name: dsh-web
image: ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox/deepseek-kubernetes-sandbox:0.2.1-rc.2
ports:
- containerPort: 3080
env:
- name: DEEPSEEK_API_KEY
valueFrom:
secretKeyRef:
name: deepseek-secret
key: api-key
volumeMounts:
- name: dsh-home
mountPath: /home/node/.dsh
volumes:
- name: dsh-home
persistentVolumeClaim:
claimName: dsh-home-pvc # You will need to create this PVC