全面移除 Nix:用 mise + uv 重建本地工具链,用单个自建镜像接管 CI

本文最后更新于:2026年11月6日 上午

背景

我的 Homelab2 repo 之前是遵循上游 repo,使用 nix。但是有 2 点很别扭:

  1. 我的台式机是 Ubuntu,用 nix 很奇怪;
  2. nix 镜像尺寸有点大,国内每次跑 CI 拉取镜像得好几分钟。

这次改动的目标只有一个:把 Nix 从本地开发和 CI 两条链路里彻底拿掉,并且不留回潮的口子。

原来的 devShell 里装了 31 项工具,CI 侧依赖 nixos/nix 和 nixery.dev 的镜像。迁移完成后,这两个名字在仓库里出现一次都会被 scripts/check-no-nix 拦下来。

接下来介绍规划的工具分工。

一、uv 管 Python,mise 管其余

Python 这一摊全部交给 uv:

  1. 解释器版本由 .python-version 固定到 3.13
  2. 虚拟环境、依赖解析、uv.lock 一起入库
  3. Python CLI 工具也走 uv
1
2
# .python-version
3.13

依赖分组用根 pyproject.toml 里的 PEP 735 dependency groups,一共四组:scripts、dev、ci、docs。很清楚,看名字就能知道用途。

非 Python 工具交给 mise,版本精确固定到补丁位(我的习惯,出于安全考量):

工具 固定版本
helm 3.20.2
kubectl 1.36.6
kustomize 5.8.3
opentofu 1.11.14
go 1.26.10
shellcheck 0.11.0

入口统一成一条命令:mise run setup

1
2
3
4
5
6
[tasks.setup]
description = "安装 mise 工具并同步 uv 依赖"
run = """
mise install --yes
uv sync --all-groups
"""

为什么不全用 mise,或者全用 uv?

uv 对 Python 的解释器、venv、lock 是一体的,换成 mise 管 Python 反而要自己拼装。mise 反过来能管 helm、go、opentofu 这类二进制,uv 管不了。

📝 Notes:这条分工的边界是语言生态与通用二进制两条线,不是新工具与旧工具的分界。个人习惯,仅供参考。

二、CI 只拉镜像,不建镜像

CI 侧的做法是单镜像加宿主机构建(宿主机构建也是为了节省 CI 时间)。

镜像定义在 .woodpecker/ci/Dockerfile,基础镜像 debian:13-slim 按 digest 固定,不跟 tag 漂:

1
2
# .woodpecker/ci/Dockerfile
FROM debian:13-slim@sha256:<digest> # 按 digest 固定,不跟随 tag

镜像里只装 pre-commit (用于静态检查) 与 helm-diff 需要的东西:helm、dyff(用于 helm chart 选然后 diff)、shellcheck、git、diffutils、Python 与 pre-commit。

go、opentofu、ansible、kubectl、kustomize、gotestsum、docker CLI 一个都不装。原因是:

  1. 装了后镜像太大了
  2. go、opentofu、ansible、kubectl、kustomize、gotestsum、docker CLI 在正常 CI 场景中很少会用到,因为相关代码很少会变动

构建由我在宿主机执行 scripts/build-ci-image,内部是:

1
docker buildx build --push --provenance=false

同时推 latest 和 sha-<short> 两个 tag,并输出 digest。CI 侧只做 pull: true,不使用 Woodpecker secret,不使用 privileged 步骤。收益是:pipeline 里没有构建镜像的耗时,也没有把 registry 凭据塞进 CI 的必要。代价是镜像重建变成一个人工环节,后面会讲到。(我认为可接受)

三、镜像从 3.57GB 瘦到 842MB

第一版是全量镜像,3.57GB。(解压后的尺寸)

k8s agent 拉取直接超时,CI pipeline 失败:

1
backoff: maximum elapsed time exceeded (last error: container "wp-01m68s38mskt3yad4bz2yjvh0d" in pod "wp-01m68s38mskt3yad4bz2yjvh0d" is waiting to start: trying and failing to pull image)

然后是一轮一轮砍:3.57GB → 2.36GB → 1.68GB → 842MB,四轮。

最后落地的 DISK USAGE 219MB,CONTENT SIZE 842MB 版本,体积只有首版的约 23%。

📝 Notes:镜像体积在这里不是审美问题。3.57GB 会直接把冷启动拉过超时线,这是 CI pipeline 的真实失败原因。

四、性能账:从 1174s 到 20s

迁移前最近 5 次成功 PR pipeline 的耗时中位数是 1174s🤣🤣🤣(国内,没办法)。

刚开始镜像尺寸很大的时候:

Pipeline 耗时 说明
#2702(PR #600) 423s 完整 pipeline
#2704 569s 完整 pipeline
#2707 71s 仅 pre-commit

最终优化后:

  • 完整 pipeline 60s
  • 仅 pre-commit 20s

这个收益基本可以归因到一件事:镜像里不再有 go、opentofu、kubectl、kustomize、docker CLI 这一堆用不上的东西,拉取时间大大减少。

五、防回潮:把约定写成脚本

一次性迁移和长期不退化是两码事。

仓库里加了 scripts/check-no-nix,两条扫描线:

  1. 用 git ls-files 扫文件名,命中 flake.nix、flake.lock、.envrc、run_nix_develop.sh 就报错
  2. 用 git grep 扫内容,命中 Nix 命令、nixos/nix、nixery.dev、nixpkgs 就报错

这个脚本同时挂在 pre-commit 本地 hook 上,本地提交时给反馈,CI 里做阻断。

CI 的 static-checks 现在只跑 SKIP=tofu_fmt 的其他 pre-commit,13 个 hook。要跑 tofu_fmt 就得装 opentofu,而且 terraform 几乎不会修改。

六、好处与代价

👍️ 好处:

  • 本地环境一条 mise run setup,版本精确到补丁位
  • CI 冷启动从 1174s 基线降到 20s
  • 镜像 219MB,不含用不上的工具链
  • Nix 回潮有脚本兜底,不靠人记

👎 代价:

  • CI 镜像需要自己构建

迁移完成后,2026-10-31 做最终验收:本地 make filter=Tools(11 tests)、pre-commit(13 hooks)、mkdocs --strict、scripts/check-no-nix 全部通过。

总结

这次迁移的总结如下:

  • 把 Nix 从本地开发与 CI 两条链路彻底移除:
    • Python 归 uv,二进制工具归 mise,
    • CI 侧改为宿主机构建单个自建镜像、pipeline 只拉取,
  • 并用 check-no-nix 脚本防止回潮。

感觉这篇文章可能对读者没啥参考价值(唯一有价值的点可能就是:鉴于国内网络,选择工具链首选尺寸小或国内可达的或国内有源的),可以当个乐子读~

📚️ 参考文档


全面移除 Nix:用 mise + uv 重建本地工具链,用单个自建镜像接管 CI
https://ewhisper.cn/posts/55930/
作者
东风微鸣
发布于
2026年11月5日
许可协议