跳转至

从 Docker 到 Kubernetes:DeepSeek Harness、内置 Chromium 与 DSH Plugin 实战

DeepSeek Harness 开源后,有人把它概括成“DeepSeek 开源了一个 Web 应用”。这种说法忽略了 Web Surface 后面的 Agent Runtime:模型适配、Session、工具、权限、Workspace、子 Agent、插件与 Headless 入口共享同一套运行核心。Web 并不只是聊天页面,它既能包装成 Native App,也能直接部署到远程开发机或云端;相比把一个 CLI Agent 重新包装成远程终端和移动端入口,它天然更适合跨设备访问。

本项目先把官方 npm 运行时封装成可复现的 Docker 镜像,再部署到 Kubernetes 1.30.4 测试集群;随后沿用此前 OpenClaw 与 Chrome all-in-one 镜像的思路,把 Chromium、Xvfb、Openbox、noVNC 和 DeepSeek Harness 放进同一 OCI 镜像,并把 Harness 与浏览器的接入层实现成独立的 @runzhliu/dsh-browser-desktop 插件。

这不是“页面打开就算部署成功”的演示。真正值得记录的是:Agent Runtime 同时拥有会话状态、工作区、Shell/PTY 子进程、模型凭据和代码执行能力,传统 Web 应用的 Deployment 模板并不能直接覆盖这些边界。实测还发现,deny-all NetworkPolicy 会在当前 Cilium 环境中阻断 kubelet HTTP 探针,使已经启动的进程被 liveness probe 误杀。最终通过容器内 loopback exec probe 保留了网络隔离,也恢复了正确的健康检查。

配套实现位于 runzhliu/deepseek-harness-docker。如果想先了解镜像构建、node-pty、EROFS、HOME/DSH_HOME 和 Cordis Patch,请先阅读 DeepSeek Harness Docker、Compose 与 Helm 部署实战;如果想理解 Cordis、Profile、Agent Loop 和事件溯源会话,则从 DeepSeek Harness GitHub 仓库深度解析 开始。

Kubernetes、DeepSeek Harness、DSH Browser Plugin 与 Chromium 的 all-in-one 架构

这张图展示的是当前 all-in-one 边界:远程电脑或手机经 Tailscale、port-forward 或受保护的 HTTPS 入口访问;Kubernetes 管理 Pod;Pod 内的单一 OCI 镜像同时包含 DeepSeek Harness Web、Browser Plugin 和 Chromium 桌面;PVC 保存 Harness 状态、浏览器 Profile 与工作区。它不是 Docker-in-Docker,Docker 负责构建镜像,测试集群实际由 CRI 容器运行时启动该镜像。

1. 先给实验结论

项目 实测结果
Kubernetes 1.30.4 测试集群,linux/amd64
CNI Cilium 1.16.1,执行 NetworkPolicy
Harness @deepseek-ai/dsh@0.1.0-rc.6
镜像 runzhliu/deepseek-harness:0.1.0-rc.6
内置浏览器 Debian Chromium、Xvfb、Openbox、x11vnc、websockify/noVNC
DSH Plugin @runzhliu/dsh-browser-desktop@0.1.0
控制器 单副本 StatefulSet
状态存储 5 GiB RWO 本地 LVM PVC,Bound
工作区 本次为 emptyDir,Pod 重建会清空
运行用户 UID/GID 1000,非 root
根文件系统 只读;仅 $DSH_HOME/workspace/tmp 可写
网络入口 Headless Service;无 Ingress、NodePort、LoadBalancer
网络策略 默认拒绝全部 Pod 入站
验证 Pod 1/1 Ready、重启 0;port-forward 返回 HTTP 200

最初不带 Chromium 桌面的精简基线从 Docker Hub 拉取约 190 MB,用时 28.401 秒。修复探针后,新 Pod 启动日志输出 Web 地址,容器内 dsh --version 返回 0.1.0-rc.6;本机经 API Server 转发访问首页,下载 12,109 Bytes HTML,用时约 89 ms。加入完整桌面栈后的本地 Docker 镜像约 2.46 GB,换来了可复现的可视浏览器环境,也显著增加了镜像分发与冷启动成本。

本轮没有注入模型 API Key,也没有把“Web 可用”写成“Agent 模型调用已经验证”。真实上线前还要单独测试模型 Provider、Shell/PTY、文件工具、权限审批和沙箱行为。

2. 为什么 Agent Runtime 值得用 Kubernetes 编排

npx @deepseek-ai/dsh web 已足够让个人快速打开页面。把它放进 Kubernetes,不是为了把简单体验变复杂,而是为了把一组容易漂移的运行约束变成声明式对象:

Agent Runtime 问题 Kubernetes 对应能力
固定 Node、原生模块和 DSH 版本 不可变镜像、Tag/Digest
进程退出、卡死和升级 StatefulSet、Probe、滚动更新
Profile、设置、凭据和会话 $DSH_HOME PVC
Agent 实际修改的代码 独立 Workspace Volume
API Key 与 Provider 凭据 Secret、运行时注入
CPU、内存和进程失控 requests/limits、PID 边界
无认证 Web 与代码执行接口 Service、NetworkPolicy、可信入口
审计和复现 Manifest、Event、日志和版本记录

其中最关键的是生命周期分层。镜像可以重建,Harness 状态不能跟着丢;工作区可以替换,也不应和凭据、Profile、Session 混在同一个匿名目录里。

3. 本次部署拓扑

flowchart LR
  B[本机浏览器] -->|kubectl port-forward| A[Kubernetes API Server]
  A --> S[Headless Service :3080]
  S --> P[DeepSeek Harness StatefulSet Pod]
  P --> H[DSH_HOME PVC]
  P --> W[Workspace emptyDir / existing PVC]
  P --> T[tmpfs /tmp]
  NP[deny-all NetworkPolicy] -.阻断普通 Pod 入站.-> P
  EP[loopback exec probe] -->|127.0.0.1:3080| P

这里故意没有创建公开入口。Harness Web 当前没有适合直接暴露的认证、TLS 和 Origin 安全边界,而其后端又能读写文件、启动 Shell。测试阶段通过 API Server 的 port-forward 使用,能够把访问面限定在操作者本机。

4. 把 Chromium 做进镜像,并通过 DSH Plugin 接入

当前镜像不是简单增加一个 chromium 软件包,而是组合了一套完整、可交互的桌面运行链路:

DeepSeek Harness Web :3080
        ├── @runzhliu/dsh-browser-desktop
        │       └── Chromium DevTools :9222
        └── noVNC :6080
                └── websockify → x11vnc :5900
                                  └── Xvfb :99 + Openbox + Chromium

Dockerfile 使用 Debian 原生架构的软件包安装 Chromium 和桌面组件,并加入 Noto CJK 字体,因此 linux/amd64linux/arm64 都能构建原生镜像。Compose 为 Chromium 配置 1 GiB /dev/shm;启动器只对浏览器进程增加 --no-sandbox,容器整体仍保留 cap_drop: ALLno-new-privileges

deepseek-harness-entrypoint 负责准备插件、启动 Xvfb/Openbox、x11vnc、websockify/noVNC、Chromium 与 DSH,并统一处理信号和子进程退出。Chromium 通过 9222 暴露仅容器 loopback 可达的 DevTools Endpoint;异常退出后由入口脚本自动重启。浏览器 Profile 放在 /home/node/.dsh/chrome-profile,与 Harness 状态一起持久化。

DeepSeek Harness WebUI 内嵌的可交互 Chromium

@runzhliu/dsh-browser-desktop@0.1.0 是独立 DSH Plugin,而不是写死在 Web 页面里的 iframe。它通过 sidebar.footer.action 增加入口,通过 shell.overlay 显示可移动、缩放和最大化的面板,并注册 browser_open Agent 工具。用户要求“用浏览器打开某个地址”时,Host 侧通过 CDP 创建标签页,Web Client 再自动展开同一容器里的可视桌面。

插件可以独立打包和安装:

npm pack ./plugins/dsh-browser-desktop --pack-destination /tmp
dsh plugin --profile web add \
  /tmp/runzhliu-dsh-browser-desktop-0.1.0.tgz

插件只负责 Harness Host/WebUI 集成,并不会单独安装 Chromium、Xvfb 或 noVNC;仓库中的 Docker 镜像是完整参考运行时。这一分层让其他 DSH 环境可以复用插件,也避免把 UI 集成和桌面进程管理混成一段不可拆分的启动脚本。

4.1 Tailscale 与移动端语音入口

Web Surface 的价值之一是可以跨设备进入。一个实际可行的个人场景是:项目与 DSH 留在家里的开发机或测试集群,只通过 Tailscale Tailnet 暴露受控入口;用户在外面用手机浏览器打开 DSH,以语音输入修改需求,让 Agent 操作挂载的 /workspace,再通过内置 Chromium 查看页面和验证结果。

这并不意味着可以直接公开 3080 和 6080。参考实现的 noVNC 没有独立认证,DSH 又能访问 Shell、文件和模型凭据,因此至少需要 Tailnet 身份与 ACL、最小端口暴露、受控 HTTPS 入口和凭据隔离。Tailscale 提供的是私有连通与身份基础,不会自动替代应用授权和 Agent Sandbox。

5. 为什么是 StatefulSet,而不是多副本 Deployment

Harness 的 Profile、模型设置、凭据引用、会话事件和领域存储位于 $DSH_HOME。Web Surface 当前也更接近单用户开发入口,而不是已经具备共享会话协调和多租户身份的无状态服务。

因此本次采用一个 StatefulSet 副本:

  • Pod 名和状态卷绑定关系稳定;
  • Pod 重建后重新挂载同一 $DSH_HOME
  • Helm 卸载或缩容时 PVC 默认保留;
  • 不用横向扩容伪装成高可用。

单副本不等于高可用。本次 PVC 还是本地 LVM,节点故障后的恢复能力受本地卷拓扑约束。若要做跨节点恢复,需要换成支持相应拓扑与故障语义的存储,并验证 Harness 自身对并发访问和迁移的契约。

6. 最小 Helm 配置

测试集群只覆盖公开镜像和 RWO StorageClass,没有把任何凭据写进 values:

image:
  repository: runzhliu/deepseek-harness
  tag: 0.1.0-rc.6
  pullPolicy: IfNotPresent

persistence:
  storageClass: <RWO_STORAGE_CLASS>
  size: 5Gi

networkPolicy:
  enabled: true
  ingress: []

安装命令:

helm --kube-context <TEST_CONTEXT> upgrade --install deepseek-harness \
  charts/deepseek-harness \
  --namespace deepseek-harness \
  --create-namespace \
  -f values-test.yaml \
  --wait \
  --timeout 5m

Chart 还设置了以下安全默认值:

  • automountServiceAccountToken: false
  • UID/GID 1000 与 runAsNonRoot: true
  • RuntimeDefault seccomp;
  • Drop 全部 Linux capabilities;
  • allowPrivilegeEscalation: false
  • 只读根文件系统;
  • /tmp 使用有大小限制的内存 emptyDir

7. 第一个真实问题:NetworkPolicy 把探针也挡住了

第一次安装时,镜像、PVC 和 Web 进程都正常:

dsh web: http://127.0.0.1:3080 (LAN: http://<POD_IP>:3080)

但 Pod 一直不能 Ready,Event 持续出现:

Readiness probe failed: dial tcp <POD_IP>:3080: i/o timeout
Liveness probe failed: context deadline exceeded
Container failed liveness probe, will be restarted

旧 Pod 最终累计重启 6 次。问题不在 Node.js、镜像架构、PVC 权限或 Harness 启动,而在下面这组配置的相互作用:

  1. NetworkPolicy 对 Harness Pod 设置 ingress: []
  2. Cilium 在该集群执行主机到 Pod 的入站策略;
  3. kubelet 使用 Pod IP 发起 httpGet Probe;
  4. Web 已经监听,但探针流量在进入容器前被策略丢弃。

尝试只用标准 NetworkPolicy ipBlock 放行节点网段,在这套 host-probe 路径中仍未命中。直接关闭 NetworkPolicy 虽然能让探针恢复,却会让无认证的代码执行接口重新暴露给集群内其他 Pod,不是合理修复。

8. 正确修复:容器内 loopback exec probe

健康检查真正需要验证的是本容器的 Web 进程,不需要经过 Pod 网络。因此把 readiness/liveness 改成容器内 Node.js fetch

livenessProbe:
  exec:
    command:
      - node
      - -e
      - >-
        fetch('http://127.0.0.1:3080/').then((response) =>
        process.exit(response.ok ? 0 : 1)).catch(() => process.exit(1))
  initialDelaySeconds: 20
  periodSeconds: 20
  timeoutSeconds: 3

readinessProbe:
  exec:
    command:
      - node
      - -e
      - >-
        fetch('http://127.0.0.1:3080/').then((response) =>
        process.exit(response.ok ? 0 : 1)).catch(() => process.exit(1))
  initialDelaySeconds: 5
  periodSeconds: 10
  timeoutSeconds: 3

这项修改有三个效果:

  • 仍然检查真实 HTTP 状态,而不是只检查 Node 进程存在;
  • 不需要为 kubelet 添加 3080 入站例外;
  • deny-all NetworkPolicy 可以继续保留。

StatefulSet 当时处于 OrderedReady 更新流程,旧 Pod 从未 Ready,因此模板更新后没有立即替换。删除唯一的测试 Pod 后,控制器按新 revision 重建并重新挂载原 PVC;新 Pod 最终 1/1 Ready、重启次数为 0。

9. 验收不能只看首页

先检查控制器、Endpoint、PVC 和策略:

kubectl --context <TEST_CONTEXT> -n deepseek-harness \
  get pod,statefulset,service,endpoints,pvc,networkpolicy -o wide

本次结果:StatefulSet 1/1、Pod Running/Ready、Endpoint 指向 3080、PVC 为 Bound,NetworkPolicy 仍只选择 Harness Pod 且没有 ingress 规则。

再验证版本和写入边界:

kubectl --context <TEST_CONTEXT> -n deepseek-harness \
  exec deepseek-harness-0 -- sh -lc '
    id
    dsh --version
    test -w /workspace && echo workspace-writable
    test -w /home/node/.dsh && echo dsh-home-writable
    test ! -w / && echo root-not-writable
  '

实测输出为 UID/GID 1000、DSH 0.1.0-rc.6,两个挂载目录可写而根目录不可写。

最后从操作者本机访问:

kubectl --context <TEST_CONTEXT> -n deepseek-harness \
  port-forward service/deepseek-harness 3080:3080

curl --fail http://127.0.0.1:3080/

首页返回 HTTP 200。不要为了少执行一条 port-forward,就把 Service 改成 NodePort 或给它添加公开 Ingress。

10. Kubernetes 解决了什么,没有解决什么

这次实验已经证明 Kubernetes 可以管理 Harness 的镜像版本、单实例生命周期、状态卷、健康检查、网络入口和安全上下文。它还没有证明以下能力:

  • 模型 Provider 的正确性与故障回退;
  • Harness 工具审批和 Linux Sandbox 在目标内核中的完整行为;
  • 多用户身份、授权、配额和审计;
  • 多副本共享 Session 或 Workspace;
  • 节点故障后的跨节点状态恢复;
  • 运行不可信代码时的强隔离。

容器和 Kubernetes 限制的是进程能看到哪些宿主资源;Harness 自己的权限系统限制的是 Agent 可以调用哪些工具。若要执行不可信代码,还要评估 gVisor、Kata、微虚机、独立凭据代理和受控出站网络。不能因为 Pod 使用了非 root 和只读根文件系统,就把它描述成完整的多租户 Agent Sandbox。

11. 从 OpenClaw 到 DeepSeek Harness:重点不是“放进 Pod”

越来越多 Agent 项目开始携带 Shell、浏览器、文件系统、插件和长期会话。它们不再只是一个调用模型 API 的 Web 页面,而是拥有状态、权限和执行环境的 Runtime。

Docker 的价值是把依赖、版本和启动契约写成可执行的安装文档;Kubernetes 的价值则是继续管理这些 Runtime 的状态、资源、身份、网络和生命周期。真正值得复用的不是一份“能跑起来”的 YAML,而是下面这组清晰边界:

镜像负责版本与运行依赖
StatefulSet 负责单实例生命周期
DSH_HOME PVC 负责 Harness 状态
Workspace Volume 负责用户代码
Secret 负责 Provider 凭据
NetworkPolicy 负责访问面
Probe 负责真实健康状态
更强 RuntimeClass 负责不可信执行隔离

个人本机临时体验仍然优先使用 npx 或 Docker Compose。只有需要团队复现、持久化、资源治理、版本升级和安全边界时,Kubernetes 才开始体现价值。编排不是目的,把 Agent Runtime 的隐含假设变成可观察、可升级、可恢复的契约,才是目的。

12. 参考资料