Devbox:用声明、锁文件与服务编排还原开发环境
新成员装好了 Node.js,构建结果还是不一样
仓库的 README 写着“使用 Node.js 20”,三位开发者也都能执行 node --version,但一台机器调用了全局 protoc,一台从旧项目继承了 OPENSSL_CONF,CI 则在缓存未命中时临时编译了一套系统库。版本号看起来一致,真正参与构建的可执行文件、依赖闭包和环境变量却没有共同来源。继续在 README 里追加安装命令,只会把差异藏得更深。
Devbox 把项目所需包、变量、初始化动作、脚本和服务写进 devbox.json,再由 Nix 把解析结果落入只读 Store,并用 devbox.lock 固定精确包与 nixpkgs 提交。它不需要 Docker,也不会创建虚拟机;普通 devbox shell 仍共享宿主内核、网络、文件系统和大部分环境。团队得到的是一条可审查的包环境还原链,而不是安全沙箱。
安装动作会把 Nix 带进主机
Devbox 支持 Linux、macOS 和 WSL2;Windows 开发机应从 WSL2 发行版内安装和运行,而不是把原生 Windows 路径与 Linux Store 混用。安装前先记录操作系统、CPU 架构、当前 nix --version 和 shell 类型,再从 Devbox 安装页选择目标发布线的官方入口。发布升级可能提高最低 Nix 版本,团队基线应同时约束 Devbox 与 Nix,而不能只检查一个 CLI。
个人实验机可以下载官方安装脚本、完成评审后再执行,避免把网络响应直接交给 shell。生产开发镜像则应把批准的安装器摘要和 Devbox 发布线固化在镜像流水线中:
curl --fail --show-error --location \
https://get.jetify.com/devbox -o /tmp/devbox-install.sh
less /tmp/devbox-install.sh
bash /tmp/devbox-install.sh
rm -f /tmp/devbox-install.sh安装器默认跟随最新发布,不能充当团队版本锁。使用官方 launcher 的团队可在宿主配置 DEVBOX_USE_VERSION 固定 CLI,例如 export DEVBOX_USE_VERSION=0.17.5;通过 Nix profile 安装的团队则固定 flake revision 或 profile generation。两条路径选其一并记录来源,避免 launcher 环境变量、Nix profile 和 PATH 中旧二进制相互抢占。
官方安装器在找不到 Nix 时可以代为安装:Linux 与 WSL2 通常进入 single-user 形态,macOS 使用 multi-user 形态。已有 Nix 的主机先在临时 VM 或专用开发机验证,因为 daemon、nix.conf、trusted users 和 binary cache 都可能是组织级配置。安装完成后,用实际输出确认命令来源,而不是只看安装器的成功提示:
devbox version
nix --version
command -v devbox
nix show-config | grep -E '^(substituters|trusted-public-keys|trusted-users)'command -v devbox 应指向批准的安装位置,Nix 配置应只包含团队认可的 substituter 与公钥。若 devbox 存在而 nix 初始化失败,常见证据是 /nix 不可写、daemon socket 不可达或当前 shell 尚未加载 Nix profile;重新打开 shell 后仍失败,再按安装形态检查 daemon 和目录所有权,不要用 sudo devbox shell 绕过用户边界。
环境证据至少同时保留 Devbox、Nix、OS/CPU、command -v devbox 与 lock 摘要。只保存一句“安装成功”无法解释后续究竟是哪一个 launcher、Nix daemon 或包闭包参与了构建。
声明负责意图,锁文件负责解析结果
在项目根目录初始化环境,并加入一个可观察的工具:
mkdir devbox-lab && cd devbox-lab
devbox init
devbox add ripgrep
devbox installdevbox.json 是人维护的意图,devbox.lock 是 Devbox 生成的解析结果。一个适合已有 package-lock.json 和 npm test 脚本的 Node 项目起点可以写成:
{
"$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.17.5/.schema/devbox.schema.json",
"packages": [
"nodejs@20",
"ripgrep@14"
],
"env": {
"APP_ENV": "development"
},
"shell": {
"init_hook": [
"export PROJECT_ROOT=$DEVBOX_PROJECT_ROOT"
],
"scripts": {
"doctor": [
"node --version",
"rg --version",
"command -v node",
"test \"$APP_ENV\" = development"
],
"host-marker": [
"printf '%s\\n' \"${HOST_ONLY_MARKER:-missing}\""
],
"test": [
"npm ci",
"npm test"
]
}
}
}Schema URL 应绑定团队采用的 Devbox 发布线并随升级审查;它帮助编辑器校验字段,却不替代 CLI 兼容检查。nodejs@20 是解析约束,不是精确结果,@latest 更会随索引变化。执行安装后,lock 才会记录解析出的包版本和 nixpkgs commit。devbox.json 与 devbox.lock 必须一起提交:前者解释为什么选择,后者阻止下次在相同约束下重新得到另一份依赖图。
不要手改 lock。升级包时用 devbox update 产生新的 lock diff,评审时同时看直接版本、nixpkgs 提交、平台项和闭包变化。Flake 包也要固定 tag、revision 或 commit;引用可变分支只会把漂移从包索引转移到 Git 上游。
lock 是否真的承担了恢复职责,可以用副本做一次反向实验。先保存摘要,再升级并恢复两份声明文件:
sha256sum devbox.lock
cp devbox.json /tmp/devbox.json.saved
cp devbox.lock /tmp/devbox.lock.saved
devbox update
git diff -- devbox.json devbox.lock
cp /tmp/devbox.json.saved devbox.json
cp /tmp/devbox.lock.saved devbox.lock
devbox install
devbox run doctor升级后的 diff 应明确显示包版本或 nixpkgs 提交变化;恢复后 doctor 应重新落回旧闭包。旧 lock 只能恢复解析引用,前提是对应 Store path 仍在本机或可信二进制缓存中可取;上游删除、缓存过期或平台缺失仍会让冷恢复失败。因此关键环境还要保存缓存保留策略与构建来源,不能把一个 JSON lock 当作制品归档。
用普通 Shell 跑通,再用 pure 模式找宿主泄漏
空实验目录先只执行不依赖应用源码的命名脚本:
devbox run doctor预期能看到 Node.js 与 ripgrep 的版本,command -v node 指向 /nix/store/.../bin/node 一类路径,脚本以 0 退出。这个结果证明包已解析、Store 闭包可用、init_hook 已执行且变量断言成立。若版本正确而路径仍指向 /usr/local/bin/node,先检查包是否真的写入当前目录的配置,再检查 shell 中是否嵌套了另一个激活环境。
随后制造一个宿主泄漏。退出 Devbox,在宿主 shell 定义变量,再比较普通运行与 pure shell:
export HOST_ONLY_MARKER=visible-on-host
devbox run host-marker
devbox shell --pure
printf '%s\n' "${HOST_ONLY_MARKER:-missing}"
command -v node
exit普通模式可能打印 visible-on-host,pure shell 应把它收敛为 missing,同时仍能找到 Store 中的 Node.js。退出后再运行 command -v node,路径应恢复为宿主状态。pure 模式只少量继承 HOME、USER、DISPLAY 等必要变量,它适合发现隐式依赖,却仍能访问宿主文件和网络;把它当作不可信代码沙箱会错误扩大安全承诺。
init_hook 在 shell 和 run 时都会执行,必须短小、幂等,不能每次都下载大文件或修改共享数据库。会产生构建、迁移或发布副作用的动作放进有名字的 script,让调用者和 CI 明确选择。脚本失败时,Devbox 应保留底层命令的非零退出码;项目门禁不要用 || true 抹平证据。
把同一入口接进项目与 CI
项目的 README、IDE task 和 CI 统一调用 devbox run,避免每个入口各自重写 PATH。已有 package-lock.json 和测试脚本的 Node 项目可以让开发者执行 devbox run test,CI 则从干净 checkout 运行;前面的空实验目录不应把这一步记成通过:
devbox install
devbox run doctor
devbox run testCI 镜像只需提供受支持的 Devbox/Nix 基线,不应再预装一套“碰巧兼容”的项目工具。缓存键至少包含 OS、CPU、Devbox 发布线和 devbox.lock 摘要;恢复缓存后仍运行 doctor,因为缓存命中只证明文件存在,不证明当前环境选中了正确路径。
需要 Dev Container 时可运行 devbox generate devcontainer,需要镜像入口时可运行 devbox generate dockerfile。生成的 .devcontainer/ 或 Dockerfile 是待评审的产物:继续检查基础镜像、目标架构、运行用户、端口、挂载和 secret。相同 Devbox 配置不会自动把服务数据、宿主 CA 和网络策略封进镜像,也不会把开发镜像变成生产镜像。
服务由 Process Compose 持有,不由 Shell 魔法持有
Devbox 使用 Process Compose 管理开发期长进程。项目可提交 process-compose.yml:
version: "0.5"
processes:
demo-api:
command: >-
node -e "require('http').createServer((req,res) => {
res.end('ok') }).listen(4310, '127.0.0.1')"
readiness_probe:
http_get:
host: 127.0.0.1
port: 4310
initial_delay_seconds: 1
period_seconds: 2前台启动后,从另一个终端验证真实请求:
devbox services up
curl --fail http://127.0.0.1:4310/
devbox services ls预期响应为 ok,服务列表显示进程存活。前台会话负责持有 Process Compose;中断它后应再次探测端口。后台模式 devbox services up -b 则会在当前 shell 退出后继续运行,必须显式停止:
devbox services up -b
devbox services ls
devbox services stop
curl --fail http://127.0.0.1:4310/最后一次 curl 应连接失败,这个失败正是清理证据。若仍返回 ok,使用 devbox services ls 和端口查询确认是否有旧 Process Compose 或宿主进程占用,不要直接杀掉所有 Node 进程。Devbox service 是本地进程编排,不提供 Docker Compose 的容器、网络与 volume 隔离;需要数据库数据时,数据目录必须单独标识归属和清理策略。
插件既复用配置,也扩大代码执行面
插件可以增加包、环境变量、helper 文件、hook、scripts 和服务。多个 include 按顺序合并,后面的插件覆盖前面的冲突项,项目自身配置再覆盖插件。这个规则适合建设团队基线,但插件 hook 会在激活时执行,生成文件也可能改变项目行为,因此每次引用升级都要像依赖代码一样评审。
先用仓库内本地插件验证合并结果,再接 Git 托管插件。远程引用固定到 tag 或 commit;默认缓存时间只决定何时重新拉取,不能把可变分支变成不可变输入。插件生成的 .devbox/virtenv/.../.env 不应提交,确实需要版本化的 helper 文件应移动到可评审位置并说明生成来源。
故障证据通常落在三处:devbox.json 的 include 顺序、.devbox 中生成结果,以及 shell 启动时的 hook 输出。出现“本机有效、CI 无效”时,先比较引用 commit 与生成文件摘要,再检查 CI 是否恢复了旧插件缓存。修复后从空 .devbox 重建一次,才能证明不是旧生成物继续生效。
缓存节省等待,也会修改主机信任
Devbox 首次还原可能从 Nix binary cache 下载,也可能在未命中时本地构建。成本会出现在网络流量、CPU 时间、CI 分钟和 /nix/store 容量上;项目目录的 .devbox 很小,并不代表环境占用很小。团队应分别记录冷启动与热启动耗时、下载字节、Store 增量和缓存命中来源。
Jetify Cache 是可选云能力,本地 Devbox 环境并不要求登录。启用前先审查 Cache 认证与主机配置:devbox cache configure 在 multi-user Nix 上可能使用 sudo 修改系统信任并写入凭据,这是主机级变更。执行前后分别保存 nix show-config 中的 substituters、trusted-public-keys 和 trusted-users,再用一次冷安装区分“命中可信 substituter”和“回退到本地构建”;仅看到下载更快不能证明缓存来源正确。
CI 使用 DEVBOX_API_TOKEN 时,把它放进 Secret 管理器,限制 push/pull 权限,禁止打印 env,并建立轮换与撤销入口。共享 runner 不要把带写权限的 token 烘焙进镜像或持久化到跨项目 cache;否则任意项目脚本都可能读取凭证并向团队缓存发布闭包。撤销后应以无 token 身份执行一次拉取和一次推送:公开或已授权闭包仍可读取,写操作应失败,失败日志中不能出现 token 内容。
回收 Store 前先关闭项目 shell 与服务,再查看磁盘和有效引用。官方提供的 Devbox 入口是:
devbox run -- nix store gc --extra-experimental-features nix-commandGC 面向全局 Nix Store,不只面向当前仓库。共享开发机和多用户 Nix 上应由管理员安排窗口,先确认其他 profile、服务和 CI 作业没有依赖待删路径。删除后下一次 cache miss 可能重新下载或构建,所以 GC 回收容量的收益要与重建成本一起观察。
配置、凭证与敏感数据要走不同通道
非敏感默认值可以写入 env。本地 .env 可通过 JSON 字段 "env_from": ".env" 注入,但必须加入忽略规则,并提供不含真实值的 .env.example。Jetify Secrets 使用 "env_from": "jetify-cloud" 注入 shell、run 和 services,需要 devbox auth login 或自动化令牌;它属于可选托管能力,不是 Devbox 还原本地包集合的必要组成。
无论 secret 来自文件还是云端,进入进程环境后都可能被子进程、崩溃转储和调试日志读取。脚本不得输出完整环境,健康检查不得回显凭证,服务日志要脱敏。外部贡献分支中的 init_hook、script 和插件先审查再授予 secret;自动激活未受信代码,会把“开发便利”变成凭证执行入口。
升级、回退与退出都围绕可恢复状态
升级先在分支中更新 Devbox 发布线和包约束,生成新的 devbox.lock,从空 .devbox 做一次冷还原,再依次运行 doctor、项目测试、pure 泄漏实验和服务清理。若闭包明显膨胀、cache miss 变多或目标平台没有包,恢复上一版 devbox.json 与 devbox.lock,重新 devbox install;lock 能恢复解析选择,却不能回退服务已经写入的数据。
项目退出 Devbox 时,先让 CI 在替代入口中通过相同的 doctor 与项目测试,再停止 devbox services,撤销 Cache/Secrets token,删除仅属于该项目的 .devbox 与生成文件。/nix/store 是共享对象,不能随项目目录一起粗暴删除。最后移除配置和 lock,并在干净机器验证新入口;这样退出证据是项目已经不再依赖 Devbox,而不是本机恰好还能从旧 PATH 找到工具。
团队长期维护的关键不是追逐每个新字段,而是让责任可见:应用团队维护包、脚本和服务,平台团队维护 Devbox/Nix 基线、Cache 信任和 CI 镜像,安全团队维护 token 与插件来源策略。冷启动时间、Store 增长、lock 变更频率、后台残留服务和撤销失败应形成趋势;当宿主内核、强隔离或生产容器成为主要矛盾时,就应升级隔离层,而不是继续往 shell 环境里追加承诺。
