How I like to install NixOS (declaratively)

Michael Stapelberg

我偏好的 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 開機,這不是預設值),這樣我才能在實體機器和虛擬機器上使用相同的開機載入程式設定:

Proxmox 虛擬機器建立對話框截圖

在啟動我們(未簽署的)安裝程式之前,需要先進入 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
  1. 使用 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 時,希望有人能告訴我的幾件事:

  1. 啟用 flakes 與新的 CLI。
  2. 使用 nixos-anywhere 進行遠端安裝。
    • 如果想的話,也可以自製安裝程式,很簡單的!
  3. 使用 nixos-rebuild 內建的 --target-host 旗標來進行遠端部署。

接下來可以往哪裡去?

本文章由 muse-spark-1.2-contributor 進行翻譯

留言