Chapter 09

Nix 在 CI/CD 中的应用

用 Nix 构建可复现的 CI 环境、构建 Docker 镜像,无需 Dockerfile 也无需担心缓存失效

为什么 Nix + CI/CD 是绝配?

CI/CD 面临的核心挑战与 Nix 的优势完美对齐:

构建可复现性
传统 CI 中,apt install 的结果随时间变化,导致"今天能过 CI 明天不行"。Nix + flake.lock 确保相同代码永远产生相同构建结果。
环境一致性
开发机、CI 机、生产服务器使用完全相同的 derivation,消除"在我本地没问题"的问题类别。
增量缓存
Nix 的内容寻址缓存使得只有真正变化的部分需要重新构建。Cachix 等二进制缓存服务让团队共享构建结果,CI 速度大幅提升。

GitHub Actions + Nix

使用 cachix/install-nix-action 在 GitHub Actions 中安装 Nix:

# .github/workflows/ci.yml
name: CI
on:
  push:
    branches: [main]
  pull_request:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      # 1. 检出代码
      - uses: actions/checkout@v4

      # 2. 安装 Nix(Determinate Systems 的 Action,推荐)
      - uses: DeterminateSystems/nix-installer-action@main
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}

      # 3. 配置 Cachix 二进制缓存(可选但强烈推荐)
      - uses: cachix/cachix-action@v14
        with:
          name: my-cache             # 你的 Cachix cache 名称
          authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}

      # 4. 检查 flake
      - name: Check flake
        run: nix flake check

      # 5. 构建
      - name: Build
        run: nix build

      # 6. 运行测试
      - name: Test
        run: nix develop --command npm test

使用 nix develop 运行 CI 命令

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: DeterminateSystems/nix-installer-action@main
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}

      - name: Run tests in dev environment
        run: |
          nix develop .#ci --command sh -c "
            cargo build --release
            cargo test
            cargo clippy -- -D warnings
          "

Cachix — 二进制缓存服务

Cachix 是专为 Nix 设计的二进制缓存托管服务。上传构建结果后,团队成员和 CI 可以直接下载,无需重新构建:

# 安装 cachix CLI
nix profile install nixpkgs#cachix

# 登录(获取 API token)
cachix authtoken <YOUR_TOKEN>

# 创建新的 cache
cachix create my-project-cache

# 使用 cache(添加到 Nix 配置)
cachix use my-project-cache
# 这会在 /etc/nix/nix.conf(或 ~/.config/nix/nix.conf)中添加:
# substituters = https://cache.nixos.org https://my-project-cache.cachix.org
# trusted-public-keys = cache.nixos.org-1:... my-project-cache.cachix.org-1:...

# 构建并推送到 cache
nix build .#my-app
cachix push my-project-cache ./result

在 NixOS 中配置二进制缓存

{
  nix.settings = {
    substituters = [
      "https://cache.nixos.org"
      "https://nix-community.cachix.org"  # nix-community 工具
      "https://my-project.cachix.org"      # 你的项目缓存
    ];
    trusted-public-keys = [
      "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
      "nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCUSeBc="
      "my-project.cachix.org-1:xxxx..."
    ];
  };
}

nix build 构建产物

# 构建默认包
nix build

# 构建指定包
nix build .#my-app

# 构建并输出到指定路径(不使用 result 符号链接)
nix build .#my-app --out-link /tmp/my-app-build

# 不创建 result 链接(CI 中常用,避免 result 成为 GC root)
nix build .#my-app --no-link

# 查看构建产物的 store 路径
nix build .#my-app --print-out-paths

# 构建所有支持的平台(交叉编译需要额外配置)
nix build .#packages.x86_64-linux.my-app
nix build .#packages.aarch64-linux.my-app

nix flake check — 静态检查

# 检查 flake 的所有 outputs 是否有效
nix flake check

# 只检查,不构建(更快)
nix flake check --no-build

# 检查包括:
# - outputs 结构是否符合 Nix 规范
# - packages.* 是否可以求值(不一定构建)
# - NixOS 配置是否能求值(nixosConfigurations.*)
# - devShells.* 是否有效

# 检查 Nix 代码格式(nixpkgs-fmt)
nix develop --command nixpkgs-fmt --check .

用 Nix 构建 Docker 镜像

pkgs.dockerTools 可以不需要 Dockerfile,直接用 Nix 构建 Docker 镜像,结果完全可复现:

# flake.nix 中定义 Docker 镜像
{ self, nixpkgs, ... }:
let
  pkgs = nixpkgs.legacyPackages."x86_64-linux";
  myApp = self.packages."x86_64-linux".my-app;
in {
  packages."x86_64-linux".docker-image = pkgs.dockerTools.buildImage {
    name = "my-app";
    tag = "latest";

    # 只包含必要的文件(比 FROM ubuntu 的镜像小 10 倍以上)
    contents = [
      myApp
      pkgs.busybox    # 基础工具(可选)
      pkgs.cacert     # TLS 证书(HTTPS 必需)
    ];

    # 等价于 Dockerfile 的 CMD
    config = {
      Cmd = [ "${myApp}/bin/my-app" ];
      Env = [ "NODE_ENV=production" ];
      ExposedPorts = { "3000/tcp" = {}; };
    };
  };

  # 流式镜像(更高效,适合大镜像)
  packages."x86_64-linux".docker-stream = pkgs.dockerTools.streamLayeredImage {
    name = "my-app";
    tag = "latest";
    contents = [ myApp pkgs.cacert ];
    config.Cmd = [ "${myApp}/bin/my-app" ];
  };
}
# 构建 Docker 镜像(输出到 /nix/store)
nix build .#docker-image

# 将镜像加载到 Docker
docker load < result

# 验证镜像
docker images | grep my-app
docker run --rm my-app:latest

完整 CI/CD 实战:构建并推送 Docker 镜像

# .github/workflows/docker.yml
name: Build and Push Docker Image
on:
  push:
    tags: ['v*']

jobs:
  docker:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - uses: actions/checkout@v4

      - uses: DeterminateSystems/nix-installer-action@main
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}

      - uses: cachix/cachix-action@v14
        with:
          name: my-project
          authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}

      - name: Login to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build Docker image with Nix
        run: nix build .#docker-image

      - name: Push to registry
        run: |
          docker load < result
          docker tag my-app:latest ghcr.io/${{ github.repository }}:${{ github.ref_name }}
          docker push ghcr.io/${{ github.repository }}:${{ github.ref_name }}
Nix 构建 vs Dockerfile 构建

Nix 构建的 Docker 镜像通常比传统 Dockerfile 构建的小 3-10 倍,因为只包含精确声明的依赖,没有 apt 缓存、构建工具等无用文件。同时构建完全可复现,不受构建时间和网络环境影响。对于 Rust/Go 等静态链接语言,最终镜像可以小到 10-20 MB。

本章小结

Nix 在 CI/CD 中的核心价值:① 用 flake.lock 确保 CI 构建永远可复现;② 用 nix develop --command 保证 CI 和本地开发环境完全一致;③ 用 Cachix 共享二进制缓存加速构建;④ 用 dockerTools 无需 Dockerfile 就能构建更小、更安全的 Docker 镜像。