nixos-anywhere 远程部署:把裸机变成 NixOS

云主机与托管裸机通常只给一个 root SSH,没有控制台、没有 ISO 挂载权限。nixos-anywhere 用 kexec 把目标机直接引导进 NixOS 安装环境,在内存中执行 disko 分区与 nixos-install,一次命令完成从空盘到可复现系统。本文讲清三段式原理、关键参数、密钥注入、批量流程、CI 自动化与常见失败排查。

引言

托管裸机与云主机通常只提供两样东西:一个 root SSH,以及一块空盘。没有控制台(KVM over IP 要额外申请),没有挂载自定义 ISO 的权限,更不可能抱着 U 盘进机房。传统做法是写一堆一次性脚本:dd 镜像、chroot、手写 GRUB,装完还要祈祷引导项没写错。

nixos-anywhere 走的是另一条路:先把目标机引导进一个临时的 NixOS 安装环境(在内存里运行),再在这个环境里完成分区与系统安装。由于安装环境本身就是 NixOS,分区可以交给 disko 声明式完成,系统安装可以交给 nixos-install,于是「从空盘到可复现系统」收敛成一条命令,并且与本地安装、虚拟机测试共用同一份配置。

本文讲清它的三段式原理、关键参数怎么选、密钥如何安全注入、批量与 CI 场景的组织方式,以及各类失败的定位路径。

前置:NixOS 远程部署工具 、NixOS 密钥管理 。

目录

1. 为什么需要远程安装

1.1 三条可行路径的对比

路径需要什么局限
厂商 ISO 挂载控制台权限 + ISO 上传云上通常不开放,且 ISO 内容不可复现
在现有系统上 nixos-infect目标机已有 Linux依赖既有发行版,残留多、不可复现
kexec 引导 + 安装只需 root SSH需要内核能 kexec 到目标机架构

1.2 为什么 kexec 路径最干净

kexec 的语义:把另一个内核加载进内存并直接启动,跳过固件与引导器
于是目标机当前的系统(Ubuntu/Debian/厂商镜像)在引导后完全消失
后续所有操作都发生在一个"内存中的 NixOS"里
结果:分区与安装过程不依赖原系统,因而完全可复现

1.3 适合与不适合的场景

适合:
  - 云 VPS / 托管裸机,只有 root SSH
  - 需要批量重建同样配置的一批机器
  - 需要 LUKS 加密根分区(安装期必须由安装环境处理)
不适合:
  - 目标机无法 kexec(某些虚拟化平台禁用、或架构不匹配)
  - 内存过小(安装环境跑在 tmpfs 上,通常建议 ≥1G 可用内存)
  - 目标机是 Windows 或需要保留既有系统做双启动

记忆:kexec 路径的价值是「跳过固件与引导器,把目标机直接换成内存中的 NixOS 安装环境」,因此分区与安装完全脱离原系统、可复现;代价是要求能 kexec 且内存足够。

2. 三段式原理

2.1 第一阶段:连接与探测

① 用 SSH 连上目标机(默认 root@target)
② 探测架构(x86_64-linux / aarch64-linux)与内存
③ 上传一个最小引导包(kexec 工具 + 目标内核 + initrd)

2.2 第二阶段:kexec 进入安装环境

④ 目标机执行 kexec,重启进 NixOS 安装环境(运行在 RAM 中)
⑤ 安装环境里启动 sshd,等待控制端连接
⑥ 控制端把 disko 配置、flake 的 store 闭包推送过去

2.3 第三阶段:分区与安装

⑦ 在安装环境里执行 disko(destroy/format/mount)完成分区与加密
⑧ 执行 nixos-install 把系统写进目标盘
⑨ 注入密钥、host key 等额外文件
⑩ 重启进新系统

2.4 关键点:闭包从哪来

安装环境里没有网络保证,因此系统闭包是从控制端(或指定的构建机)推过去的:

# 默认在本地构建并推送
nixos-anywhere --flake .#host --target-host root@203.0.113.10

# 指定在别的机器上构建(本地是 macOS 或算力不足时)
nixos-anywhere --flake .#host --target-host root@203.0.113.10 \
  --build-on remote --build-on-remote-host builder@10.0.0.5

记忆:三段式——SSH 探测上传引导包、kexec 进内存 NixOS、在安装环境里跑 disko + nixos-install;系统闭包由控制端或指定构建机构建后推送,不依赖目标机联网。

3. 最小可用流程

3.1 准备目标机

# 云控制台或厂商面板里重置 root 密码 / 上传 SSH 公钥
ssh-copy-id -i ~/.ssh/id_ed25519.pub root@203.0.113.10
ssh root@203.0.113.10 'uname -m; free -m | head -2'

3.2 写 flake

# flake.nix
{
  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
  inputs.disko.url = "github:nix-community/disko";

  outputs = { self, nixpkgs, disko, ... }: {
    nixosConfigurations.web01 = nixpkgs.lib.nixosSystem {
      system = "x86_64-linux";
      modules = [
        disko.nixosModules.disko
        ./hosts/web01/disko.nix      # 分区方案(见 disko 篇)
        ./hosts/web01/default.nix    # 系统配置
      ];
    };
  };
}

3.3 一条命令安装

nixos-anywhere --flake .#web01 --target-host root@203.0.113.10

3.4 第一次连接

# 安装完成后目标机会重启,SSH host key 会变,需要清理旧记录
ssh-keygen -R 203.0.113.10
ssh root@203.0.113.10

3.5 分阶段执行以便排错

# 只做前两个阶段,装完不重启,便于登进去检查
nixos-anywhere --flake .#web01 --target-host root@203.0.113.10 --phases kexec,disko,install --no-reboot

# 只跑 disko(目标机已在安装环境中)
nixos-anywhere --flake .#web01 --target-host root@203.0.113.10 --phases disko

记忆:最小流程三步——给目标机放 SSH 公钥、写 flake(disko 模块 + 分区方案 + 系统配置)、nixos-anywhere --flake .#host --target-host root@IP;装完记得 ssh-keygen -R 清掉旧 host key。

4. 关键参数详解

4.1 常用参数表

参数作用何时用
--flake .#host指定要安装的 NixOS 配置必填
--target-host root@IP目标机 SSH 地址必填
--build-on remote在远端构建闭包控制端是 macOS / 算力不足
--disk-encryption-keys推送 LUKS 密钥到安装环境加密根分区
--extra-files安装后注入文件(host key、authorized_keys)需要保留身份
--phases只执行指定阶段排错与增量重跑
--no-reboot安装后不重启登进去检查
--kexec <path>使用自定义 kexec 镜像特殊硬件 / 私有内核
--store-paths直接指定要推送的 store 路径CI 里已构建好闭包
--ssh-option透传 SSH 选项跳板机、非标端口

4.2 阶段名称与顺序

kexec  → disko  → install
  kexec : 上传引导包并在目标机执行 kexec
  disko : 在安装环境里按 disko 配置分区、格式化、挂载
  install : 执行 nixos-install 并注入 extra-files,然后重启

4.3 用 –store-paths 跳过重复构建

# CI 里先构建好,再安装时直接推 store 路径,避免二次构建
nixos-anywhere --flake .#web01 --target-host root@203.0.113.10 \
  --store-paths "$(nix build .#nixosConfigurations.web01.config.system.build.toplevel --print-out-paths)"

记忆:参数记住四条——--build-on remote 解决控制端架构不匹配、--disk-encryption-keys 给 LUKS 送密钥、--phases 分阶段排错、--store-paths 在 CI 里复用已构建闭包;跳板机与非标端口用 --ssh-option 透传给 ssh。

5. 密钥与凭据注入

5.1 LUKS 密钥

# 生成一次性密钥,安装期用它加密根分区,装完立刻删除
head -c 64 /dev/urandom > /tmp/luks.key
nixos-anywhere --flake .#web01 --target-host root@203.0.113.10 \
  --disk-encryption-keys /tmp/luks.key
rm -f /tmp/luks.key

5.2 注入 host key 与 authorized_keys

# --extra-files 指向一个目录,安装后按相对路径复制到目标系统根
mkdir -p extra/etc/ssh extra/root/.ssh
cp /etc/ssh/ssh_host_ed25519_key* extra/etc/ssh/
cp ~/.ssh/authorized_keys extra/root/.ssh/authorized_keys
chmod 600 extra/etc/ssh/ssh_host_ed25519_key

nixos-anywhere --flake .#web01 --target-host root@203.0.113.10 \
  --extra-files ./extra

注入 host key 的价值:重启后 SSH host key 不变,自动化脚本无需 ssh-keygen -R,也避免首次连接的主机身份校验中断。

5.3 声明式密钥的分工

nixos-anywhere 负责"把系统装上",不负责长期密钥管理
长期密钥(服务凭据、TLS 私钥、API token)应走 sops-nix / agenix:
  - 加密文件进 git
  - 解密密钥在机器上就位(可用 --extra-files 注入一次)
  - 由 systemd 服务在启动时解密
见本专题的密钥管理篇。

5.4 不落盘原则

- LUKS 密钥用临时文件,安装后立即删除,绝不写进 flake 或 store
- 不要把私钥作为 --extra-files 的一部分长期留在目标机(除 host key 与公钥外)
- CI 里用 secret 注入,不要 echo 到日志

记忆:密钥注入三条——LUKS 用一次性临时文件并在装后删除、用 --extra-files 注入 host key 避免 host key 变化、长期密钥交给 sops-nix/agenix 而不是安装工具。

6. 目标机的 flake 结构

6.1 推荐目录布局

flake.nix
hosts/
  web01/
    default.nix        # 系统配置(模块化,引用 common/)
    disko.nix          # 分区方案
    hardware.nix       # 生成的硬件配置(可选)
modules/
  common.nix           # 所有机器共享:时区、SSH、用户、防火墙

6.2 硬件配置从哪来

# 方式一:安装时让工具生成(在安装环境里探测)
nixos-anywhere --flake .#web01 --target-host root@IP --generate-hardware-config nixos-generate-config ./hosts/web01/hardware.nix

# 方式二:先装一次最小系统,登进去执行 nixos-generate-config 再合并
# 方式三:云主机场景下磁盘与网卡都很标准,可以手写最小配置

6.3 用同一份配置覆盖本地安装与测试

同一份 hosts/web01 配置:
  - nixos-anywhere 用于远程裸机
  - disko --mode destroy,format,mount 用于本地 U 盘安装
  - runNixOSTest + emptyDiskImages 用于 CI 验证
三者共用 disko.nix 与 default.nix,因此不存在"文档与实物不符"的问题。

6.4 最小系统配置示例

# hosts/web01/default.nix
{ config, pkgs, ... }: {
  imports = [ ../../modules/common.nix ];
  networking.hostName = "web01";
  services.openssh = {
    enable = true;
    settings.PermitRootLogin = "prohibit-password";
  };
  users.users.root.openssh.authorizedKeys.keys = [
    "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... admin@laptop"
  ];
  system.stateVersion = "24.11";
}

记忆:目标机 flake 按 hosts/<name>/{default,disko,hardware}.nix 组织,硬件配置可用 --generate-hardware-config 生成;同一份配置同时供远程安装、本地安装与虚拟机测试。

7. 批量与后续维护的分工

7.1 职责划分

工具负责什么什么时候用
nixos-anywhere首次安装(空盘 → NixOS)装机、重装、换盘
deploy-rs / Colmena增量更新(已有系统)日常发版、配置变更
nixos-rebuild –target-host单机手动更新临时修补

把 nixos-anywhere 用在已有系统上等于重装并清空数据,日常更新必须换工具。

7.2 批量安装

# 对每台机器循环执行;先并行探测,再串行安装以免互相抢带宽
for h in web01 web02 web03; do
  ip=$(getent hosts "$h" | awk '{print $1}')
  nixos-anywhere --flake ".#$h" --target-host "root@$ip" --build-on remote \
    --build-on-remote-host builder@10.0.0.5
done

7.3 灰度与回滚

安装层面的回滚 = 重装(代价高,因此安装前必须确认配置正确)
系统层面的回滚 = NixOS 代际回滚(切换 generation,代价低)
建议:新配置先在一台机器上装一遍并观察,再批量推开;
      日常变更走代际回滚路径,不要依赖重装。

记忆:nixos-anywhere 只管首次安装,日常更新交给 deploy-rs/Colmena 或 nixos-rebuild;批量安装先探测再串行执行,回滚走代际而不是重装。

8. 在 CI 中自动化

8.1 流水线骨架

# 伪代码:构建 → 校验 → 安装 → 验证
jobs:
  install:
    steps:
      - run: nix build .#nixosConfigurations.web01.config.system.build.toplevel
      - run: nixos-anywhere --flake .#web01 --target-host root@${{ secrets.TARGET }}
               --store-paths "$(nix path-info -r ./result | grep toplevel | head -1)"
               --extra-files ./ci/extra
      - run: ssh -o StrictHostKeyChecking=yes root@${{ secrets.TARGET }} 'systemctl is-system-running --wait'

8.2 CI 里的三个要点

① 幂等性:CI 重跑意味着重装,因此只在"确认可清空"的机器上启用
② 密钥:LUKS 密钥与 SSH 私钥从 CI secret 注入,用完即弃
③ 验证:安装后用 systemctl is-system-running、ss -tlnp、nixos-rebuild dry-activate 校验

记忆:CI 化三步——先构建出 toplevel 闭包并用 --store-paths 复用、密钥从 secret 注入、安装后用 systemctl is-system-running 与端口检查做验证;安装前务必先 nix build ... --dry-run 求值预检,避免装到一半才发现配置错误。

9. 常见失败与排错

9.1 kexec 失败

症状:目标机执行 kexec 后 SSH 断连且不再回来
原因:平台禁止 kexec、内核与目标架构不匹配、或内存不足
处理:
  - 确认 uname -m 与 flake 的 system 一致
  - 检查可用内存(free -m),安装环境跑在 tmpfs 上,建议 ≥1G
  - 部分平台需要先加载 kexec 内核模块:modprobe kexec

9.2 卡在等待 SSH

症状:kexec 后长时间等待 SSH,最后超时
原因:安装环境网络未起(DHCP 拿不到地址、网卡驱动缺失)
处理:改用带网卡驱动的自定义 kexec 镜像(--kexec),或临时在目标机所在网段抓包确认

9.3 磁盘未被识别

症状:disko 阶段报 device not found
原因:设备路径写成了 /dev/sda,而实际是 /dev/nvme0n1;或 NVMe RAID 模式需要额外驱动
处理:安装环境里 lsblk -f 确认实际路径,改用 by-id

9.4 LUKS 解锁失败

症状:重启后停在等待加密设备的提示
原因:initrd 里声明的设备路径与 disko 实际创建的分区号不一致
处理:blkid 确认分区号,同步 boot.initrd.luks.devices;确认 allowDiscards 一致

9.5 host key 变化导致自动化中断

症状:第二次执行时 ssh 报 REMOTE HOST IDENTIFICATION HAS CHANGED
处理:用 --extra-files 注入固定 host key(推荐);或脚本里 ssh-keygen -R

9.6 构建机与目标机架构不一致

症状:本地是 aarch64 却要装 x86_64 机器,构建极慢或失败
处理:--build-on remote --build-on-remote-host 指定同架构构建机

记忆:六类失败——kexec 被禁或内存不足、安装环境网络不通、设备路径不是 by-id、LUKS 分区号与 initrd 不同步、host key 变化、构建机架构不匹配。

10. 速查表与一句话记忆

阶段命令 / 检查点
准备ssh-copy-id、确认 uname -m 与内存
安装nixos-anywhere --flake .#host --target-host root@IP
加密--disk-encryption-keys /tmp/luks.key 后立即删除
身份--extra-files ./extra 注入 host key
排错--phases kexec,disko,install --no-reboot
验证systemctl is-system-running --wait

一句话记忆:nixos-anywhere 用 kexec 把目标机直接换成内存中的 NixOS 安装环境,再在里面跑 disko 分区与 nixos-install,于是「只有 root SSH + 一块空盘」就能装出完全可复现的系统;参数上记住 --build-on remote 解决架构、--disk-encryption-keys 送 LUKS 密钥、--phases 分阶段排错、--store-paths 复用 CI 闭包;密钥用一次性文件并在装后删除、host key 用 --extra-files 固定;装完立刻用 ssh-keygen -R 清旧记录,日常更新换 deploy-rs/Colmena 而不是重复安装。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「nix」更多文章

  1. Nix 语言服务器与编辑器工具链:补全、格式化与静态检查
  2. Store 垃圾回收与存储优化:gc root、去重与瘦身
  3. 构建沙箱与可复现性:Nix 如何隔离构建过程