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。
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
experiments/docker-socket.ts
- 解析 symlink,确认路径最终指向一个 Unix socket;
- 把它挂到 Sandbox 里固定的
/var/run/docker.sock; - 按 socket 所属的数值 GID,把
node用户加进对应的组。
docker info 连到的就是这个 socket 背后的 daemon。镜像缺少 Docker CLI、用户访问不了 socket,或者镜像把默认 endpoint 改到了别处时,Sandbox 创建失败。
这项检查只保证 Agent 默认用的是这个 daemon,不是安全边界。Agent 之后仍可以用 docker --host 连到别处。不可信任务要靠 managed 模式的网络限制,而不是 Docker CLI 的默认值。
方案二: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 接管:
- 忽略镜像原有的
ENTRYPOINT和CMD。 - 检查镜像里有没有
docker-init、node、docker、dockerd-entrypoint.sh、timeout和tail。 - 启动一个监督进程,由它同时看管官方的
dockerd-entrypoint.sh dockerd和 Sandbox 保活进程。
/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.create 阶段记为 errored,Agent 不会拿到准备了一半的 Sandbox。
固定内容留在镜像里,每个 Attempt 只复制或导入必须写进 tmpfs 或 Sandbox 内 Docker 数据目录的东西。这样能少装几次依赖,也不容易因为网络变化让每次运行的环境不一样。
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:
Managed rootless DinD
在共享宿主上运行,或者 Agent 不可信时,选 managed 模式。和 raw 模式相比,它多了这些保护:- 运行前检查宿主机的 profile 是否按要求部署;
- 限制每个 Sandbox 的 CPU、内存、进程数和磁盘;
- 所有 NiceEval 进程共用同一份容量预算,超出时排队;
- 每个 Sandbox 有独立的外层网络;
- 进程被强杀后由宿主服务回收资源。
/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 会排队等待。先用较小的并发跑一轮冒烟:
maxConcurrency 限制一次运行同时跑几个,两者不能互相替代。
定位 DinD 创建失败
Sandbox 创建失败时,这个 Attempt 记为errored。用终端给出的 Attempt locator 查看错误正文,不要直接翻 .niceeval/ 下的文件:
在评估用例里验证 Docker 任务
三种模式下,评估用例里用 Docker 的写法都一样。下面的评估让 Agent 修复并启动一个 Compose 项目,再确认 Sandbox 里能正常运行容器:evals/docker-compose.eval.ts
sandbox 也可以直接写在 defineEval({ sandbox: ... }) 里。多个评估用例共用同一套配置时,写在 Experiment 上。