もっと小さいNixOS ISOは作れるのか?
(natkr.com)- NixOSは設定だけでVMやISOを作りやすいが、最小に近いライブイメージでも最初から 458MiB あり、AlpineのVM ISO約 66MiB とは大きな差があった
- サイズの大半は nix-store.squashfs が占めており、その中にはPython 3.13.13、Linux modules、systemd、Perl、GRUB、ドキュメント、Nix関連の依存関係が入っていた
nix.enable = false,documentation.enable = false,register-nix-pathsの削除を経て、ISOは 458MiB → 384MiB → 360MiB まで縮小し、Boost依存も外れた- OpenSSHクライアント、デフォルトパッケージ、GRUBインストールツール、ランタイムのカーネルモジュール、Perlベースのアクティベーション経路まで取り除き、最終サイズは 183MiB まで下がった
- 小さな実験用ブートイメージには参考になるが、多くの機能を削っているため、デスクトップや重要な環境にそのまま使うのは難しい
NixOS設定からISOを作る
- NixOS は設定をもとにVMを簡単に作成できる
nixos-rebuild build-vmは現在のシステム設定のVMを生成するpkgs.nixosを使えば、システム設定でなくても任意の設定でVMを作れる
- 基本例では
system.stateVersion = "26.05"とservices.getty.autologinUser = "root"だけを置いた最小VMを作成する - このVMは thin VM として動作する
- ディスクイメージにはVM内で直接作成したファイルだけが入る
/nix/storeなど残りはホストOSからマウントされる
- ホストにNixがない場合や、リモートホスト、一般的なハイパーバイザーで実行するには 自己完結型ISO が必要になる
- NixOSの
iso-image.nixモジュールを import するとISOをビルドできるimage.baseName = lib.mkForce "nixos"で出力ISO名を指定する- 実行例は
qemu-system-x86_64 --cdrom .../nixos.iso -m 1G --accel kvmの形 - amd64の現代的なLinux環境でなければ、アーキテクチャやアクセラレーション方式の変更が必要なことがある
出発点: 458MiBのISO
- デフォルトのISOビルド結果は 458MiB だった
- このイメージにはまだ
vimすら含まれていない- 起動後に
vimを実行するとcommand not foundになる
- 起動後に
- 比較対象として挙げられたAlpineのVM ISOは約 66MiB
- Damn Small Linuxは、はるかに小さいサイズで完成度の高いデスクトップ環境を提供していた例として登場する
- 目標はDamn Small Linux級に到達することではなく、NixOS ISOをどの程度でも小さくできるかを確かめることにある
ISO内部のサイズ分析
- ISOをマウントして
duで確認すると、サイズは次のように分かれていたnix-store.squashfs: 416MiB- initrd: 26MiB
- kernel: 13MiB
- ISO全体: 458MiB
- サイズの主因は 主要ユーザー空間 である
nix-store.squashfsだった - squashfsをマウントすると、Nix storeのように見えるパスが入っていた
python3-3.13.13: 128MiBlinux-6.18.35-modules: 144MiBsystemd-260.1: 60MiBperl-5.42.0: 56MiBgrub-2.12: 複数項目の合算で約 62MiBnix-manual-2.34.7,nixos-manual-htmlなどのドキュメントも含まれていた
- ISOはホスト上でビルドされるため、ISO内のstoreパスはホストの
/nix/storeでも追跡できる nix why-dependsで依存関係の出どころを確認した- BoostはNix daemonの経路から入っていた
nix-daemon.conf,nix,libnixutil.soを経由してboost-1.89.0に到達した
Nixとドキュメントの削除
nix.enable = falseでNix自体をイメージから外そうとしたdocumentation.enable = falseでドキュメントも無効化した- 最初の結果は 458MiB → 384MiB だった
- しかしBoostはまだ残っていた
register-nix-paths.serviceが起動時にISO storeの内容を登録しようとしていた- この経路が再びNixとBoostを引き込んでいた
systemd.services.register-nix-paths = lib.mkForce {}でそのサービスを空にして削除した- その結果ISOは 360MiB になり、
nix why-dependsでもBoost依存がないことを確認できた
OpenSSHとデフォルトパッケージの削除
- 同様の方法で
environment.defaultPackagesも空にできた sshの削除はより厄介だったmodules/programs/ssh.nixがOpenSSHをenvironment.corePackagesに追加していた- これを制御する
programs.ssh.enableのようなオプションは見つからなかった services.openssh.enableはサーバー設定であり、クライアント削除のためのオプションではない
disabledModulesでprograms/ssh.nixを除外することはできたが、他のモジュールがprograms.sshオプションの存在を前提としており、連鎖的にエラーが発生した- 解決策は、
programs.sshオプションを使わない スタブオプション を別モジュールとして用意することだったoptions.programs.ssh = lib.mkOption {};- その後
disabledModules = [ "programs/ssh.nix" ];で実際のSSHモジュールを除外した
- この過程で次の設定も併せて適用した
documentation.man.enable = falsenetworking.firewall.enable = falseenvironment.defaultPackages = lib.mkForce []
NixOSモジュール構造メモ
- NixOSモジュールは大きく3つの部分から成る
- モジュールレベルの項目:
imports,disabledModules - オプション定義:
options.* - 実装:
config.*
- モジュールレベルの項目:
- オプションを定義しないモジュールは、
config.接頭辞なしで実装属性を書く短縮形を使える - 残りの設定でも短縮形を維持するため、
programs.sshのスタブオプションは別の import モジュールに分離した
GRUBインストールツールの削除
- 残っていた大きな項目の1つが約 62MiB のGRUB関連ファイルだった
- ブートローダー自体は必要だが、インストールツールをすべて含める必要はないと判断した
- NixOSのISOプリセットはUEFI版とBIOS版のGRUBを両方バンドルする
- これを無効にする明確なオプションはなかったため、より荒い方法で次の値を再設定した
system.extraDependencies = lib.mkForce []environment.systemPackages = lib.mkForce config.environment.corePackages
environment.systemPackagesを完全に空にはしないbashがないとgettyがcrashloopし続ける可能性があるため、シェルがある程度動くようcorePackagesは残した
カーネルモジュールの削除
linux-6.18.35-modulesは 144MiB で、全体サイズの約4分の1を占めていた- NixOSにはランタイムで使うカーネルモジュールを制限する良いフックは見当たらなかった
- そこでシステム出力から
kernel-modulesフォルダーを削除したsystem.systemBuilderCommands = lib.mkAfter "rm $out/kernel-modules";
- この方法は ランタイムのモジュールロードを事実上無効化 する
- 必要なモジュールは
boot.initrd.kernelModulesまたはavailableKernelModulesに入れておく必要がある
- 必要なモジュールは
- この変更後、より快適なディスプレイ解像度に切り替える機能は失われたが、起動は可能だった
- ISOサイズは 197MiB まで下がった
Perlの削除と実験的な代替機能
- それでもなお 56MiB の Perl が含まれていた
nix why-dependsで確認すると、Perlはシステム有効化中にユーザーと/etcを構成するために使われていた- ユーザー管理と
/etc構成を完全に捨てることはできなかった - そこで実験的機能で既存経路を置き換えた
/etc管理にはoverlay方式を使用- ユーザー管理にはネイティブな userborn を使用
- 適用した設定は次のとおり
system.etc.overlay.enable = truesystem.etc.overlay.mutable = falseservices.userborn.enable = true
- 最終的なISOサイズは 183MiB になった
最終状態と限界
- 出発点の 458MiB から最終的に 183MiB まで減り、元のほぼ3分の1になった
- それでも結果を「良い」と呼ぶのは難しいとしている
- 実際に使うデスクトップや重要な環境には向いていない
- 削除した機能にはすべて存在理由がある
- 小さな実験用ブートイメージが必要で、ごく小さな作業だけを行えばよい場合には参考になる
- 最終設定をそのままコピーして使うと、目的に必要な機能が欠けている可能性がある
さらに縮める余地
- 今回の作業は、「単純に削除できる」か、比較的明確な代替手段がある項目に集中していた
- より深い作業が必要な領域も残っている
- 現在は
systemdMinimalとsystemdの両方がバンドルされている - どちらか片方を外そうとすると別のビルド経路が壊れた
- 現在は
- 小さな項目もまだ削れる可能性があり、合算すれば意味のあるサイズになるかもしれない
- 追加の最適化には、さらに多くの調査と実験が必要になる
1件のコメント
Lobste.rs のコメント
まさにこの用途向けに作られたモジュールがある。かなりのコンパイルは必要だが、NixOS のユーザー空間全体を含む完全に自己完結した initrd を、zstd 圧縮で約 80MiB にできる
この作業は自己完結型 initrd に限らず、どんな NixOS でも容量削減に使える。インストール ISO にもおそらく適用できるはず
https://github.com/wucke13/minimal-nixos/
TinyCore Linux の基本システムは 17MB の Core
X と FLTK/FLWM まで欲しければ 23MB の TinyCore、さらに多くのウィンドウマネージャやアプリまで欲しければ 248MB の CorePlus がある
http://www.tinycorelinux.net/downloads.html
NixCon での、NixOS を Yocto の代替として小さくまとめた発表を勧める: https://youtu.be/AsXY61laNb8
期待したほど詳しくはなかったが、カンファレンスで Óli と Matthew から直接聞いた話は素晴らしかった。まとめ記事があるのか気になる
NixOS で小さなインストール構成を作るのは、いつも少しもどかしい
SSH 周りのサイズは次の設定で減らせそう
"${nixpkgs}/nixos/modules/profiles/minimal.nix"を import することもできる。そこには記事で行っていた最適化の一部が入っているそれでも、たいていの場合はこちらのほうが合理的である可能性が高い
"${nixpkgs}/nixos/modules/profiles/minimal.nix"は以前に見て期待ほどではないと感じていたので、調査を始めたときには含めようと思わなかった。後から思い出したときにはもう半分ほど進んでいて、本来入れるべき初期段階に後付けするのは少し誠実でない気がした最近のシステムでは Perl が引き込まれる場面が多すぎて妙だ。小さな ISO にも Perl が入るし、何かまともなものを最初からコンパイルしようとしても openssl -> Perl となる
読む前から、誰かが C に書き直していない間抜けな Perl スクリプトが原因だろうと思っていた
修正: やはりその通りだった
NixOS は 26.05 からデフォルトの initrd で systemd を使う。現代の OS がサポートすべき initrd のユースケース が多いためだ
systemdMinimalは、より少ないフラグと依存関係でビルドした systemd バイナリなので、initrd をより小さく保つのに役立つただし最小 ISO が目標なら、両方を同じバイナリに依存させることもできそうだ