How I like to install NixOS (declaratively)

Michael Stapelberg

私なりのNixOSインストール方法(宣言的に)

原文は Michael Stapelberg により に公開されました。 このブログを購読する

あるネットワークストレージ用PCの自作で、Flatcar Container Linuxの代替を探していた際に、ほぼ10年ぶりにNixOSを再び試してみました。NixOSのインストール方法はいくつもありますが、この記事では私が物理マシンや仮想マシンにNixOSをインストールする際に気に入っている方法、すなわちネットワーク経由で完全に宣言的に行う方法を紹介します。

はじめに:宣言的(Declarative)とは?

宣言的(declarative)という用語は、どのように実現するかではなく、何を達成すべきかを記述することを意味します。NixOSで言えば、たとえばapt installを実行する代わりに、システムに含めたいソフトウェアを宣言する(設定オプションenvironment.systemPackagesに追加する、あるいはモジュールを有効化する)ということです。

宣言的アプローチの良いところは、システムが設定に追従することです。そのため、設定の変更を取り消せば、システムへの変更もきれいに元に戻せます。

私は宣言的な設定ファイルをバージョン管理、たいていは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のインストール

私は保有するマシンのうち1台でNixOSを使いたいのですが、(今のところ)メインのデスクトップPCで使うつもりはありません。

そのため、Arch Linuxには(NixOSを動かさなくてもビルドに使える)nixツールだけをインストールしました。

% 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パッケージがインストールされた新しいシェルに入るはずです。

% 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メモリに書き込んでそこからマシンを起動する(あるいはVMでISOを選択して起動する)だけです。

しかし、SSHでリモートログインできるようにする前に、手動でパスワードを設定する必要があります。また、私の愛用する端末であるrxvt-unicodeのtermcapファイルがデフォルトのNixOSインストーラー環境に含まれていないため、TERM=xtermという環境変数を付けてSSHする必要があります。同様に、私が設定しているロケールも使えず、愛用のシェルであるZshも利用できません。

インストーラーがあらかじめ使いやすい環境で設定されていたら、ずっと快適ではないでしょうか。

DebianやFedora、Arch Linuxといった他の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-build(1)iso.nixファイルを参照し、NixOS 25.05のアップストリームチャネルを選択するように、環境変数NIX_PATHを設定しました。

% 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分半で、インストーラーISOがresult/iso/nixos-minimal-25.05.802216.55d1f923c480-x86_64-linux.isoに生成されました(私の場合、サイズは1.46 GBでした)。

Nix Flakesを有効化する

残念ながら、nixプロジェクトは5年以上も利用可能でありながら、いまだに「experimental」な新しいコマンドラインインターフェース(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 shellnix-shellではなく)を実行します。

% nix shell nixpkgs#hello
/tmp 2 % hello
Hello, world!

(再)インストール手順

参考までに、ProxmoxでNixOS用の新しいVMを作成する際に私が使っている設定を紹介します。最も重要な設定はbios=ovmf(=UEFIブート、デフォルトではありません)で、これにより物理マシンとVMで同じブートローダー設定を使えるようになります。

ProxmoxのVM作成ダイアログのスクリーンショット

(署名されていない)インストーラーを起動する前に、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 = "/";
              };
            };
          };
        };
      };
    };
  };
}

希望するNixOS設定をconfiguration.nixで宣言します。

{ 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]

約1分後、VMのインストールが完了し再起動されました!

完全な 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のメッシュ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を使い始めたときに誰かに教えてほしかったことを伝えられていれば幸いです。

  1. flakesと新しいCLIを有効化する。
  2. リモートインストールにはnixos-anywhereを使う。
    • 必要ならカスタムインストーラーをビルドする。簡単です!
  3. リモートデプロイにはnixos-rebuildに組み込まれている--target-hostフラグを使う。

ここからどこへ進むべきでしょうか?

この記事は「muse-spark-1.2-contributor」を使用して翻訳されました。

コメント