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:
@@ -1,3 +1,4 @@
|
||||
DEEPSEEK_API_KEY=
|
||||
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
1
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1 @@
|
||||
* @ngeorger
|
||||
283
.github/workflows/multi-build.yaml
vendored
Normal file
283
.github/workflows/multi-build.yaml
vendored
Normal 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 }}'
|
||||
@@ -48,9 +48,9 @@ FROM ${NODE_RUNTIME_IMAGE} AS runtime
|
||||
|
||||
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.source="https://github.com/deepseek-ai/deepseek-harness"
|
||||
org.opencontainers.image.source="https://github.com/sredevopsorg/deepseek-kubernetes-sandbox"
|
||||
|
||||
ENV NODE_ENV=production \
|
||||
HOME=/home/node \
|
||||
|
||||
297
README.md
297
README.md
@@ -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
|
||||
# syntax=docker/dockerfile:1.7
|
||||
Original: https://github.com/kubernetes-sigs/agent-sandbox/tree/main/examples/hermes-agents-as-a-service
|
||||
|
||||
ARG NODE_BUILD_IMAGE=docker.io/node:24-trixie
|
||||
ARG NODE_RUNTIME_IMAGE=docker.io/node:trixie-slim
|
||||
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.
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Build stage
|
||||
# ---------------------------------------------------------------------------
|
||||
FROM ${NODE_BUILD_IMAGE} AS build
|
||||
### 🔑 Mechanism Mapping: Hermes → DSH
|
||||
|
||||
ARG DSH_REPO=https://github.com/deepseek-ai/deepseek-harness.git
|
||||
ARG DSH_VERSION=dsh-v0.2.1-rc.2
|
||||
ARG PNPM_VERSION=11.28.5
|
||||
| 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 |
|
||||
|
||||
# CI=true skips the repository's lefthook postinstall in the container.
|
||||
ENV CI=true \
|
||||
NODE_OPTIONS=--max-old-space-size=4096
|
||||
### 📦 Equivalent Kubernetes Manifests
|
||||
|
||||
RUN corepack enable \
|
||||
&& corepack prepare "pnpm@${PNPM_VERSION}" --activate
|
||||
The manifests below mirror the Hermes example's organization. Apply them in order, all in a dedicated namespace (e.g. `dsh-demo`).
|
||||
|
||||
WORKDIR /src
|
||||
#### 1. Secret: DSH API key
|
||||
|
||||
# Exact upstream tag, e.g. dsh-v0.2.1-rc.2.
|
||||
RUN git clone --depth 1 --branch "${DSH_VERSION}" "${DSH_REPO}" .
|
||||
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# ── Workaround for upstream packaging defect ──────────────────────────────
|
||||
# dsh-app-boot statically imports @deepseek-ai/cordis-plugin-group, which is
|
||||
# only a peer/workspace dependency and is omitted by `pnpm deploy --prod`.
|
||||
# Promote it to a real dependency of @deepseek-ai/dsh so it is always part of
|
||||
# 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"]
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: dsh-api-secret
|
||||
namespace: dsh-demo
|
||||
type: Opaque
|
||||
stringData:
|
||||
DEEPSEEK_API_KEY: "sk-..." # replace with a real key
|
||||
```
|
||||
|
||||
### What changed and why
|
||||
#### 2. SandboxTemplate: DSH Web blueprint
|
||||
|
||||
| Area | Before | Now | Reason |
|
||||
| ------ | -------- | ----- | -------- |
|
||||
| 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. |
|
||||
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).
|
||||
|
||||
### 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
|
||||
docker build \
|
||||
--build-arg DSH_VERSION=dsh-v0.2.1-rc.2 \
|
||||
-t dsh-web:0.2.1-rc.2 .
|
||||
|
||||
docker run --rm -it \
|
||||
-p 3080:3080 \
|
||||
-v dsh-home:/home/node/.dsh \
|
||||
-e DEEPSEEK_API_KEY \
|
||||
dsh-web:0.2.1-rc.2
|
||||
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
|
||||
```
|
||||
|
||||
Then open the `dsh web:` URL printed in the logs (it carries a per-process access token).
|
||||
|
||||
### 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.
|
||||
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.
|
||||
|
||||
30
deploy/k8s/sandbox-template.yaml
Normal file
30
deploy/k8s/sandbox-template.yaml
Normal 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
|
||||
Reference in New Issue
Block a user