原文与参考
- 《Nix 和 NixOS:你们安利方法错了》 — Nyk Ma,2023-11-21,约 3743 字https://nyk.ma/posts/nix-and-nixos/
- Flake 中文教程https://nixos-and-flakes.thiscute.world/zh/nixos-with-flakes/nixos-with-flakes-enabled
核心论点
NixOS 的卖点不是「可复现」——该诉求 Docker 与 K8s 已能覆盖,以此作为宣传点易使人不得要领。真正的差异是完整的状态解算:
- 安装或更新系统 = 对「完整最终系统状态」求解一次方程,属函数式思维;而非「安装 / 删除什么包」的过程式思维。
- 其它包管理器只计算版本号;Nix 将配置文件(系统级与用户级)、
systemd服务、防火墙、shell 注入、GUI 菜单注册等全部纳入求解。目标:只要系统状态有解,系统即可正常使用。 - 每次「系统更新」都是从零完整构建依赖树与配置,再将整个系统状态树原子替换为当前结果。
关键特性
1. 操作系统 = 一个大号软件包
操作系统本质是一个依赖大量上游软件包的大型 project,定义它的过程即「引入一堆依赖并写胶水将其粘合」,思路与 Emacs 一致。为项目添加过 nix flake init 开发环境后即可看出:项目中的 flake.nix 与定义系统的 configuration.nix 是同一种东西。
2. 原子替换带来的容错
- 凡由 Nix 管理的文件,运行时被删除均无影响;即使删除
/boot,执行一次nixos-rebuild switch即可恢复。 - NixOS 可将整个 root 挂载到 tmpfs 上。
- 系统完全无法进入时,用 U 盘启动 NixOS,再以同一份系统配置执行一次
nixos-install即可。 - 更新执行到一半取消无影响,因为尚未进行最后「替换系统根节点」那一步——只有最后一步是原子的,性质类似日志型文件系统。
- 对比:Ubuntu
do-release-upgrade中途报错退出后,系统处于不稳定的中间态,重启与否均难处理,重跑亦不可。在 NixOS 中,软件包、内核 module 乃至内核本身都可随时切换:成功即可用,失败不影响现有系统。
3. flake = 完整的时间切片
- 只要项目
flake.lock不更新,两年后进入该项目的人会回到同一时间点,使用当时可用的软件包。 - 示例:NodeJS 使用
opencv时对系统 OpenCV 组件版本限制严格。锁定flake.lock中 Nixpkgs channel 的版本号后,将永远无法安装比当时更新的 OpenCV 及其周边生态(含显卡驱动)。 - Nixpkgs 软件包的本体是源码、上游依赖与编译流程的定义,编译产物二进制只是副产物。即使预编译二进制被镜像站清除,只要源码仍可通过
git checkout/ 下载获得并通过哈希校验,就一定能编译(使用stablechannel 时几乎必定编译通过)。 - 对比:Arch 只能依赖 Archive,而 Archive 的存续无保证,且每个二进制体积不小。Nixpkgs 当前整个 unstable channel 仅约 35 MiB,保存历史几无成本;且 channel 本身即一个 git repo。
- 对比:无法保证一两年后 Ubuntu
focal软件源中的imagemagick/ffmpeg不会升一两个小版本号,从而导致应用失效或效率下降。 - 推论:即使最终要将应用打包为 Docker 镜像,也建议以
FROM nixos/nix为基础,用 flake 思路安装依赖。 - 附注:有观点认为 NixOS 是 Gentoo 的精神续作。
4. 依赖隔离,无冲突
每个项目只 link 自己的依赖,因此 asdf、rtx、virtualenv、rustup 这类工具显得多余。开发环境除打镜像外几乎不需要 Docker;用 nix flake init --template=xxx 创建的开发环境优于 Docker:
- 「时间切片」是 Docker 无法保证的能力,属不可替代项。
- 项目内定义的依赖得到确保的同时,个人配置(如 shell 配置)仍可保留;不像 Docker 镜像内开发始终与宿主机隔一层(通常是
ssh)。
5. 一次性环境(nix-shell)
示例:iPhone 白苹果时执行 nix shell nixpkgs#idevicerestore,进入含救砖工具的 shell 环境,处理完毕后 C-d 退出即可不再关心;两周后系统 GC 会自动将其删除。
局限
1. Nix 语言
精确地说是类型系统不足:输入 services. 后 LSP 无法补全;模块系统隐式行为过多。若有 TypeScript 到 Nix 的生成器会颇受欢迎。相比 k8s YAML 与 Dockerfile 仍好不少。
作者补充观点:Unison 的思路与 Nix 高度契合——为每个函数做哈希,正如 Nix 为每个软件包做哈希;另认为 Nix 应有条件把软件包上传到 IPFS,或至少能输出对应的 IPFS CID。
2. 只负责到服务的单点 deploy,无复杂运行时编排
- NixOS 部署服务端无法替代 K8s,容器自动编排目前仍是 K8s 独一档。好在用 Nix 打包 Docker 容器既容易又精简:可以做到镜像里连 busybox 都没有,从而变相减小攻击面。参考 dockerTools.buildLayeredImage。
- NixOS 仍不能完整管理 systemd,偶尔仍需手动
systemctl enable。示例见 blueman-applet.nix。
3. 每个用户都有打包义务
小到为自己的项目写 flake.nix,中到 override 现有软件包以满足自身需要,大到向社区贡献新 pkgs 定义——使用 Nix 过程中几乎无法避免广义上的「打包」。但按入门顺序练级,难度可控;接受 Nix 的哲学后理解会越来越顺畅。
4. 文档混乱
NixOS 非常需要文档(在操作系统层面引入了过多新概念),但旧世代文档与新时代(flake)文档混杂。flake 用户常搜到旧世代配置,直接贴入 nix 文件即报错,且难以排查。
5. 报错信息不精确
函数式编程过于动态,很多类元编程场景导致难以给出精确的错误原因。
与同类物的比较
dotfile 管理器
若只把 Nix 当软件包管理器用,它是 dotfile manager 的上位替代。dotfile manager 的问题:
- 若只负责
ln配置文件:跨机器共用同一套配置会特别死板。同一配置文件在两台机器间需改动 5% 时难以处理;能像fish那样把配置拆成conf.d/*.fish碎片的软件尚可,不能拆配置的则无从下手。新配一台机器时还需判断该ln哪些碎片。 - 若具备字符串注入 / template 功能:弹性边界不明——内置哪些控制流函数、能否满足需要、能否执行 shell 命令取返回值再注入、该 shell 命令能否在两台机器都跑通。
Nix 不存在上述问题:
- 信息源灵活:系统环境(Linux 或 darwin、x64 或 arm64)、对该软件的直接配置(如
programs.atuin.settings.sync_frequency = "5m")、其它软件的配置、其它软件的注入(大量 CLI 工具会注入 shell completion)。 - 所有字段均可动态求值,可引用其它配置的值做注入或判断,例如:
services.gitea.repositoryRoot = "${config.services.gitea.stateDir}/repositories";更高阶的控制流同样可行:
# home-manager/modules/manual.nix
config = {
home.packages = mkMerge [ # 即 flatMap
(mkIf cfg.html.enable [ docs.manual.html docs.manual.htmlOpenTool ])
(mkIf cfg.manpages.enable [ docs.manPages ])
(mkIf cfg.json.enable [ docs.options.json ])
];
};- 几乎不需要执行 shell 获取系统信息:系统状态被当前 Nix 配置树完全定义,只需找到定义该信息的 key 并取 value。
- 网卡名可先验地定义,参见 networking.usePredictableInterfaceNames、networking.interfaces.<name>.name、相关讨论。
- 硬盘分区亦可先验地定义,参见 disko。
- 保底手段:仍可自行提供全量配置文件本身。
最后,Nix 把求值后的配置「DOM」渲染为文本配置文件,打包为一个 Derivation,再用类似 OverlayFS 的方式放到应有位置。
Docker / podman 容器
由于缺少「时间切片」能力,在不同时间点对同一 Dockerfile 执行两次 docker build 可能产出两个不同镜像。除非像大公司那样严格控制上游 Base 镜像版本,否则不算特别可控;一个能用的镜像 .tar 有可能成为传家宝。
Ansible / Chef
思路接近,但未对「可复现」特别上心(例如未严格限制被部署机器中软件包的版本),仅比纯 shell 脚本在可读性与可组合性上强一些。
带 .lock 的依赖管理器(bundler / cargo / pnpm 等)
可理解为这些工具的语言无关版包管理器。事实上 Nix 已把其它程序语言的「包」纳入 Nixpkgs 管辖范围,该功能称为 Package Set。

入门路径
- 先学新世代标准 nix flake,而非旧式 Nix 定义法。
- 差异不在语法(语法相同),而在对外部依赖与软件包定义的风格,可理解为换了一套约定俗成。
- 判别方法:文章中出现
nix-xxx命令 = 旧世代;nix xxx= 新世代。 - 几乎所有教程都会建议开启
nix-command与flake两个 experimental feature,开启即进入新世代。建议直接自新世代入手。
- 先在虚拟机中安装 NixOS。
- 推荐安装 GUI 版(如 KDE 版)。
- 读懂官方提供的配置文件(在
/etc/nixos中),尝试做简单修改并执行nixos-rebuild switch,观察结果是否符合预期。 - 暂不引入
home-manager、agenix这类进阶组件。
- 然后在工作机器上安装 Nix 包管理器(暂不格盘)。
- 慢慢打磨配置;之后想格盘装 NixOS,只需给配置加二三十行即可。
- 尝试用 Nix 与
direnv管理手头项目的依赖,并解决出现的问题。 - 此时已有两个环境,可研究如何重构配置、复用配置、按功能拆 module——重构配置是最大乐趣,与 Emacs 类似。
- 顺带了解
home-manager在系统中的地位、解决了什么问题、如何使用。 - 顺带了解如何在配置文件中安全存储敏感数据(如
agenix)。
- 万事俱备后再格盘安装 NixOS,此时对配置该做何修改已胸有成竹。
- 若想把手上的机器统一到一套配置,可进一步了解
nixos-anywhere、deploy-rs、disko这类安装器与部署器。
参考链接
教程
文档
- noogle.dev — 所有内置函数的用法
- https://ryantm.github.io/nixpkgs — 内置
lib函数与各语言打包器用法 - https://mynixos.com —
nixpkgs、nixosConfigurations、hmConfigurations中可写的软件包与配置项搜索