Nix Flakes 深度解析:从「跑在我机器上能工作」到真正的声明式可复现开发环境
2026年7月24日 程序员茄子
序言:一个所有工程师都踩过的坑
你一定遇到过这种情况:
- 本地跑得好好的,CI 报错了,一排查发现是本地 Node.js 版本比 CI 新两个小版本
- 同事 clone 了你的项目,跑不起来,原因是他的 Python 环境装的是 3.11 而你需要 3.9
- 服务器上跑得好好的,升级了依赖包,生产环境突然崩了
- Docker 镜像打出来体积 2GB,CI 跑了 20 分钟,每次构建结果还不一样
这些问题的根源只有一个:环境不是可复现的。
传统的解决方案要么治标不治本,要么引入新的复杂度:
.nvmrc/.python-version只能锁定版本,无法管理全局工具链virtualenv/conda解决的是 Python 隔离,不是复现问题- Docker 解决了环境隔离,但引入了构建复杂度和「Docker 里能跑本地不一定」的新问题
- Devcontainer / devfile 引入了额外的配置层,yml 套 yml
2026 年的今天,真正从根上解决这个问题的技术栈已经成熟了——Nix Flakes。它不是新概念,但 2024-2026 年这段时间,Nix Flakes 从「geek 玩具」变成生产级工具的速度令人侧目。AWS、Cloudflare、Cachix 等公司都在用 Nix 做基础设施;Determinate Systems 这样的商业公司专门做 Nix 企业级支持;连 NixOS 之外的 Linux 发行版(Debian、Fedora、macOS)也纷纷开始集成 Nix。
这篇文章,我们从工程师视角,把 Nix Flakes 彻底讲透:它是什么、为什么解决了传统方案解决不了的问题、怎么用、以及什么时候不该用它。
第一章:Nix 的核心思想——函数式包管理
1.1 传统包管理器的困境
在讲 Nix 之前,先看清楚传统包管理器的问题。
主流的包管理器——apt、yum、brew、pip、npm——都是基于路径的(path-based)。它们把包安装到一个固定的全局位置(比如 /usr/lib、~/.local/lib),包与包之间通过文件系统路径共享状态。
这带来了一个根本性的矛盾:包的安装是幂等的,但卸载不是。
# pip install 同一个包两次,结果是幂等的
pip install requests==2.28.0
pip install requests==2.28.0 # 什么都不发生
# 但卸载后重新安装,不一定等价于从未安装
pip uninstall requests
pip install requests==2.28.0 # 真的和之前一样吗?
原因是:包安装时会产生副作用——写配置文件、创建用户、注册系统服务、修改环境变量。第一次安装留下的痕迹,第二次安装不一定能完美复现。
更严重的是依赖冲突:
项目A 需要 Python 3.9 + Django 3.2
项目B 需要 Python 3.11 + Django 4.2
用传统方案,要么用虚拟环境隔离(venv/conda),要么用容器。但每种方案都有代价:虚拟环境管理复杂、容器构建慢且不可移植(在你的机器上 docker build 成功,在 ARM Mac 上可能失败)。
1.2 Nix 的革命性思路:内容寻址存储
Nix 的核心创新说起来很简单:不是路径寻址,而是内容寻址(content-addressed)。
在 Nix 的世界里,每一个包不是「安装到 /nix/store/xxx-package」,而是通过包的输入内容(源码、依赖、构建脚本)生成一个确定性的哈希,作为包的唯一标识。
# 传统方案:包名 → 路径
requests-2.28.0 → /usr/lib/python3.9/site-packages/requests
# Nix 方案:内容 → 哈希
requests-2.28.0 + 所有依赖 + 构建脚本 → nix-store-id
nix-store-id → /nix/store/gcc7d9...-python3.9-requests-2.28.0
这个哈希是怎么算的?它由包的所有输入内容决定:
- 源码的哈希
- 每个依赖包的哈希
- 构建脚本的哈希
- 环境变量的哈希(部分)
这意味着:给定完全相同的输入,nix-build 永远生成完全相同的输出目录。两个不同的机器上,对同一个 Nix 表达式求值,会得到完全相同的 /nix/store/xxx 路径。
1.3 Nix Store:不可变的事实来源
Nix Store(/nix/store)是 Nix 的核心。它有以下几个关键性质:
不可变性:Store 里的内容一旦写入,永远不修改。你想升级一个包?Nix 会生成一个新的 store path,不会动旧的。这是纯粹的函数式思维——没有副作用。
垃圾回收:因为旧版本永远保留,所以存在空间浪费。Nix 的 GC(nix-collect-garbage)通过追踪「哪些 store path 被当前 profile 引用」,安全地删除无人引用的旧版本。
原子性 profile 切换:Nix 的「当前环境」通过一个 profile(软链接树)来表达。切换环境不需要改 store,只需要改软链接——原子操作,失败回滚。
# 查看当前 profile 指向的 store path
ls -la ~/.nix-profile # → /nix/store/abc123...-python-env
# 切换环境(原子操作)
nix-env --switch-profile /nix/store/def456...-new-python-env
第二章:Flakes——Nix 的声明式复兴
2.1 Flakes 是什么
Nix Flakes 是 Nix 2.0 引入的实验性特性(到 2026 年已成为主流用法),它对 Nix 的包管理做了两件事:
1. 把「整个项目依赖」变成一个原子性的输入图
Flakes 用一个 flake.nix 文件声明项目及其所有依赖。依赖不只是「需要哪些包」,还包括:
- 依赖的 Flake 输入(其他 flake)
- 每个输入的版本/引用(Git SHA、分支、tag)
- 输出规范(要导出哪些 package、devShell、app)
2. 让 Nix 的输入输出完全确定性
Flakes 的核心约束:同样的输入产生同样的输出。Nix 会缓存 flake 的输入(flake.lock),确保每次构建基于完全相同的依赖版本。
2.2 flake.nix——你的项目声明
一个最小的 Flake 长这样:
{
description = "My awesome CLI tool";
# 输入:项目的依赖
inputs = {
# nixpkgs 是 Nix 的包仓库
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
# 用 flake-utils 简化多平台输出
utils.url = "github:numtide/flake-utils";
# 你的项目自己的 flake 输入
# (如果这是个库,其他项目会通过这里引用你)
# 这里先留空
};
outputs = { self, nixpkgs, utils, ... }@inputs:
# flake-utils 的旅用函数:为每个支持的 platform 生成 output
utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs { inherit system; };
in
{
# 默认的包输出
packages.default = self.packages.${system}.my-cli;
# 实际要构建的包
packages.my-cli = pkgs.stdenv.mkDerivation {
pname = "my-cli";
version = "0.1.0";
src = self;
buildInputs = with pkgs; [ go_1_21 ];
buildPhase = ''
go build -o bin/my-cli ./cmd
'';
installPhase = ''
install -Dm755 bin/my-cli $out/bin/my-cli
'';
};
# 开发 shell:进入项目时自动获得的环境
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [
go_1_21
gotools # gofmt, goimports 等工具
golangci-lint
delve # Go 调试器
];
};
# 可执行 app
apps.default = {
type = "app";
program = "${self.packages.${system}.my-cli}/bin/my-cli";
};
}
);
}
flake.nix 是声明,不是脚本——它描述「项目是什么」,而不是「如何构建项目」。
2.3 flake.lock——确定性的锁文件
当你第一次运行 nix flake show 或 nix develop 时,Nix 会自动生成 flake.lock:
{
"version": 7,
"outputs": {
"nixpkgs": {
"owner": "NixOS",
"repo": "nixpkgs",
"branch": "nixos-unstable",
"rev": "abc123...",
"lastModified": 1753401234
},
"utils": {
"owner": "numtide",
"repo": "flake-utils",
"branch": "master",
"rev": "def456...",
"lastModified": 1741234567
}
}
}
flake.lock 类似 package-lock.json 或 go.sum,但有一个关键区别:它锁定的不只是版本号,而是完整的 Git 引用(包括具体的 commit SHA)。这意味着:
- 你可以
git checkout到任何历史 commit,重建当时的完整环境 - 任何两个人在同一个 commit 下,都能得到完全相同的环境
- 即使某个依赖的 Git 仓库被 force push 了,只要你的
flake.lock在,你的环境不变
2.4 nix develop——进入可复现的 shell
这是 Nix Flakes 最杀手级的功能:nix develop。
# 进入项目环境
nix develop
# 自动获得 flake.nix 中定义的 devShells
# 进入后,你的 PATH、CC、LDFLAGS 等全部自动配置好
# 没有任何副作用——退出 shell 后,所有改动消失
❯ nix develop
warning: Nix 2.18 with experimental features enabled
You can use `direnv` to auto-enter this shell.
See https://nix.dev/guides/deploy-software-to-nixos-using-flakes
Learn to use Nix with the Nix manual:
https://nixos.org/manual/nix/stable/
❯ which go
/nix/store/abc...-go-1.21.0/bin/go
❯ which golangci-lint
/nix/store/def...-golangci-lint-v1.60.0/bin/golangci-lint
❯ go version
go version go1.21 linux/amd64
没有 .bashrc 修改、没有 source ~/.venv/bin/activate、没有「我在本地能用 Go 1.23 但 CI 上是 1.21」的问题。
2.5 与 direnv 集成——自动进入环境
每次进目录都要敲 nix develop 还是麻烦。direnv 可以自动拦截 cd,在你进入项目目录时自动加载环境:
# 安装 direnv 和 nix-direnv
nix-env -iA nixpkgs.direnv nixpkgs.nix-direnv
# 在项目目录添加 .envrc
echo "use nix" > .envrc
# 允许 direnv 执行
direnv allow .
# 以后 cd 进入目录,环境自动激活;离开自动卸载
第三章:深度解剖 Flake 机制
3.1 输入系统——不只是 nixpkgs
Flake 的 inputs 可以是:
inputs = {
# GitHub Flake
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
# 指定具体版本
nixpkgs-stable.url = "github:NixOS/nixpkks/nixos-24.05";
# Git 仓库(任意 URL)
my-lib.url = "git+https://github.com/myorg/mylib?ref=main&dir=flake";
# 本地路径(monorepo 中引用同仓库其他模块)
shared-config = ./.config;
# Nix 表达式(通过 tarball URL)
my-patch.url = "https://example.com/patch.tar.gz";
my-patch.sha256 = "0...";
# GitHub 的非 Flake 仓库(通过 flake-compat 兼容层)
legacy-lib = {
url = "github:myorg/legacy-project";
flake = false; # 这不是 Flake,降级为传统 Nix
};
};
输入的传递性很重要:A 项目的 flake 引用了 B 项目的 flake,那么 C 项目引用 A 时,自动获得 B 的完整版本。这就是 Flakes 的依赖图——确定性从依赖传播到传递依赖。
3.2 输出系统——一个 Flake 可以导出什么
outputs = { self, nixpkgs, ... }@inputs:
{
# Packages:可安装的包
packages.x86_64-linux.my-package = ...;
packages.aarch64-darwin.my-package = ...;
# Dev shells:开发者环境
devShells.x86_64-linux.default = ...;
# Apps:可执行程序(给 nix run 用)
apps.x86_64-linux.my-app = {
type = "app";
program = "${self.packages.x86_64-linux.my-package}/bin/my-app";
};
# NixOS modules:NixOS 配置模块
nixosModules.my-module = { config, pkgs, ... }: {
# 定义 NixOS 选项
services.my-service.enable = false;
};
# Home manager modules:用户环境配置
homeManagerModules.my-home = { config, lib, pkgs, ... }: {
home.packages = with pkgs; [ vim git ];
};
# Overlays:nixpkgs 覆盖层(修改包版本/添加包)
overlay = final: prev: {
my-package = final.callPackage ./my-package {};
};
# Hydra(CI)配置
hydraJobs.build = {
type = "jobset";
...
};
};
这就是为什么 Nix Flakes 能做的事远不止「开发环境」——它是一个全栈声明式系统,包、系统配置、用户配置、开发环境、CI 任务,全部是同一套语言的输出。
3.3 模板系统——用 Flake 脚手架新项目
Nix 官方提供了一组 Flake 模板:
# 创建 Go 项目
nix flake new -t github:DeterminateSystems/flake-hub#go ./my-go-project
# 创建 Python 项目
nix flake new -t github:DeterminateSystems/flake-hub#python ./my-py-project
# 创建 Rust 项目
nix flake new -t github:DeterminateSystems/flake-hub#rust ./my-rs-project
# 创建 NixOS 配置
nix flake new -t github:DeterminateSystems/flake-hub#nixosConf ./my-nixos-config
模板本质上就是一个 flake.nix 骨架,省去从零写的麻烦。Determinate Systems 的 flake-hub 提供了大量经过生产验证的模板。
第四章:实际工程落地——多语言项目实践
4.1 Go + Nix:真正的可复现构建
一个真实的 Go 项目 Flake:
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
gomod2nix.url = "github:nix-community/gomod2nix";
# gomod2nix 把 go.mod/go.sum 转换为 Nix 构建
};
outputs = { self, nixpkgs, gomod2nix, ... }@inputs:
let
# 为特定系统构建
forSystems = nixpkgs.lib.genAttrs [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" ];
in
{
packages = forSystems (system:
let
pkgs = import nixpkgs { inherit system; };
gomod2nixPkgs = import gomod2nix { inherit pkgs; };
in
{
# 标准化输出名
default = self.packages.${system}.my-service;
my-service = pkgs.buildGoApplication {
pname = "my-service";
version = "1.0.0";
src = ./.;
# gomod2nix 根据 go.mod 自动生成构建参数
go = pkgs.go_1_22;
modules = gomod2nixPkgs.importTOML ./go.mod;
nativeBuildInputs = with pkgs; [
golangci-lint
mockgen
];
ldflags = [
"-s" "-w"
"-X main.Version=${self.rev or "dev"}"
"-X main.BuildTime=$(date -u '+%Y-%m-%d %H:%M:%S')"
];
};
}
);
# 开发环境
devShells = forSystems (system:
let
pkgs = import nixpkgs { inherit system; };
gomod2nixPkgs = import gomod2nix { inherit pkgs; };
in
{
default = pkgs.mkShell.override { stdenv = pkgs.clangStdenv; } {
nativeBuildInputs = with pkgs; [
go_1_22
gotools
golangci-lint
delve # 调试器
mockgen # mock 生成
golangci-lint
gomod2nixPkgs.tools
];
# Go modules 缓存(避免每次重新下载)
GOMODCACHE = "$HOME/go/pkg/modcache";
};
}
);
};
}
关键点解析:
gomod2nix 是真正的工程级工具。它读取你的 go.mod 和 go.sum,自动生成对应的 Nix 构建表达式。不再需要手动写 buildPhase、installPhase——构建逻辑完全由 Go modules 的内容决定。
# 日常工作流
git clone git@github.com:myorg/my-service.git
cd my-service
nix develop # 立即获得正确的 Go 版本和所有工具
go test ./... # 跑测试,环境完全一致
# 构建生产二进制
nix build # 输出 /nix/store/xxx-my-service
构建出来的二进制文件是静态链接的吗?不完全是——Go 默认动态链接 glibc。但通过 Nix,可以轻松构建完全静态的二进制:
my-service-static = pkgs.buildGoModule {
# ...
ldflags = [ "-extldflags '-static'" ];
# 配合 nixpkgs 的 musl 交叉编译器
};
4.2 Python + Nix:比 conda 更干净的隔离
Python 项目最让人头疼的是「谁动了我的 site-packages」。Nix 的方式是把 Python 环境当成一个整体打包:
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
poetry2nix.url = "github:nix-community/poetry2nix";
poetry2nix.inputs.nixpkgs.follows = "nixpkgs";
};
outputs = { self, nixpkgs, poetry2nix, ... }@inputs:
let
systems = [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" ];
in
{
devShells = nixpkgs.lib.genAttrs systems (system:
let
pkgs = import nixpkgs { inherit system; };
poetry-plugins = poetry2nix.lib.mkPoetry2Nix { inherit pkgs; };
in
pkgs.mkShell {
buildInputs = with pkgs; [
# Python 基础环境
python311
poetry
ruff # Python linter(替代 pylint)
mypy
pytest
pytest-cov
# 数据科学包(如果需要)
numpy
pandas
];
# poetry2nix:从 pyproject.toml 自动导入依赖到 Python 环境
# 注意:这里实际上是把 poetry 的依赖注入到 mkShell 的环境中
# 完整方案需要 poetry2nix 的 mkPoetryEnv
};
);
};
}
但更推荐的方式是使用 poetry2nix 的 mkPoetryEnv——它会把整个虚拟环境打包成 Nix store path,完全隔离且可复现:
poetryEnv = poetry-plugins.mkPoetryEnv {
projectDir = self;
python = pkgs.python311;
# 读取 pyproject.toml,自动处理所有依赖
};
devShells.default = pkgs.mkShell {
buildInputs = [ poetryEnv ];
};
4.3 Node.js + Nix:不只是 npm
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
nodejs-snapshot.url = "github:thomasJM/node2nix-snapshot";
};
outputs = { self, nixpkgs, nodejs-snapshot, ... }@inputs:
let
systems = [ "x86_64-linux" "aarch64-darwin" ];
in
{
devShells = nixpkgs.lib.genAttrs systems (system:
let
pkgs = import nixpkgs { inherit system; };
# node2nix 可以把 package-lock.json 转换为 Nix 构建
in
pkgs.mkShell {
buildInputs = with pkgs; [
nodejs_22
pnpm
# pnpm 比 npm 快 2-3 倍
bun
# bun 同时是包管理器和运行时,比 Node 快
];
shellHook = ''
export NODE_ENV=development
echo "Node.js $(node --version) · pnpm $(pnpm --version)"
'';
}
);
};
}
4.4 多语言 monorepo:一个仓库管理所有技术栈
这是 Flakes 最强大的地方——monorepo 中不同子项目可以用完全不同的技术栈,但共享同一个 Flake:
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
rust-overlay.url = "github:oxalica/rust-overlay";
rust-overlay.inputs.nixpkgs.follows = "nixpkgs";
fenix.url = "github:nix-community/fenix"; # Rust 工具链
fenix.inputs.nixpkgs.follows = "nixpkgs";
};
outputs = { self, nixpkgs, rust-overlay, fenix, ... }@inputs:
let
systems = [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" ];
in
{
# Go 后端服务
packages = nixpkgs.lib.genAttrs systems (system:
let
pkgs = import nixpkgs {
inherit system;
overlays = [ rust-overlay ];
};
in
{
backend = pkgs.buildGoApplication {
src = self;
pname = "backend";
# ...
};
frontend = pkgs.buildNpmPackage {
src = self + "/frontend";
npmDepsHash = "...";
# ...
};
infra = pkgs.writeShellScriptBin "infra" ''
#!/usr/bin/env bash
${pkgs.docker}/bin/docker compose -f ${self}/docker-compose.yml up -d
'';
}
);
# 统一的开发 shell,同时包含所有工具
devShells = nixpkgs.lib.genAttrs systems (system:
let
pkgs = import nixpkgs { inherit system; overlays = [ rust-overlay ]; };
fenixPkgs = fenix.packages.${system};
in
pkgs.mkShell {
buildInputs = with pkgs; [
go_1_22
nodejs_22
docker-compose
kubectl
helm
] ++ (with fenixPkgs; [
# Rust 工具链(通过 fenix 获取,版本锁定)
cargo
rustc
rust-analyzer
clippy
]);
};
);
};
}
第五章:NixOS——用 Flake 管理整个系统
5.1 宣言式系统配置
NixOS 是 Nix 哲学的终极体现:整个操作系统配置都是 Nix 表达式。
# configuration.nix(简化版)
{ config, pkgs, ... }:
{
imports = [
# 导入硬件配置(nixos-generate-config 自动生成)
./hardware-configuration.nix
];
# Flake 风格:把整个系统配置锁定到特定 nixpkgs 版本
nixpkgs.config.allowUnfree = true;
# 用户账户
users.users.sage = {
isNormalUser = true;
description = "Sage";
extraGroups = [ "wheel" "docker" ];
# shell 设为 zsh
shell = pkgs.zsh;
};
# 编程环境
environment.systemPackages = with pkgs; [
git
vim
curl
htop
];
# Docker
virtualisation.docker.enable = true;
# 系统服务
services.nginx = {
enable = true;
recommendedProxySettings = true;
virtualHosts."myapp.dev" = {
locations."/" = {
proxyPass = "http://localhost:8080";
};
};
};
# 时区与 locale
time.timeZone = "Asia/Shanghai";
i18n.defaultLocale = "zh_CN.UTF-8";
# 引导加载
boot.loader.grub.enable = true;
boot.loader.grub.device = "/dev/sda";
}
当你把 configuration.nix 提交到 Git,每次在任意机器上 nixos-rebuild switch,都会得到完全相同的环境——包括系统包、服务配置、用户环境,全部可复现。
5.2 home-manager——用户级配置管理
不只是系统,用户自己的 dotfiles 也可以用 Nix 管理:
# home.nix
{ config, pkgs, ... }:
{
home.username = "sage";
home.homeDirectory = "/home/sage";
# 状态版本(用于迁移)
home.stateVersion = "24.05";
# 编程工具
home.packages = with pkgs; [
# Shell
zsh
starship # 跨平台的 zsh 提示符
# 开发工具
neovim
ripgrep
fd # find 替代品
fzf # 模糊搜索
delta # git diff 增强
# 网络
curl
httpie # HTTP 客户端
wuzz # 交互式 API 测试
];
# Neovim 配置(用 Nix 表达配置)
programs.neovim = {
enable = true;
defaultEditor = true;
plugins = with pkgs.vimPlugins; [
nvim-lspconfig
nvim-cmp
telescope-nvim
nvim-treesitter
];
extraConfig = builtins.readFile ./nvim/init.vim;
};
# Git 配置
programs.git = {
enable = true;
userName = "Sage";
userEmail = "sage@example.com";
aliases = {
co = "checkout";
st = "status";
lg = "log --oneline --graph --decorate";
};
};
# Zsh 配置
programs.zsh = {
enable = true;
oh-my-zsh = {
enable = true;
theme = "robbyrussell";
plugins = [ git docker node npm ];
};
};
# 窗口管理器(如果是 Sway/Hyprland)
programs.sway = {
enable = true;
package = pkgs.sway;
};
}
有了 home-manager,你的 dotfiles 不再是「在各处 clone 然后手动同步」——而是作为 Nix 表达式的一部分,由 Flake 管理,版本控制、复现、迁移全部自动化。
第六章:CI/CD 集成——让矩阵构建真正可复现
6.1 GitHub Actions + Nix
# .github/workflows/build.yml
name: Build & Test
on:
push:
branches: [ main ]
pull_request:
jobs:
test:
strategy:
matrix:
system:
- x86_64-linux
- aarch64-linux
- aarch64-darwin
runs-on: ${{ matrix.system }}
steps:
- uses: actions/checkout@v4
- name: Install Nix
uses: DeterminateSystems/magic-nix-action@main
- name: Setup Flake
run: |
nix flake show # 触发 flake.lock 生成
- name: Run tests
run: |
nix develop . --command go test ./...
build-binary:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: DeterminateSystems/magic-nix-action@main
- name: Build
run: nix build .#packages.x86_64-linux.my-service
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: my-service-linux
path: result/bin/my-service
关键点:矩阵构建的每一行都用了完全相同的 Flake 和 flake.lock,保证了跨平台构建的一致性。
6.2 Cachix——分布式 Nix 缓存
Nix 的构建是纯函数式的——相同的输入永远产生相同的输出。这带来了一个工程优势:构建结果可以被缓存,且缓存永远是正确的(因为哈希保证)。
Cachix 提供了 Nix 的分布式缓存服务:
# 安装 cachix
nix-env -iA nixpkgs.cachix
# 使用公司的 cachix
cachix use mycompany
# 以后任何 nix build / nix develop 的结果都会自动推送到 cachix
# 团队其他成员直接使用缓存,不需要重新构建
在 CI 中使用 Cachix:
- name: Setup Cachix
uses: cachix/cachix-action@v14
with:
name: mycompany
# 从 GitHub Secrets 读取认证 token
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
# CI 构建完成后,结果自动推送到 Cachix
# PR reviewer's 本地 nix develop 时,如果缓存命中,直接用缓存,不需要构建
这使得 CI 的构建时间可以从「从源码重新构建 10 分钟」变成「下载缓存 30 秒」。
6.3 Determinate Nix——企业级 Nix 管理
Determinate Systems 提供了一套企业级工具:
- Nix Daemon:将 Nix 的构建操作集中管理,适合多用户共享构建缓存
- Flake Hubs:托管和分发企业内部的 Flake,版本管理 + 访问控制
- Zero-to-Nix:交互式 Nix 入门教程,帮助新工程师快速上手
对于团队来说,Flake Hubs 解决了「内部包怎么分发」的问题:
# 使用公司 Flake Hub 上的内部库
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
company-lib = {
url = "https://flake.company.com/our-lib";
# 通过 flake hub 认证
authToken = "..."; # 可以从环境变量读取
};
};
第七章:性能与安全深度分析
7.1 构建性能:为什么 Nix 第一次构建慢
Nix 的第一次构建慢,有两个原因:
1. 下载所有源码
Nix 构建包时,需要下载所有源码。相比 apt 直接安装预编译二进制,Nix 从源码构建自然更慢。
但这在 2026 年的网络条件下已经不是问题——千兆宽带下,下载一个 Node.js 源码包也就几秒。
2. 每个依赖独立构建
Nix 的依赖隔离特性意味着每个包都要独立构建,即使多个包依赖同一个 glibc 版本,也要各自构建一遍。
不过 Nix 的 sandboxing 机制(buildFHSUserEnv)可以复用预编译的 FHS 环境,显著减少重复构建。
解决方案:使用 binary cache
# 在 flake.nix 中指定额外的 binary cache
nix.settings.substituters = [
"https://nix-community.cachix.org"
"https://cache.nixos.org"
];
配合 Cachix,团队成员可以共享预构建结果,首次构建后,后续所有人直接用缓存。
7.2 安全模型分析
Nix 的安全模型有几个关键点:
沙箱构建:Nix 默认在沙箱中构建包,网络访问、文件系统访问都受到严格限制。这防止了构建脚本偷偷联网下载恶意代码(supply chain attack)。
# 查看某个包的沙箱配置
nix show-config | grep sandbox
# sandbox = true
# bind-mounted = true
不可变的 Store:/nix/store 是只读的(root 除外),任何包的安装/修改都需要通过 Nix 命令,确保所有变更都被 Nix 追踪。
构建结果可验证:每个 store path 的哈希由其输入决定。如果构建结果被篡改(比如磁盘 bit flip),哈希就会不匹配,Nix 会拒绝使用。
缺陷:Nix 的安全模型在非 NixOS 系统上有一个弱点——root 用户可以绕过沙箱。在 NixOS 上,内核的 namespace 机制保证了更强的隔离。
7.3 何时不用 Nix Flakes
Nix 很强大,但不是银弹。在以下场景中,Nix 可能不是最佳选择:
1. 团队整体技术栈一致且没有复杂依赖
如果你的团队全员用 macOS、用 Homebrew 管理包、没有跨平台构建需求,Docker + Makefile 已经够用。引入 Nix 会带来额外的学习成本。
2. 项目需要特定版本的系统库(非语言级依赖)
Nix 的生态虽然丰富,但不是所有 C/C++ 库都有 nixpkgs 版本。如果你的项目依赖某个只有 apt 能装的系统库,Nix 会增加适配工作量。
3. 对构建速度有极致要求
Nix 从源码构建比 apt 安装预编译二进制慢。对于追求 30 秒内完成 CI 的场景,Nix 的缓存命中前反而是劣势。
4. 团队成员对 Nix 完全陌生
Nix 的学习曲线不低——Nix 语言、NixOS、Flakes 三层概念。如果团队没有时间和意愿学习,强行推广只会适得其反。
建议:从个人开发机器开始,在小项目中试点,验证了效果再逐步推广到团队。
第八章:从零迁移指南
8.1 现有项目的迁移策略
Step 1:安装 Nix
# macOS / Linux
sh <(curl -L https://nixos.org/nix/install)
# 开启 Flakes 和 nix-command(2026 年标准配置)
mkdir -p ~/.config/nix
echo 'experimental-features = nix-command flakes' >> ~/.config/nix/nix.conf
Step 2:初始化 Flake
cd your-project
nix flake init
git add flake.nix flake.lock
Step 3:逐步替换现有工具链
不要一次性迁移所有东西。推荐顺序:
1. 先迁移开发工具(go/python/node 版本)→ nix develop
2. 再迁移构建脚本(makefile / shell scripts)→ nix build
3. 最后迁移系统配置(如果用 NixOS)→ configuration.nix
Step 4:配置 CI
# .github/workflows/nix.yml
name: Nix CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: DeterminateSystems/magic-nix-action@main
- run: nix build
8.2 一个真实迁移案例
一家中型互联网公司的 Node.js 后端团队,5 个服务,共享一个 monorepo:
迁移前:
- 团队成员 Node.js 版本不一致(14/16/18/20 混用)
- CI 上的 Node.js 版本是 16,本地开发用 18,导致线上 bug 本地无法复现
- 每次新人入职,环境配置需要 2-3 天("在我机器上能跑"的调试)
- Docker 镜像里有 6 层,每层 200MB,构建 12 分钟
迁移后:
- 所有服务通过
nix develop获得完全一致的 Node.js 版本(22.4) flake.lock锁定在 Git,每次 git pull 后nix develop自动更新- 新人入职:
git clone && nix develop && npm install,5 分钟完成 - Docker 构建改为
nix build,单层镜像,总大小 80MB,构建 3 分钟 - CI 缓存命中后构建时间 45 秒
迁移工作量:1 个工程师花了 2 周完成了所有服务的 Flake 迁移,CI 集成,团队培训(2 小时 workshop)。
总结:Nix Flakes 的本质价值
回到开头的问题:什么是真正的可复现开发环境?
不是「版本锁定」,不是「虚拟隔离」,不是「容器化」——这些方案都是在问题出现后的补救。Nix Flakes 的思路是从根源上重新设计:通过内容寻址、不可变存储、声明式配置,让「环境」变成一个可以被版本控制、被确定性地重建的东西。
2026 年的今天,Nix 生态已经足够成熟:
- Flakes API 稳定:不再需要
--experimental-features,开箱即用 - 主流语言全覆盖:Go、Rust、Python、Node.js、C++ 全部有成熟的构建方案
- 企业级工具就绪:Cachix、Determinate Nix、Flake Hubs 提供了团队协作所需的一切
- 跨平台:Linux(所有主流发行版)、macOS(Intel + Apple Silicon)、WSL2
Nix 的学习曲线确实存在——Nix 语言本身需要学习、函数式思维需要适应、错误排查需要理解 Nix 的执行模型。但这些投入是值得的:一旦团队掌握了 Nix,「环境不一致」这个问题就从你的工作流中彻底消失了。
真正值得追求的开发体验,是每个工程师 clone 了代码仓库之后,nix develop 一行命令就能得到完全一致的环境——不需要文档、不需要 Wiki、不需要「问一下谁遇到过这个问题」。
这不是理想,这是 Nix Flakes 已经在实现的事实。
参考资源:
- Nix 官方文档
- Zero to Nix - Determinate Systems 交互式教程
- NixOS & Flakes Book - 中文友好的 Flakes 入门
- flakehub.com - 社区 Flake 模板市场
- Cachix - Nix 分布式缓存
- gomod2nix - Go modules → Nix 构建
- poetry2nix - Python Poetry → Nix 构建
本文原创,转载需注明出处:程序员茄子 chenxutan.com