引言
托管裸机与云主机通常只提供两样东西:一个 root SSH,以及一块空盘。没有控制台(KVM over IP 要额外申请),没有挂载自定义 ISO 的权限,更不可能抱着 U 盘进机房。传统做法是写一堆一次性脚本:dd 镜像、chroot、手写 GRUB,装完还要祈祷引导项没写错。
nixos-anywhere 走的是另一条路:先把目标机引导进一个临时的 NixOS 安装环境(在内存里运行),再在这个环境里完成分区与系统安装。由于安装环境本身就是 NixOS,分区可以交给 disko 声明式完成,系统安装可以交给 nixos-install,于是「从空盘到可复现系统」收敛成一条命令,并且与本地安装、虚拟机测试共用同一份配置。
本文讲清它的三段式原理、关键参数怎么选、密钥如何安全注入、批量与 CI 场景的组织方式,以及各类失败的定位路径。
前置:NixOS 远程部署工具 、NixOS 密钥管理 。
目录
- 1. 为什么需要远程安装
- 2. 三段式原理
- 3. 最小可用流程
- 4. 关键参数详解
- 5. 密钥与凭据注入
- 6. 目标机的 flake 结构
- 7. 批量与后续维护的分工
- 8. 在 CI 中自动化
- 9. 常见失败与排错
- 10. 速查表与一句话记忆
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 而不是重复安装。
延伸阅读
- NixOS 远程部署工具
- NixOS 密钥管理实战
- NixOS 网络与防火墙
- NixOS 运维实战
- 部署策略与发布模式 — 灰度与回滚的工程实践
- Linux 引导故障排查 — 引导项与 initrd 问题定位
- nixos-anywhere 项目仓库
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。