How I like to install NixOS (declaratively)

Michael Stapelberg

私が気に入っているNixOSのインストール方法(宣言的に)

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

はじめに:宣言的とは?

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

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

私は宣言的な設定ファイルをバージョン管理、たいていはGitで管理するのが好みです。

現在のネットワークストレージ環境を最初に構築したとき、私はCoreOS(後のFlatcar Container Linux)を選びました。宣言的なcloud-init設定を持つ、自動更新されるベースシステムだったからです。

NixOSのインストール方法

グラフィカルインストーラー:デスクトップ向けのみ

NixOSマニュアルの「Installation」の章では、グラフィカルインストーラー(「デスクトップユーザー向け」、Calamaresシステムインストーラーをベースに2022年に追加されたもの)と、手動インストーラーについて解説されています。

グラフィカルインストーラーを使えば、NixOSをディスクに簡単にインストールできます。デフォルト設定を何度か確認していくだけで、動作するシステムが手に入ります。ただし、いくつか欠点があります。

  • インストール後にSSHを手動で有効化する必要があります──ローカルでの作業が必要で、ネットワーク経由ではできません。
  • グラフィカルインストーラーは初期のNixOS設定を自動生成してくれますが、自分で用意した初期設定を流し込む方法がありません。

グラフィカルインストーラーは、明らかにリモートインストールや自動インストールを想定していません。

手動インストール

一方、手動インストーラーは私には手作業が多すぎます。印象をつかむには、NixOSマニュアルのInstallation summaryの章で「Example 2」や「Example 3」を展開してみてください。手順自体は十分に実行可能ですが、急いでいるときにこの方法でインストールしたいとは思いません。理由のひとつは、手作業の手順は緊張している状況ほどミスを招きやすいからです。また、コマンドを対話的にコピー&ペーストする作業は、宣言的な設定ファイルを書くことと文字通り正反対だからでもあります。

ネットワークインストール:nixos-anywhere

理想を言えば、インストール作業の大半は自分のPCから快適に行いたいと思っています。つまり、インストーラーはネットワーク経由で使える必要があります。また、インストール直後に、最初から動作するNixOS設定でマシンが起動してほしいのです(手作業は一切なしで!)。

幸い、(コミュニティ提供の)解決策があります。nixos-anywhereです。こちらでNixOSインストーラーの起動さえ用意しておけば、あとは1つのコマンドを実行するだけで、nixos-anywhereがそのインストーラーにSSHで接続し、ディスクをパーティション分割してNixOSをディスクにインストールしてくれます。特筆すべきは、nixos-anywhereが宣言的に設定されるため、この手順をいつでも再現できることです。

(nixos-anywhereは任意のシステムにSSHで入り、kexecで再起動してNixOSインストーラーを立ち上げることさえできると知っています。確かに格好いい芸当ですが、私はあえてインストーラーを明示的に起動する方が、リスクが少なく、より汎用的で再現性も高いと感じて好みです。)

準備:Nixのインストール

私は保有するマシンの1台で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を実行すると、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メモリに書き込んでそこからマシンを起動する(あるいはISOを選択してVMを起動する)だけです。

しかし、SSHでリモートログインする前に、手動でパスワードを設定する必要があります。また、私の場合はTERM=xtermという環境変数を付けてSSHする必要があります。愛用している端末であるrxvt-unicodeのtermcapファイルが、デフォルトのNixOSインストーラー環境に含まれていないからです。同様に、設定しているロケールも効かず、好みのシェル(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_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分半で、インストーラー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のインストールが完了して再起動されました!

Full nixos-anywhere installation transcript, if you’re curious
% 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フラグを使う。

ここから先はどうすればよいでしょうか。

原文は Michael Stapelberg により に公開されました。

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