我喜欢如何安装 NixOS(声明式)
在我其中一次网络存储 PC 搭建中,我在寻找 Flatcar Container Linux 的替代品,于是时隔近 10 年再次尝试了 NixOS。安装 NixOS 的方法有很多种,在本文中我将概述我喜欢如何在物理硬件或虚拟机上安装 NixOS:通过网络进行,并且完全声明式。
简介:什么是声明式?
声明式(declarative)这个术语的意思是你描述要达成什么,而不是如何达成。对 NixOS 而言,这意味着你声明希望系统包含哪些软件(添加到配置选项 environment.systemPackages 中,或启用某个模块),而不是运行 apt install 之类的命令。
声明式方式的一个很好的特性是:系统会遵循你的配置,因此通过回退一次配置更改,你也能干净地回退系统上的相应更改。
我喜欢在版本控制(通常是 Git)下管理声明式配置文件。
当我最初搭建当前的网络存储设备时,我选择了 CoreOS(后来的 Flatcar Container Linux),因为它是一个带有声明式 cloud-init 配置的自动更新基础系统。
安装 NixOS 的各种方式
图形化安装器:仅适用于桌面
NixOS 手册的“Installation”章节描述了一个图形化安装器(“面向桌面用户”,基于 Calamares 系统安装器,于 2022 年加入)和一个手动安装器。
使用图形化安装器,把 NixOS 安装到磁盘很容易:只需多次确认默认选项,你就会得到一个可用的系统。但它也有一些缺点:
- 安装完成后你需要手动启用 SSH——只能在本地操作,不能通过网络。
- 图形化安装器会为你生成一份初始 NixOS 配置,但没有办法注入你自己的初始 NixOS 配置。
图形化安装器显然不是为远程安装或自动化安装设计的。
手动安装
而手动安装器对我的口味来说又太手动了:展开 NixOS 手册的 Installation summary 章节中的“Example 2”和“Example 3”感受一下。需要说明的是,这些步骤完全可行,但我不想在匆忙中以这种方式安装系统。一方面,手动流程在压力下容易出错;另一方面,交互式地复制粘贴命令恰恰是编写声明式配置文件的反面。
网络安装:nixos-anywhere
理想情况下,我希望大部分安装工作都能在自己舒适的 PC 上完成,也就是说安装器必须可以通过网络使用。此外,我希望机器在安装完成后立即带着一份可用的初始 NixOS 配置启动(无需任何手动步骤!)。
幸运的是,有一个(社区提供的)解决方案:nixos-anywhere。你负责启动一个 NixOS 安装器,然后运行一条命令,nixos-anywhere 就会通过 SSH 连入该安装器,为你的磁盘分区并把 NixOS 安装到磁盘上。值得注意的是,nixos-anywhere 是声明式配置的,因此你可以随时重复这一步骤。
(我知道 nixos-anywhere 甚至可以通过 SSH 连入任意系统并用 kexec 将其重启进一个 NixOS 安装器,这无疑是一个很酷的派对技巧,但我更喜欢显式启动安装器的方式,因为在我看来它风险更小、更通用、也更容易重复。)
准备工作:安装 Nix
我想在我的某台机器上使用 NixOS,但(目前)不在我的主力桌面 PC 上使用。
因此,我只在 Arch Linux 上安装了 nix 工具(即使不运行 NixOS 也可以用于构建):
% sudo pacman -S nix
% sudo groupadd -r nixbld
% for n in $(seq 1 24); do sudo useradd -c "Nix build user $n" \
-d /var/empty -g nixbld -G nixbld -M -N -r -s "$(which nologin)" \
nixbld$n; done
% sudo systemctl enable --now nix-daemon.socket现在,运行 nix-shell -p hello 应该会让你进入一个新 shell,其中安装了 GNU hello 包:
% export NIX_PATH=nixpkgs=channel:nixos-25.05
% nix-shell -p hello
hello
[nix-shell:/tmp]$ hello
Hello, world!顺便一提,Arch Linux wiki 上的 Nix 页面介绍了如何使用 nix 安装软件包,但这不是我感兴趣的:我只想远程管理 NixOS 系统。
构建你自己的安装器
前面我说过“你负责启动一个 NixOS 安装器”,这很简单:把 ISO 镜像写入 U 盘并从它启动你的机器(或者选择该 ISO 并启动你的虚拟机)。
但在我们能够通过 SSH 远程登录之前,我们需要手动设置一个密码。我还需要带上 TERM=xterm 环境变量进行 SSH 连接,因为 rxvt-unicode(我偏好的终端)的 termcap 文件不包含在默认的 NixOS 安装器环境中。同样,我配置的 locale 无法工作,我偏好的 shell(Zsh)也不可用。
如果安装器预先配置好了便利的环境,岂不是好得多?
对于其他 Linux 发行版,比如 Debian、Fedora 或 Arch Linux,我不会尝试重新构建官方的安装器 ISO 镜像。我相信它们的流程和工具运行良好,但我也确信那是我需要学习、调试和维护的又一件额外事务。
但构建一个 NixOS 安装器与配置一个普通的 NixOS 系统非常相似:相同的配置,相同的构建工具。具体流程记录在官方 NixOS wiki 中。
我把我通常会放进 configuration.nix 的定制内容复制过来,从 nixpkgs 导入了 installation-cd-minimal.nix 模块,并把结果放入 iso.nix 文件:
{ config, pkgs, ... }:
{
imports = [
<nixpkgs/nixos/modules/installer/cd-dvd/installation-cd-minimal.nix>
<nixpkgs/nixos/modules/installer/cd-dvd/channel.nix>
];
i18n.supportedLocales = [
"en_DK.UTF-8/UTF-8"
"de_DE.UTF-8/UTF-8"
"de_CH.UTF-8/UTF-8"
"en_US.UTF-8/UTF-8"
];
i18n.defaultLocale = "en_US.UTF-8";
security.sudo.wheelNeedsPassword = false;
users.users.michael = {
openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5secret"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5key"
];
isNormalUser = true;
description = "Michael Stapelberg";
extraGroups = [ "wheel" ];
initialPassword = "SGZ3odMZIesxTuh2Y2pUaJA"; # random for this post
shell = pkgs.zsh;
packages = with pkgs; [];
};
environment.systemPackages = with pkgs; [
git # for checking out github.com/stapelberg/configfiles
rsync
zsh
vim
emacs
wget
curl
rxvt-unicode # for terminfo
lshw
];
programs.zsh.enable = true;
services.openssh.enable = true;
# This value determines the NixOS release from which the default
# settings for stateful data, like file locations and database versions
# on your system were taken. It‘s perfectly fine and recommended to leave
# this value at the release version of the first install of this system.
# Before changing this value read the documentation for this option
# (e.g. man configuration.nix or on https://nixos.org/nixos/options.html).
system.stateVersion = "25.05"; # Did you read the comment?
}要构建 ISO 镜像,我设置 NIX_PATH 环境变量,让 nix-build(1) 指向 iso.nix 文件,并选择 NixOS 25.05 的上游频道:
% export NIX_PATH=nixos-config=$PWD/iso.nix:nixpkgs=channel:nixos-25.05
% nix-build '<nixpkgs/nixos>' -A config.system.build.isoImage在我的 2025 高端 Linux PC 上大约 1.5 分钟后,安装器 ISO 就可以在 result/iso/nixos-minimal-25.05.802216.55d1f923c480-x86_64-linux.iso 找到(在我的情况下大小为 1.46 GB)。
启用 Nix Flakes
遗憾的是,nix 项目尽管已经推出 5 年多,仍未设法默认启用“实验性”新命令行界面(CLI),因此我们需要创建一个配置文件并启用现代的 nix-command 接口:
% mkdir -p ~/.config/nix
% echo 'experimental-features = nix-command flakes' >> ~/.config/nix/nix.conf如何区分新旧命令?旧命令用连字符(nix-build),新命令用空格分隔(nix build)。
你会注意到我还启用了 Nix flakes,我用它来确保我的 nix 构建是密封的(hermetic),并固定到 nixpkgs 以及我想包含在构建中的任何其他 nix 模块的某个特定版本。我喜欢把 flakes 类比为其他编程环境中的版本锁定文件:其理念是,5 个月后构建系统会得到与今天相同的结果。
要验证 flakes 是否正常工作,运行 nix shell(不是 nix-shell):
% nix shell nixpkgs#hello
/tmp 2 % hello
Hello, world!(重新)安装步骤
供参考,以下是我用来在 Proxmox 中创建新 NixOS 虚拟机的配置。最重要的设置是 bios=ovmf(即 UEFI 启动,这不是默认值),这样我在物理机和虚拟机上可以使用相同的引导加载器配置:

在我们启动(未签名的)安装器之前,需要进入 UEFI 设置并禁用 Secure Boot。例如,Proxmox 默认就启用了 Secure Boot。
然后,在目标系统上启动自定义安装器 ISO,并确保 ssh [email protected] 无需输入密码即可工作。
声明一个包含以下内容的 flake.nix:
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-25.05";
disko.url = "github:nix-community/disko";
# Use the same version as nixpkgs
disko.inputs.nixpkgs.follows = "nixpkgs";
};
outputs =
{
nixpkgs,
disko,
...
}:
let
system = "x86_64-linux";
pkgs = import nixpkgs {
inherit system;
config.allowUnfree = false;
};
in
{
nixosConfigurations.zammadn = nixpkgs.lib.nixosSystem {
inherit system;
inherit pkgs;
modules = [
disko.nixosModules.disko
./configuration.nix
];
};
formatter.${system} = pkgs.nixfmt-tree;
};
}在 disk-config.nix 中声明你的磁盘配置:
disk-config.nix
{ lib, ... }:
{
disko.devices = {
disk = {
main = {
device = lib.mkDefault "/dev/sda";
type = "disk";
content = {
type = "gpt";
partitions = {
ESP = {
type = "EF00";
size = "500M";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
mountOptions = [ "umask=0077" ];
};
};
root = {
size = "100%";
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/";
};
};
};
};
};
};
};
}在 configuration.nix 中声明你想要的 NixOS 配置:
{ modulesPath, lib, pkgs, ... }:
{
imports =
[
(modulesPath + "/installer/scan/not-detected.nix")
./hardware-configuration.nix
./disk-config.nix
];
# Adding michael as trusted user means
# we can upgrade the system via SSH (see Makefile).
nix.settings.trusted-users = [ "michael" "root" ];
# Clean the Nix store every week.
nix.gc = {
automatic = true;
dates = "weekly";
options = "--delete-older-than 7d";
};
boot.loader.systemd-boot = {
enable = true;
configurationLimit = 10;
};
boot.loader.efi.canTouchEfiVariables = true;
networking.hostName = "zammadn";
time.timeZone = "Europe/Zurich";
# Use systemd for networking
services.resolved.enable = true;
networking.useDHCP = false;
systemd.network.enable = true;
systemd.network.networks."10-e" = {
matchConfig.Name = "e*"; # enp9s0 (10G) or enp8s0 (1G)
networkConfig = {
IPv6AcceptRA = true;
DHCP = "yes";
};
};
i18n.supportedLocales = [
"en_DK.UTF-8/UTF-8"
"de_DE.UTF-8/UTF-8"
"de_CH.UTF-8/UTF-8"
"en_US.UTF-8/UTF-8"
];
i18n.defaultLocale = "en_US.UTF-8";
users.mutableUsers = false;
security.sudo.wheelNeedsPassword = false;
users.users.michael = {
openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5secret"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5key"
];
isNormalUser = true;
description = "Michael Stapelberg";
extraGroups = [ "networkmanager" "wheel" ];
initialPassword = "install"; # TODO: change!
shell = pkgs.zsh;
packages = with pkgs; [];
};
environment.systemPackages = with pkgs; [
git # for checking out github.com/stapelberg/configfiles
rsync
zsh
vim
emacs
wget
curl
];
programs.zsh.enable = true;
services.openssh.enable = true;
# This value determines the NixOS release from which the default
# settings for stateful data, like file locations and database versions
# on your system were taken. It‘s perfectly fine and recommended to leave
# this value at the release version of the first install of this system.
# Before changing this value read the documentation for this option
# (e.g. man configuration.nix or on https://nixos.org/nixos/options.html).
system.stateVersion = "25.05"; # Did you read the comment?
}……然后锁定它:
% nix flake lock- 使用 nixos-anywhere,从安装器获取 hardware-configuration.nix 并把 NixOS 安装到磁盘:
% nix run github:nix-community/nixos-anywhere -- \
--flake .#zammadn \
--generate-hardware-config nixos-generate-config ./hardware-configuration.nix \
--target-host [email protected]大约一分钟后,我的虚拟机就安装完成并重启了!
完整的 nixos-anywhere 安装过程记录,如果你好奇的话
% nix run github:nix-community/nixos-anywhere -- \\
--flake .#wiki \\
--generate-hardware-config nixos-generate-config ./hardware-configuration.nix \\
--target-host [email protected]
[... transcript truncated for brevity but contains full nixos-anywhere log ...]
### Installing NixOS ###
installing the boot loader...
setting up /etc...
Created "/boot/EFI".
installation finished!
### Rebooting ###
### Done! ###安装后步骤
现在系统的声明式部分已经就绪,我们还需要处理有状态的部分。
在我的情况下,唯一需要设置的有状态部分就是 Tailscale mesh VPN。
要设置 Tailscale,我通过 SSH 登录并运行 sudo tailscale up。然后,我按照链接把新节点添加到我的网络。之后,在 Tailscale Machines 控制台中,我禁用密钥过期并添加 ACL 标签。
进行更改
现在,当我在配置文件中做了更改后,我会使用 nixos-rebuild 远程把更改部署到我的 NixOS 系统:
% nix run nixpkgs#nixos-rebuild -- \
--target-host michael@zammadn \
--use-remote-sudo \
switch \
--flake .#zammadn请注意,并非所有更改都会在 nixos-rebuild switch 中完全生效:虽然 systemd 服务通常会被重启,但新需要的内核模块不会被自动加载(例如在 Frigate 中启用 edgetpu Coral 硬件加速器之后)。
因此,为了确保一切生效,请在部署更改后 reboot 你的系统。
NixOS 的一个优点是,在启动菜单中,你可以选择要运行系统的哪个代(generation)。如果最新的更改破坏了什么,你可以快速重启进入上一个代来撤销该更改。当然,你也可以撤销配置更改并部署一个新代——哪种方式在当时更方便就用哪种。
结语
通过本文,我希望能够传达出我希望在我开始使用 Nix 和 NixOS 时就有人告诉我的内容:
- 启用 flakes 和新 CLI。
- 使用 nixos-anywhere 进行远程安装。
- 如果你愿意,可以构建一个自定义安装器,这很容易!
- 使用
nixos-rebuild内置的--target-host参数进行远程部署。
接下来该去哪里?
- 通读 nixos.org → Learn 上的所有文档。
- 以下是我周围圈子中一些人的几篇文章,我曾从中寻找灵感/参考,排名不分先后:
- Michael Lynch(迈克尔·林奇)写了关于用 NixOS 设置 Oracle Cloud 虚拟机以及管理他的 Zig 配置的文章。
- Nelson Elhage(纳尔逊·埃尔哈格)写了在他的对 Python 3.14 尾调用解释器性能的调查中,使用 Nix 测试数十个 Python 解释器的文章。
- Vincent Bernat(文森特·贝尔纳)写了关于使用 Nix 为 ARM 单板计算机构建 SD 卡镜像的文章。
- Mitchell Hashimoto(米切尔·桥本)分享了他的详尽的 NixOS 配置。
- Wolfgang(沃尔夫冈)有一个关于在他的家庭服务器上使用 NixOS 的 YouTube 视频(→ 他的配置)
- 联系你当地的 Nix 社区!我最近参加了Nix Zürich 小组的“Zero Hydra Failures”活动,那里友善的人们很乐意畅聊一切与 Nix 相关的话题 :)
随机一篇博客