编程 Nix Flakes 深度解析:从「跑在我机器上能工作」到真正的声明式可复现开发环境

2026-07-24 10:15:00 +0800 CST views 8

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 shownix 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.jsongo.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.modgo.sum自动生成对应的 Nix 构建表达式。不再需要手动写 buildPhaseinstallPhase——构建逻辑完全由 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
        };
      );
    };
}

但更推荐的方式是使用 poetry2nixmkPoetryEnv——它会把整个虚拟环境打包成 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 已经在实现的事实。


参考资源


本文原创,转载需注明出处:程序员茄子 chenxutan.com

推荐文章

解决 PHP 中的 HTTP 请求超时问题
2024-11-19 09:10:35 +0800 CST
liunx服务器监控workerman进程守护
2024-11-18 13:28:44 +0800 CST
JavaScript 流程控制
2024-11-19 05:14:38 +0800 CST
2025,重新认识 HTML!
2025-02-07 14:40:00 +0800 CST
在 Vue 3 中如何创建和使用插件?
2024-11-18 13:42:12 +0800 CST
程序员茄子在线接单