Skip to main content
当评估任务要求 Agent 执行 docker build、docker run 或 docker compose 时,先按任务的信任边界 选择 Docker access: 拿不准时按这个顺序判断:Agent 可信、只在自己机器上跑,选 Docker socket;一次性 VM 或专用 runner,选 raw privileged DinD;共享宿主或不可信 Agent,选 managed rootless DinD。 managed rootless DinD 需要宿主机先装好 Docker profile。它依赖 Linux cgroup v2、systemd 和支持 project quota 的文件系统。NixOS 可以用 NiceEval module 部署;Ubuntu、Debian 和其它 systemd Linux 需要自己装等价的宿主服务。 macOS 上的实际选择是 OrbStack 或 Apple container:
  • OrbStack 的 Docker engine 可以用于可信任务的 Docker socket 模式。需要完整 profile 时,在 OrbStack Linux machine 内运行 NiceEval,并按该发行版的 Linux 安装方式部署宿主服务。
  • Apple 官方 container 在 Apple silicon 和 macOS 26 上把 Linux container 作为轻量 VM 运行。当前 NiceEval Docker Provider 不操作它的 CLI 或 API,因此不能直接选择 container;这需要独立 Provider。
Apple container 使用 Containerization framework,不是 containerd。直接安装 containerd 也不会让 Darwin 获得 Linux cgroup 或 project quota;它仍需要 Linux VM。 不管选哪种模式,镜像里都要预装 Docker CLI,NiceEval 不会在运行时帮你装 Docker。 两种 DinD 模式还要求镜像从官方 docker:<version>-dind 派生,并装好 Node 和评估要用的工具。你不需要写自己的 ENTRYPOINT:Sandbox 里 Docker daemon 的启动和监督、Sandbox 保活、容器内 TTL 和 docker info 检查都由 NiceEval 负责。 不要直接把 docker:<version>-dind 写成 source.image。这个镜像没有 NiceEval 需要的 Node,也没有被测 Agent 的工具,创建 Sandbox 时会报 dind-image-incompatible: missing node。用下面的 Dockerfile 派生一个镜像,或者先发布一个等价的派生镜像,再在 source 里引用它。

方案一:直接挂 Docker socket

Agent 可信、你只想让它用上机器上已有的 Docker 时,选这个模式。它启动最快,因为不用再起一个 daemon。 先准备一个只装 Docker CLI、Node 和评估工具的镜像:
sandbox/Dockerfile
然后在 Experiment 里写明宿主机的 Unix socket 路径。NiceEval 不会从环境变量或 Docker context 猜这个路径:
experiments/docker-socket.ts
运行时,NiceEval 会:
  • 解析 symlink,确认路径最终指向一个 Unix socket;
  • 把它挂到 Sandbox 里固定的 /var/run/docker.sock;
  • 按 socket 所属的数值 GID,把 node 用户加进对应的组。
Agent 启动前,NiceEval 还会确认镜像没有预设 Docker endpoint 或 context,而且直接运行 docker info 连到的就是这个 socket 背后的 daemon。镜像缺少 Docker CLI、用户访问不了 socket,或者镜像把默认 endpoint 改到了别处时,Sandbox 创建失败。 这项检查只保证 Agent 默认用的是这个 daemon,不是安全边界。Agent 之后仍可以用 docker --host 连到别处。不可信任务要靠 managed 模式的网络限制,而不是 Docker CLI 的默认值。
这不是隔离方案。Agent 可以通过 socket 创建 privileged 容器、挂宿主目录,并操作同一 daemon 上的 其它容器、image、volume 和 network。不要给不可信 Agent 挂 rootful 宿主 socket。

方案二:Docker-in-Docker

需要每个 Sandbox 有自己独立的 Docker 时,选 Docker-in-Docker(DinD)。每个 Sandbox 里单独起一个 Docker daemon,image、network 和缓存都不和别的 Sandbox 共享。 先创建镜像:
sandbox/Dockerfile
正式评估时,把 FROM 固定到审核过的 digest,并把被测 Agent 要用的 CLI 提前装进镜像。

NiceEval 怎样启动 DinD

写了 dockerAccess: { mode: "dind", ... } 后,镜像怎么启动由 NiceEval 接管:
  1. 忽略镜像原有的 ENTRYPOINT 和 CMD。
  2. 检查镜像里有没有 docker-init、node、docker、dockerd-entrypoint.sh、timeout 和 tail。
  3. 启动一个监督进程,由它同时看管官方的 dockerd-entrypoint.sh dockerd 和 Sandbox 保活进程。
其中任何一个进程提前退出,整个外层容器都会停止。你不会拿到一台看起来还活着、却用不了 Docker 的 Sandbox。 Sandbox 里的 Docker daemon 只监听 /var/run/docker.sock,不开放 2375/2376 TCP 端口。Agent 仍以 user: "node" 运行。上面的 Dockerfile 在构建时把 node 加进了 docker 组,所以不需要对 socket 做 chown root:node 或 chmod 666。 派生镜像不要设置 DOCKER_HOST 或 DOCKER_CONTEXT。NiceEval 会先确认用的是默认 context,再核对直接运行 docker info 和显式访问 /var/run/docker.sock 连到的是同一个 daemon。这项检查通过后,才会运行你在 readiness 里写的命令。 镜像缺工具、用户不在 docker 组、daemon 提前退出或 readiness 超时时,NiceEval 会在删除这个容器前收集一段日志尾部,并在错误里写明原因。排查方法见下面的「定位 DinD 创建失败」。 镜像原有的启动方式不会保留。要评测一组必须用自己 ENTRYPOINT / CMD 启动的服务,改用 Docker Compose Sandbox,由 Compose 文件声明怎么启动这些服务。

在 Agent 开始前准备 Docker 环境

Dockerfile 只放固定的工具和项目初始文件。需要用到 Sandbox 里 Docker daemon 的操作,写进 Sandbox 的 .before() 准备步骤,不要写进镜像的 ENTRYPOINT。 下面这个准备步骤创建一个可写的 workspace,并确认 Docker daemon 可用。镜像里确实放了对应的归档或文件时,再在这里加 docker load、文件复制或项目自检命令:
lib/dind-sandbox.ts
运行顺序是:先启动并检查 Sandbox 里的 Docker daemon,再执行这个准备步骤,最后运行 Agent。命令非零退出时,这个 Attempt 在 sandbox.create 阶段记为 errored,Agent 不会拿到准备了一半的 Sandbox。 固定内容留在镜像里,每个 Attempt 只复制或导入必须写进 tmpfs 或 Sandbox 内 Docker 数据目录的东西。这样能少装几次依赖,也不容易因为网络变化让每次运行的环境不一样。
使用 Docker profile 时,准备步骤的结果默认不能缓存,因为默认的 sandboxState.all 无法保存。写了 cache.state: sandboxState.dockerData 的步骤,要同时满足两个条件才能下次直接沿用:这个步骤只改动 Sandbox 里已经停止写入的 /var/lib/docker;profile 配置了独立、预先分配好、固定大小的存储位和种子数据。 共享 loop 或 project-quota 的 profile 会显示 Unsupported,这个步骤和它后面的步骤每次都真实执行。

Raw privileged DinD

在用完就丢的 VM 或专用 runner 上,可以选 raw privileged。外层容器以 privileged 运行,所以它不是隔离方案,只适合机器本身就是一次性的场景:
experiments/dind-raw.ts
isolation: "raw-privileged" 必须写明,它代表你确认接受这个风险。失败时 NiceEval 也不会自动改用 managed 模式。 storageProfile 给 Sandbox 里的 /var/lib/docker 分配有上限的磁盘空间,不要把这个路径放进 tmpfs。它只负责磁盘额度和进程被强杀后的清理,不会让 raw privileged 变得安全。

在 macOS 上使用 OrbStack

在 macOS 上跑可信的本地评估时,可以用 OrbStack 的 Docker。先让 OrbStack 创建 Docker context,再查出实际的 Unix socket:
去掉输出开头的 unix://,把剩下的路径填进 dockerAccess.socketPath。这仍是 Docker socket 模式,Agent 可以完全控制 OrbStack 的 Docker,不适合不可信任务。 需要 managed rootless DinD 时,在 OrbStack 里建一台完整的 Ubuntu machine,而不是把宿主服务装进 macOS:
进入 machine 后,按 Ubuntu 的方式准备 systemd、cgroup v2 和支持 project quota 的文件系统,并在这台 machine 里运行 NiceEval。Ubuntu 安装包发布前,这条路需要你自己装宿主服务,不能开箱即用。不要把 OrbStack 的 Docker 当成 managed profile 用。

Managed rootless DinD

在共享宿主上运行,或者 Agent 不可信时,选 managed 模式。和 raw 模式相比,它多了这些保护:
  • 运行前检查宿主机的 profile 是否按要求部署;
  • 限制每个 Sandbox 的 CPU、内存、进程数和磁盘;
  • 所有 NiceEval 进程共用同一份容量预算,超出时排队;
  • 每个 Sandbox 有独立的外层网络;
  • 进程被强杀后由宿主服务回收资源。
NixOS 宿主先按在 NixOS 上配置 Managed DinD部署并验证 profile,再写下面的 Experiment。 下面的例子假设 Dockerfile 已经把固定的 runtime 归档复制到 /opt/images/runtime.tar。load-fixed-runtime 只改动 Sandbox 里的 /var/lib/docker,所以可以声明 sandboxState.dockerData。如果准备步骤还会写 workspace、home、tmpfs、挂载目录或外部系统,就不能这样声明。
experiments/dind-managed.ts
dockerDataBytes 是每个 Attempt 里 Docker 数据的磁盘上限。宿主服务从磁盘配额里划出一份专用空间,挂到 Sandbox 的 /var/lib/docker。所以 Docker image 和 BuildKit 缓存占的是磁盘,不计入宿主机的 Shmem;workspace、/tmp 这类确实要放在内存里的路径,仍可以用有上限的 tmpfs。 profile 不存在、名字拼错、检查不通过或容量不够时,运行会在调用模型之前失败,不会退回 raw privileged 模式。 宿主部署完成后,先检查 profile,再运行:
list 应该列出 default。doctor 会用这个 profile 启动一次 DinD 容器,并在里面实际运行 docker run --rm alpine:3.20 true。它证明 profile 能跑嵌套 Docker,但不检查你项目的 Dockerfile;项目镜像能不能用,由运行时的 docker info 检查确认。 跑完后用 pnpm exec niceeval show 在终端查看结果,或用 pnpm exec niceeval view 在浏览器里查看。

为 DinD 设置运行并发

resources.memoryBytes 限制一台 Sandbox 的内存,dockerDataBytes 限制它的 Docker 数据。profile 的容量由同一宿主机上所有 NiceEval 进程共用;内存或磁盘不够时,新的 Attempt 会排队等待。先用较小的并发跑一轮冒烟:
再根据宿主机的空闲内存、CPU 和磁盘速度逐步调高。profile 的总容量防止几个 Experiment 加起来压垮宿主机,maxConcurrency 限制一次运行同时跑几个,两者不能互相替代。

定位 DinD 创建失败

Sandbox 创建失败时,这个 Attempt 记为 errored。用终端给出的 Attempt locator 查看错误正文,不要直接翻 .niceeval/ 下的文件:
常见错误与处理方式:

在评估用例里验证 Docker 任务

三种模式下,评估用例里用 Docker 的写法都一样。下面的评估让 Agent 修复并启动一个 Compose 项目,再确认 Sandbox 里能正常运行容器:
evals/docker-compose.eval.ts
Socket 模式下,这些命令操作的是你指定的宿主机 Docker;两种 DinD 模式下,操作的是 Sandbox 里自己的 Docker。 sandbox 也可以直接写在 defineEval({ sandbox: ... }) 里。多个评估用例共用同一套配置时,写在 Experiment 上。