我偏好的 NixOS 安裝方式(宣告式)
原文由 Michael Stapelberg 于 發布,訂閱此部落格
在我的其中一台網路儲存電腦組裝中,我想找 Flatcar Container Linux 的替代方案,於是睽違將近十年後再次嘗試了 NixOS。安裝 NixOS 的方法有很多,在本文中我將說明我偏好如何在實體硬體或虛擬機器上安裝 NixOS:透過網路、以完全宣告式的方式進行。
前言:什麼是宣告式?
宣告式一詞指的是你描述要達成什麼,而不是怎麼做。以 NixOS 來說,這表示你要宣告系統應該包含哪些軟體(加到設定選項 environment.systemPackages,或啟用某個模組),而不是例如執行 apt install。
宣告式做法有個不錯的特性:系統會跟著設定走,所以只要還原設定上的更動,就能乾淨地還原系統上的更動。
我喜歡用版本控制來管理宣告式的設定檔,通常是用 Git。
當初架設現在這台網路儲存主機時,我選擇了 CoreOS(後來變成 Flatcar Container Linux),因為它是一個會自動更新的基礎系統,並可透過宣告式的 cloud-init 設定來配置。
安裝 NixOS 的幾種方式
圖形化安裝程式:僅適用於桌面環境
NixOS 手冊的「Installation」章節介紹了圖形化安裝程式(標示為「for desktop users」,以 Calamares 系統安裝程式為基礎,於 2022 年加入)以及手動安裝程式。
使用圖形化安裝程式很容易就能把 NixOS 安裝到硬碟:只要一路按確認接受預設值,最後就能得到一個可運作的系統。但它也有一些缺點:
- 安裝完成後,你必須手動啟用 SSH——而且得在本地操作,無法透過網路。
- 圖形化安裝程式會幫你產生一份初始的 NixOS 設定,但沒有辦法注入你自己的初始設定。
顯然,圖形化安裝程式並不是為了遠端安裝或自動化安裝而設計的。
手動安裝
相較之下,手動安裝程式對我來說又太過手動:展開 NixOS 手冊「Installation summary」章節中的「Example 2」和「Example 3」就能有個概念。老實說,這些步驟是完全做得到的,但我不想在趕時間時用這種方式安裝系統。一來,手動流程在壓力下很容易出錯。二來,互動式地複製貼上指令,根本就與撰寫宣告式設定檔背道而馳。
網路安裝:nixos-anywhere
理想上,我希望大部分安裝工作都能在自己電腦前舒舒服服地完成,這表示安裝程式必須能透過網路操作。此外,我也希望機器在安裝完成後,能立刻以可運作的初始 NixOS 設定啟動(不需要任何手動步驟!)。
幸好,有一個(由社群提供的)解決方案:nixos-anywhere。你只需要負責啟動 NixOS 安裝程式,接著執行一道指令,nixos-anywhere 就會透過 SSH 連進安裝環境、幫你分割磁碟並將 NixOS 安裝到硬碟上。值得注意的是,nixos-anywhere 本身是以宣告式進行設定的,所以這個步驟隨時都可以重複執行。
(我知道 nixos-anywhere 甚至可以 SSH 進任意系統並透過 kexec 重啟進入 NixOS 安裝程式,這確實是個很酷的炫技手法,但我比較喜歡明確啟動安裝程式的做法,對我來說風險較低,也更通用、更容易重現。)
前置作業:安裝 Nix
我打算在其中一台機器上使用 NixOS,但(目前)不是在我的主力桌機上。
因此,我只在 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 應該會讓你進入一個已安裝 GNU hello 套件的新 shell:
% 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 映像檔寫入 USB 隨身碟並用它來啟動機器(或是選擇 ISO 來啟動虛擬機器)。
但在能透過 SSH 遠端登入之前,我們得先手動設定密碼。我還得用 TERM=xterm 環境變數來 SSH,因為 rxvt-unicode(我慣用的終端機)的 termcap 檔並未包含在預設的 NixOS 安裝環境中。同樣地,我設定的語系也無法使用,而我慣用的 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 的上游 channel:
% export NIX_PATH=nixos-config=$PWD/iso.nix:nixpkgs=channel:nixos-25.05
% nix-build '<nixpkgs/nixos>' -A config.system.build.isoImage在我的2025 年高階 Linux 電腦上大約花了 1.5 分鐘,安裝程式的 ISO 就能在 result/iso/nixos-minimal-25.05.802216.55d1f923c480-x86_64-linux.iso 中找到(在我的案例中大小為 1.46 GB)。
啟用 Nix Flakes
可惜的是,儘管所謂「實驗性」的新命令列介面(CLI)已經推出超過 5 年,nix 專案至今仍未將其預設啟用,所以我們需要自行建立設定檔並啟用現代的 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! ###安裝後的步驟
既然系統中宣告式的部分已經就緒,接下來要處理的是有狀態(stateful)的部分。
以我的情況來說,唯一需要設定的有狀態部分是 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 的優點之一是,你可以在開機選單中選擇要執行哪一代的系統。如果最新的更動造成問題,你可以快速重啟並選擇上一代來還原。當然,你也可以還原設定上的變更並部署新的一代——視當下哪種方式更方便而定。
結論
透過本文,我希望能傳達當初我剛開始使用 Nix 與 NixOS 時,希望有人能告訴我的幾件事:
- 啟用 flakes 與新的 CLI。
- 使用 nixos-anywhere 進行遠端安裝。
- 如果想的話,也可以自製安裝程式,很簡單的!
- 使用
nixos-rebuild內建的--target-host旗標來進行遠端部署。
接下來可以往哪裡去?
- 閱讀 nixos.org → Learn 上的所有文件。
- 以下是我為了尋求靈感/參考而看過的、來自我的社交圈及其周邊人士的幾篇文章,排名不分先後:
- Michael Lynch 寫了關於在 Oracle Cloud 上以 NixOS 建置虛擬機器以及管理他的 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 的話題 :)
隨機一篇部落格
留言
登入後參與討論