Files
dsh-web/DESIGN.md
T
m1saka 9a71772771 Harden image admission, resource recovery and session renewal
Add exact-image compatibility policy, experimental admission, paginated build history and shared reauthentication checks. Preserve uncertain Docker creation records, bound frontend resource waits and renew runtime sessions proactively.

Lazy-load and split Monaco, add regression and opt-in live Docker fault tests, and record full race and real-browser acceptance results.
2026-10-14 13:41:54 +08:00

28 KiB
Raw Blame History

DeepSeek Harness 多租户容器管理平台 — 设计文档

状态:本机管理面板核心流程已实现并通过真实 Docker 集成检查;生产隔离、硬配额等仍有差距,以 README.md 的实现边界与验证范围为准。下文是目标设计,不代表所有条目已实现。 目标项目:deepseek-ai/deepseek-harness(npm CLI @deepseek-ai/dsh,命令 dsh web) 控制面技术栈:Go + 前端 SPA 部署形态:单机 Docker daemon;面板自身亦运行于容器中 鉴权/配额:内置账号 + 目录配额


0. 调研结论与硬约束

实现阶段实测更正:本地 npm 0.2.0-rc.2 支持 --trusted-host,但拒绝 --public-url;下文基于 upstream master 的 public-url 描述不能直接用于该发行版。实际启动仅传 web/no-open/port/trusted-host,代理负责外部路径。scripts/probe-runtime.mjs 已验证默认 token 交换为 303 + cookie、认证首页 200;完整代理以集成测试为准。

针对本体 deepseek-ai/deepseek-harness(npm CLI @deepseek-ai/dsh,命令 dsh web)确认:

事实 来源 对设计的影响
无官方 Docker 镜像/Dockerfile repo 根、apps/、.gitlab-ci.yml 均无 必须自建镜像包装 dsh web
dsh web 默认监听 127.0.0.1:3080,且启动时拒绝 --host 0.0.0.0(安全原因) packages/bundle/web-app/README.md 容器内必须跑反向代理绑 0.0.0.0 转发到 loopback
端口/主机是 CLI flag(--port/--host/--public-url/--trusted-host/--no-open),无 DSH_PORT/DSH_HOST 环境变量 同上 容器启动参数化靠 CMD args,不靠 env
数据根 = $DSH_HOME(默认 ~/.dsh) docs/user/guide/providers.md、CLI README 每实例独立 DSH_HOME,bind-mount 即可保留数据
多实例硬约束:每个容器必须独立 DSH_HOME + 独立端口(task-board 单写者) architecture.md 一用户两容器 ⇒ 两个 DSH_HOME,不能共享
模型/API Key 存储:密钥在 $DSH_HOME/.credentials.yaml(YAML,上游配置接口的 write-only 语义不限制本面板所有者文件访问),模型/provider 配置在 $DSH_HOME/profiles/<profile>/cordis.patch.yml;cordis.patch.yml 只存 apiKeyEnv 引用 providers.md 备份"模型"= 备份这两个文件
"profile 文件" = profiles/web/cordis.patch.yml(YAML,用户可编辑的 patch 层);profile 目录还有 package.json(含插件依赖 + dsh.profile bundle 清单)+ node_modules(pnpm 装的插件,可重建) apps/cli/README.md 编辑 profile = 编辑 cordis.patch.yml;插件清单在 package.json
升级:用户数据全在 DSH_HOME(npm 安装树之外),升级 @deepseek-ai/dsh 后数据保留;node_modules 由 pnpm 重建 README、CLI README "保留数据 + 重启新版容器"可行
公网部署:--public-url <url/mount/> + --trusted-host;代理必须 剥前缀、保留 Host、把 Path=/ 的 cookie 改写为 mount + 加 Secure docs/user/guide/public-deployments.md 面板反代要处理 cookie/前缀,是最复杂点
启动 URL 带 per-process token,首次访问换签名 cookie 同上 面板需从容器日志取 token,服务端代换 cookie

结论:DSH 不是为容器多租户设计,但靠"自建镜像 + 容器内反代 + 面板反代桥接 token/cookie + 每实例独立 DSH_HOME"可稳态支撑。


1. 目标与范围

已确认的访问与镜像策略(覆盖下文早期假设)

  • 面向可信团队,实例所有者可通过文件管理读取、下载、修改完整 DSH_HOME,包括自己的凭据。面板不承诺凭据 write-only;跨用户隔离、路径安全、配额与恢复状态保护保持不变。下载文件/打包目录会提示泄密风险,提示属于交互防误操作,不是阻止所有者访问的安全边界。

  • 专用 Profile 编辑器提供 YAML 校验与受控停启流程,但通用文件操作、DSH 终端和 agent 不强制经过该流程;修改运行中配置存在不一致风险,建议先停机并备份。

  • 镜像准入分为推荐、仍兼容、仅存量运行、禁止使用。兼容依据绑定精确版本、模板修订、平台与镜像身份;不再仅因历史模板不是当前模板就一律判为不可用,也不能仅因历史构建成功就认定兼容。

  • 未验证 DSH 新版仅管理员可显式实验构建;保留既有完整性、依赖与启动检查。实验构建成功不是兼容验收,未明确准入的镜像不可新建实例或作为升级目标。管理员正向准入须填写验收依据并记录审计,不绕过实际镜像身份与验证记录检查。

  • 通过 docker.sock 在单机上创建/启停/删除多个 dsh web 容器,每用户最多 2 个。

  • 每用户一个独立工作区,支持文件上传/下载/删除/重命名,并有容量配额。

  • 工作区映射进每个 dsh 容器(agent 在工作区内干活)。

  • 备份/还原 dsh 配置(含模型与插件清单)。

  • 编辑 profile 文件(cordis.patch.yml)。

  • 升级:保留数据,用新版本镜像重启容器。

  • 本地镜像管理:面板查询 DSH 最新版本,用户手动触发本机构建;创建实例时选择本地已有版本,或手动构建最新版后创建。不需要私有镜像仓库,不推送生成的镜像。

  • 控制面:Go 后端 + SPA 前端;内置账号 + 目录配额;面板自身容器化运行。


2. 总体架构

面板不是宿主进程,而是挂了 docker.sock 的兄弟容器(sibling),通过 sock 编排 dsh 兄弟容器,并通过同一 bridge 网络反代它们。

┌────────────── 宿主 ──────────────────┐
│ docker.sock ──┐                       │
│ /var/lib/dsh-mgr ──┐ (bind)            │
│                    │                   │
│  ┌─────────────────▼─────────────▼───┐ │  dsh-net (bridge)
│  │ 面板容器 dsh-mgr                   │ │ :80/:443 发布
│  │  - /data ← /var/lib/dsh-mgr       │ │
│  │  - /var/run/docker.sock           │ │
│  │  - SPA + API + 反代 + 编排引擎     │ │
│  └─────────┬───────────────────────────┘ │
│            │ sock                          │
│  ┌─────────▼───────────┐ ┌───────────────┐ │
│  │ dsh-web:<v> 容器1  │ │ 容器2 ...     │ │  不发布端口
│  │ caddy:8080 → 3080   │ │               │ │  /data/dsh-home、/workspace ← 宿主 bind
│  └─────────────────────┘ └───────────────┘ │
└─────────────────────────────────────────────┘

组件:

  1. 面板服务(Go 容器):REST API + 静态 SPA + 反向代理子系统 + docker client + 配额/备份/升级引擎。
  2. dsh-web 镜像(自建):Node + @deepseek-ai/dsh + caddy + 入口脚本。
  3. dsh-net:内部 bridge 网络,容器不发布主机端口,仅面板可达。
  4. (可选)socket-proxy:面板与 sock 之间的白名单代理。

3. 宿主路径 ↔ 面板内路径映射(关键)

Docker bind 的 Source 必须是守护进程所在宿主路径,不是面板容器内路径。面板容器把宿主 /var/lib/dsh-mgr 挂到 /data,于是同一文件树有两个视角:

用途 面板直接读写(容器视角) 创建 dsh 容器 bind.Source(宿主视角)
用户工作区 /data/workspaces/<uid>/files/... /var/lib/dsh-mgr/workspaces/<uid>/files
实例 DSH_HOME /data/workspaces/<uid>/instances/<id>/home/... /var/lib/dsh-mgr/workspaces/<uid>/instances/<id>/home
备份包 /data/backups/<uid>/<id>/... (不入容器,仅面板读写)

面板配置:

HOST_DATA_ROOT=/var/lib/dsh-mgr        # 传给 docker API 的 bind 源
CONTAINER_DATA_ROOT=/data               # 面板自己 fs 操作的根
  • 所有"面板直接动文件"(文件 API、备份 tar、profile 读写)用 CONTAINER_DATA_ROOT。
  • 所有"喂给 docker API 的 bind.Source / build context"用 HOST_DATA_ROOT 拼接。
  • 封装 toHostPath(containerPath) 翻译函数集中调用,避免散落拼接出错——这是 sibling 容器最常见的 bug。

4. 数据与目录布局(宿主机)

/var/lib/dsh-mgr/
  db.sqlite                              # 用户、实例、配额、备份索引
  workspaces/<uid>/
    files/                               # 用户工作区(面板文件 API + 各 dsh 容器共享挂载)
      └─ <用户自由目录结构>
    instances/<instId>/
      home/                              # DSH_HOME(bind → 容器 /data/dsh-home)
        .credentials.yaml                #   API Key(机密)
        cordis.patch.yml                 #   home 级 patch
        profiles/web/
          package.json                   #   插件依赖 + dsh.profile bundle 清单
          cordis.patch.yml               #   profile 级 patch(= "profile 文件")
          node_modules/                  #   插件实体(可重建,不入备份)
        skins/ pets/ agent-presets/ .agent-presets/
        dsh-ssh.json                     #   0600
        sessions/ worktrees/             #   运行态(不入配置备份)
  backups/<uid>/<instId>/
    <timestamp>.tar.gz                   # 配置备份包
  # 镜像层由本机 Docker daemon 管理,不作为此目录下的文件保存;DB 记录镜像索引

配额作用域:workspaces/<uid>/ 整棵树(files + 所有 instances 的 home)。每用户一个配额单位。


5. 自建 DSH 镜像设计

FROM node:22-slim
# 1) 装 dsh CLI(版本参数化)
ARG DSH_VERSION=0.2.0-rc.2
RUN npm i -g @deepseek-ai/dsh@${DSH_VERSION}
# 2) 装 caddy(容器内反代,绕过 dsh 拒绝 0.0.0.0)
COPY caddy /usr/bin/caddy
COPY Caddyfile /etc/caddy/Caddyfile        # :8080 { reverse_proxy 127.0.0.1:3080 }
COPY entrypoint.sh /entrypoint.sh
ENV DSH_HOME=/data/dsh-home
WORKDIR /workspace
ENTRYPOINT ["/entrypoint.sh"]
# CMD 由面板按实例注入:dsh web --no-open --port 3080 --public-url <url> --trusted-host <host>
  • entrypoint.sh:tini 接管信号 → 起 caddy(后台)→ exec dsh "$@"。dsh 绑 loopback:3080,caddy 绑 0.0.0.0:8080。
  • 存储方式:生成的镜像仅保存在本机 Docker daemon,不推送 registry,也不从 registry 拉取成品 dsh-web 镜像。所有用户共享面板管理的本地镜像目录。
  • 版本查询:面板从 npm 查询 @deepseek-ai/dsh 的 dist-tags.latest,展示精确版本和查询时间;查询不触发构建或升级。不通过版本字符串排序猜测最新版。查询失败时保留本地版本列表,缓存结果需标明过期。
  • 版本固定:用户确认构建时锁定页面展示的精确版本,后端校验其确实存在于 npm 元数据中;构建过程中不再次解析 latest,Dockerfile 不安装浮动的 @latest。
  • 本机构建:Docker API POST /build 接受 tar 流;面板将内置 /image/ 的 Dockerfile+caddy+entrypoint+Caddyfile 打包后提交给 daemon,并通过 Docker API 的 buildargs 传入 DSH_VERSION。用户不能提交任意 Dockerfile、构建上下文、镜像地址或其他构建参数。
  • 镜像身份:tag 使用 dsh-web:<dshver>-<recipeRev>,其中 recipeRev 标识镜像模板修订;记录精确 DSH 版本、模板修订、架构、本地 image ID 和构建时间。创建实例使用已确认的 image ID,不依赖可能被覆盖的 tag。
  • 构建任务:手动触发后异步执行,展示排队、构建中、验证中、成功、失败状态及脱敏日志;任务持久化。同版本、模板修订和架构的并发请求复用同一任务,已有可用镜像直接复用。默认全局串行构建,并限制用户触发频率、任务时长及日志大小,构建前检查磁盘空间。
  • 可用性判定:Docker build 成功后,用临时隔离容器验证版本和 DSH 启动就绪;验证通过才加入可选择列表。失败保留错误记录,不自动替换已有可用镜像。面板重启后对账未完成任务及本地镜像,不盲目重复构建。
  • 本地清单:以 Docker inspect/list 和面板管理标签对账,展示精确版本、构建时间、可用状态及使用情况。外部删除导致 image ID 不存在时标记缺失,不再允许新建;管理员清理时保护被实例、待执行任务或回滚流程引用的镜像,不自动运行全局 prune。
  • 网络要求:仅本地保存不代表离线构建;首次构建仍可能需要拉取基础镜像及下载 npm 包。网络故障不影响选择已存在的本地可用镜像。

创建实例交互:

  1. 选择本地版本:从本地可用列表选择版本,校验 image ID 仍存在后创建。
  2. 构建最新版并创建:查询并展示精确版本 → 用户确认 → 事务预留实例名额 → 构建或复用任务 → 验证成功 → 用该 image ID 创建实例。等待构建的实例也计入每用户 2 个名额;构建/创建失败或取消时清理未完成资源并释放名额,不回退到其他版本。取消某个用户的等待不取消其他用户共享的构建任务。
  3. 面板另提供独立的“构建最新版”按钮,仅生成本地镜像,不创建或升级实例,也不占实例名额。

6. 容器生命周期(docker.sock + socket-proxy)

Go 端用 github.com/docker/docker/client:

  • 创建:
    • Image = 本地可用镜像的固定 image ID(对应精确 DSH 版本及模板修订,见 §5)
    • Env:DSH_HOME=/data/dsh-home(密钥不入 env;用户在 dsh UI 里设 key,落到 .credentials.yaml)
    • Binds(Source 用 HOST 路径):
      • workspaces/<uid>/instances/<instId>/home → /data/dsh-home
      • workspaces/<uid>/files → /workspace
    • Network = dsh-net(不发布端口)
    • Cmd = web --no-open --port 3080 --public-url https://<panel>/dsh/<instId>/ --trusted-host <panel>
    • Labels:dsh-mgr.uid、dsh-mgr.instId、dsh-mgr.version、dsh-mgr.role=dsh
    • 资源:--memory=2g --cpus=2(可配);--tmpfs /tmp;只读 rootfs + 必要写入挂载
  • 操作:start/stop/restart/remove/logs/inspect/exec,全走 SDK。
  • 2 实例上限:按逻辑实例在 DB 事务中预留名额,等待构建/创建中的实例也占名额;失败清理后释放。Docker labels 用于对账,不作为并发名额控制。
  • 对账:面板启动时按 label 枚举容器与 DB 对账(孤儿容器回收或纳管)。
  • sock 安全:面板挂裸 sock = 等效宿主 root。缓解:
    1. socket-proxy(推荐):只放行白名单 API(containers/exec/images/networks/volumes/logs/build),关 plugins/daemon/swarm。面板连 proxy 而非裸 sock。
    2. 面板自身非 root、只读 rootfs + tmpfs /tmp、资源限制。dsh 容器不 --privileged、去 cap。

7. 网络与访问方式

  • 面板启动时 ensure dsh-net(bridge)存在,并把自身加入(compose 声明或运行时 network connect)。
  • dsh 容器挂 dsh-net,不发布主机端口。
  • 面板反代 /dsh/<instId>/ → http://<container-name>.dsh-net:8080,容器名 = instId。
  • 访问方式(待定,见 §18):默认推荐"路径前缀反代 + token 桥接"(§8),面板天然为 ingress。备选:子域名(需泛 DNS+TLS)/ 直发端口(MVP,隔离弱)。

此为最复杂点。推荐方案:路径前缀反代 + token 桥接。

  • 面板路由 /dsh/<instId>/* → 反代到 http://<instId>.dsh-net:8080,剥掉 /dsh/<instId> 前缀,保留浏览器 Host,加 X-Forwarded-*。
  • token 桥接:容器启动后,面板从 docker logs 流式抓 dsh 打印的 http://127.0.0.1:3080/?token=xxx 里的 token;用户首次打开 /dsh/<instId>/ 时,面板服务端用该 token 打一次容器换 cookie,再把 Set-Cookie 的 Path=/ 改写为 Path=/dsh/<instId>、加 Secure/HttpOnly/SameSite=Lax 下发给浏览器。后续请求带该 cookie 透传。
  • 鉴权:面板先校验用户登录 + 该 instId 归属当前 uid,再放行反代。dsh 自身 token cookie 为第二层。
  • WebSocket/SSE:反代需支持 WS 升级;cookie 改写后需实测 WS 握手是否正常(关键风险)。

9. 工作区文件管理 + 容量配额

文件 API(path 校验:解析后必须在 files/ 内,拒 ../绝对路径):

  • GET /api/ws/tree?path= 列目录(带 size/mtime)
  • POST /api/ws/mkdir、POST /api/ws/rename、DELETE /api/ws/delete
  • POST /api/ws/upload(分片/流式,写前查配额)
  • GET /api/ws/download?path=(单文件流;目录自动 zip)
  • GET /api/ws/usage(当前用量/配额)

配额实现(容器化面板的约束):

  • 默认:软配额(无特权,随处可跑)。面板写前 du 校验 + 周期巡检 + 写入并发限流。并发大文件可能短暂超限。
  • 可选硬配额(二选一,需运维确认):
    • XFS prjquota:面板 --privileged + cap_add:[SYS_ADMIN,CHOWN] + 把宿主 XFS 挂载点 bind 进来,面板内对 /data 跑 xfs_quota/chattr -p。每用户唯一 project ID(uid+1000)。
    • ZFS refquota(若宿主用 ZFS):面板 --privileged + zfsutils,每用户 zfs create pool/users/<uid> && zfs set refquota=10G,再 bind 该 dataset 到 /data/workspaces/<uid>。
    • 或在宿主跑极小特权配额 sidecar(gRPC/HTTP),面板远程调它做 quota 操作——面板自身保持非特权。
  • 配额值存 DB,按用户可配(默认 10GiB)。面板展示用量。

具体选型见 §18 待拍板。


10. 配置备份与还原(含模型与插件)

备份内容(tar.gz,排除可重建/运行态):

包含 说明
.credentials.yaml 模型 API Key(机密)→ 备份包加密
cordis.patch.yml(home 级) home patch
profiles/web/package.json 插件清单(含 dsh.profile bundle 列表)
profiles/web/cordis.patch.yml 模型/provider 配置 + apiKeyEnv 引用
profiles/web/pnpm-lock.yaml(若有) 插件版本锁
skins/ pets/ agent-presets/ .agent-presets/ 用户资源
dsh-ssh.json SSH 凭据(0600,加密入包)
manifest.json dsh 版本、备份 schema 版、时间戳

排除:node_modules/(pnpm 重建)、worktrees/、sessions/、日志。

备份包加密:用用户密码派生密钥(Argon2id → AES-GCM)加密整包;密钥不落库。下载/上传均加密包。

备份流程:停容器(或仅停写)→ tar 指定文件 → 加密 → 落 backups/<uid>/<instId>/ + 索引入库 → 重启容器。支持立即下载。

还原流程:校验+解密包 → 停容器 → 备份当前 home 为 .pre-restore.bak → 覆盖配置文件(不动 node_modules)→ 按 package.json 重建插件(dsh plugin --profile web install 或 entrypoint 首启时 pnpm reconcile)→ 启容器 → 健康检查 → 失败则回滚 .pre-restore.bak。


11. profile 文件编辑

  • 编辑对象:profiles/web/cordis.patch.yml(YAML)。
  • 前端用 Monaco 编辑器;后端 GET 返回原文、POST 校验(YAML 合法 + 可选 --dump-config-schema 比对)后原子写(临时文件 + rename,dsh-atomic-write 风格)。
  • 保存后重启容器使生效(dsh 有 dsh-hmr,但配置类变更稳妥起见重启;后续可探热更)。
  • 只读展示 dsh.profile(bundle 清单)与 --dump-config 预览(面板 docker exec 跑 dsh --profile web --dump-config 取回渲染)。
  • 专用配置预览保持脱敏;完整 home 文件管理允许实例所有者读取 .credentials.yaml 原文,不承诺 write-only。

12. 升级流程(保留数据 + 重启新版容器)

1. 用户选择本地可用目标版本;若需最新版,先手动触发 §5 构建并等待验证成功。锁定本地 image ID 后才进入备份/停机流程,不自动 pull 成品镜像
2. 自动备份当前配置(§10)→ 标记 pre-upgrade 备份
3. docker stop <old>(不 remove,保留作回滚)
4. docker create <new>:同名 instId、同 binds(home + files)、同 labels、新 image、
   Cmd 按新版本要求重算 --public-url/--trusted-host
5. entrypoint 首启:若 profiles/web/node_modules 缺失/与 package.json 不一致 → pnpm install 重建插件
6. docker start <new> → 面板抓 token、健康检查(轮询 caddy :8080)
7. 成功:docker rm <old>;失败:stop <new>、start <old>、标记回滚、提示用户

关键:bind 不变 ⇒ 数据零丢失;node_modules 在新镜像里按 package.json 重建(跨版本兼容性由 pnpm/dsh-plugin-manager 保证)。


13. 安全与资源

  • docker.sock:面板容器能访问 sock 即等效宿主 root;socket-proxy 白名单 + 面板非 root + 只读 rootfs 缓解。
  • dsh 容器:不 --privileged、去 CAP_NET_RAW 等、只读 rootfs、tmpfs /tmp、mem/cpu 限制、不挂 sock。
  • 面板账号:bcrypt + JWT(短期 access + refresh),失败锁定。
  • 配置备份包加密;实例所有者通过文件 API 下载的凭据或目录 ZIP 不属于加密配置备份,应按机密数据保管。
  • 路径穿越校验、上传大小/类型白名单(可配)。
  • 审计日志:容器操作/备份/还原/升级/profile 改动入库。

14. 技术栈与项目结构

  • 后端:Go 1.22+,net/http(或 chi/gin),github.com/docker/docker/client,github.com/docker/go-connections,sqlite(modernc.org/sqlite 纯 Go 驱动,免 CGO),Argon2id、AES-GCM。
  • 前端:Vite + React + TypeScript,Monaco(profile 编辑),shadcn/ui。
dsh-mgr/
  cmd/server/         # Go 入口
  internal/
    auth/             # 账号/JWT
    docker/           # 容器编排(镜像、生命周期、对账、路径翻译)
    workspace/        # 文件 API、配额(软 du + 可选 xfs/zfs)
    proxy/            # /dsh/<id> 反代 + token 桥接 + cookie 改写
    backup/           # 备份/还原/加密
    upgrade/          # 升级编排
    profile/          # cordis.patch.yml 编辑
    store/            # sqlite schema/查询
    api/              # HTTP handlers
  web/                # SPA
  image/              # 自建 dsh-web 镜像(Dockerfile + Caddyfile + entrypoint.sh + caddy 二进制)
  deploy/             # 面板自身 docker-compose / socket-proxy / systemd

15. 数据模型(sqlite 概要)

  • users(id, username, pass_hash, quota_bytes, created_at)
  • instances(id, uid, name, dsh_version, image_id, build_id, status, container_id, created_at, ...)
  • backups(id, uid, inst_id, path, size, schema_ver, created_at, kind=auto|manual|pre-upgrade|pre-restore)
  • images(image_id, version, recipe_rev, platform, tag, status, built_at)(本地镜像索引,status=validating|available|failed|missing;与 Docker 实际状态对账)
  • image_builds(id, requested_by, version, recipe_rev, platform, status, image_id, error, created_at, finished_at)(持久化构建任务;同版本+模板修订+架构的活动任务去重)
  • audit(id, uid, action, target, detail, at)

16. 面板自身部署(compose 示例)

services:
  dsh-mgr:
    image: dsh-mgr:latest
    restart: unless-stopped
    ports:
      - "80:80"            # 前置 TLS 反代或直接 443
    user: "65532:65532"     # 非 root
    environment:
      HOST_DATA_ROOT: /var/lib/dsh-mgr
      CONTAINER_DATA_ROOT: /data
      PANEL_PUBLIC_URL: https://dsh.example.com
      DSH_NET_NAME: dsh-net
      DEFAULT_QUOTA_BYTES: 10737418240
      DOCKER_HOST: unix:///var/run/docker.sock   # 或指向 socket-proxy
    volumes:
      - /var/lib/dsh-mgr:/data
      - /var/run/docker.sock:/var/run/docker.sock:ro   # 或 socket-proxy
    networks: [dsh-net]
    read_only: true
    tmpfs: [/tmp]
    deploy: { resources: { limits: { memory: 1g } } }
    # 硬配额(XFS)可选: privileged: true; cap_add: [SYS_ADMIN,CHOWN]; volumes: [xfs mount]

  # 可选 socket-proxy
  socket-proxy:
    image: tecnativa/docker-socket-proxy
    environment:
      CONTAINERS: 1; EXEC: 1; IMAGES: 1; NETWORKS: 1; VOLUMES: 1; LOGS: 1; BUILD: 1
      POST: 1
    volumes: [/var/run/docker.sock:/var/run/docker.sock:ro]
    networks: [dsh-net]
    restart: unless-stopped

networks:
  dsh-net: { driver: bridge }

17. 关键风险与待验证

  1. token/cookie 桥接(§8)最脆——需实测 dsh token 换 cookie 在反代+前缀下的行为,确认 cookie 改写后 WS/SSE 正常(dsh web 可能有 WebSocket)。
  2. 插件首启重建:跨 dsh 版本 pnpm install 在 entrypoint 无网时失败——可能需升级时由面板 docker exec 联网重建后再切流量。
  3. XFS/ZFS 硬配额依赖宿主 FS 与特权/agent;否则退化为软配额。
  4. dsh agent 工作目录:dsh-fs-local/dsh-fs-sandbox 的根是否等于 cwd——需确认 /workspace 作为 cwd 即可让 agent 在工作区操作,或需额外配置 fs root。
  5. dsh 版本仍为 0.2.0-rc.*(RC),API/flag 可能变动——需维护版本兼容矩阵。
  6. sibling bind 路径翻译:toHostPath 出错会导致 bind 挂错目录或挂空目录,需单测覆盖。
  7. 本地镜像构建:验证 npm 查询失败仍可选本地版本、并发构建去重、失败释放实例名额、面板重启对账、镜像被外部删除,以及构建磁盘不足等路径;构建最新版不保证兼容,必须通过启动验证。

18. 待拍板决策点(open)

已确认:镜像仅保存在本机;面板支持查询并手动构建最新版;创建实例可选本地已有版本,或构建最新版后创建。不需要 registry/CI 镜像分发。

以下访问方式和配额选型仍待确认;此前审查发现的隔离、文件安全和升级回滚问题仍需另行修订。

  1. 访问方式:路径前缀反代 + token 桥接(推荐,面板天然 ingress)/子域名(需泛 DNS+TLS)/直发端口(MVP,隔离弱)。
    • 推荐默认:路径前缀反代。
  2. 配额:宿主 FS 是 XFS / ZFS / 其他?是否接受"特权面板"或"宿主配额 sidecar"做硬配额?还是软配额默认即可?
    • 推荐默认:软配额(du)起步,硬配额作为后续增强;若宿主为 XFS/ZFS 且可接受特权,则上硬配额。

19. 里程碑

  1. M1 镜像 + 单容器手动跑通:自建本地 dsh-web 镜像,验证精确版本、caddy+dsh 启动与就绪。
  2. M2 面板骨架:账号/鉴权 + npm 最新版查询 + 本地镜像列表/手动构建任务 + 创建时选本地版本或构建最新版 + docker 编排(创建/启停/删除/2 上限)+ 对账 + 路径翻译。
  3. M3 反代 + token 桥接:/dsh/<id>/ 可用(含 WS)。
  4. M4 工作区文件 API + 软配额。
  5. M5 备份/还原 + profile 编辑。
  6. M6 升级流程 + 回滚。
  7. M7 SPA 完善 + 审计 + 资源限制 + 可选硬配额。

附:调研依据(关键 URL)