能给我更小的NixOS ISO吗?

Lobsters Hottest 工具

摘要

关于构建最小化NixOS ISO并缩小其大小的指南,包含与Alpine Linux的对比以及逐步优化技巧。

<p><a href="https://lobste.rs/s/nvfvjt/i_can_haz_smoller_nixos_isos">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/06/20 14:31

# 我能拥有更小的 NixOS ISO 吗?| natkr 的碎碎念 来源:https://natkr.com/2026-06-19-nixos-but-smol/ 在这篇文章里,我痛苦地移除了一个 Linux 现场镜像中的功能。就像个正常人一样。 ## https://natkr.com/2026-06-19-nixos-but-smol/#can-haz-iso但首先,我得先有 ISO NixOS (https://nixos.org/) 最酷的技巧之一,就是你可以轻松拿一份配置,然后把它当成虚拟机 (VM) 来跑。`nixos-rebuild build-vm` 会为你的系统配置生成一个 VM¹ (https://natkr.com/2026-06-19-nixos-but-smol/#fn-1)。但你也可以为任何你想要的配置这么做,不管是不是系统配置都行! ``` let pkgs = import (fetchTarball { url = "https://github.com/NixOS/nixpkgs/archive/567a49d1913ce81ac6e9582e3553dd90a955875f.tar.gz"; sha256 = "1vq77hlx8mi3z03pw2nf6r5h7473r1p9yxyf58ym3fh01zppmfln"; }) {}; in pkgs.nixos { system.stateVersion = "26.05"; services.getty.autologinUser = "root"; } ``` 这就够得到一个最小化的虚拟机了,你可以自己跑起来:`$(nix-build basic-vm.nix --attr vm --log-format bar --no-out-link)/bin/run-nixos-vm` 这会创建一个“瘦”虚拟机:有一个磁盘镜像,但里面只有你进入虚拟机后自己创建的文件。² (https://natkr.com/2026-06-19-nixos-but-smol/#fn-2) 其他所有东西(*咳* `/nix/store`)都是从宿主机挂载进来的。 这样在合适的时候很好用,但有时候我们需要更正常一点的东西。比如能在 libvirt (https://libvirt.org/) ³ (https://natkr.com/2026-06-19-nixos-but-smol/#fn-3) 里直接跑,或者甚至可以发给一个没有 Nix 的远程主机。那台主机甚至可能根本不跑 Linux。靠,可能连个*虚拟机监控器*都没有⁴ 简而言之,我就想要一个该死的 ISO。 幸好 NixOS 在这方面仍然支持我: ``` let pkgs = import (fetchTarball { url = "https://github.com/NixOS/nixpkgs/archive/567a49d1913ce81ac6e9582e3553dd90a955875f.tar.gz"; sha256 = "1vq77hlx8mi3z03pw2nf6r5h7473r1p9yxyf58ym3fh01zppmfln"; }) {}; in pkgs.nixos ({ lib, ... }: { system.stateVersion = "26.05"; services.getty.autologinUser = "root"; imports = [ "${pkgs.path}/nixos/modules/installer/cd-dvd/iso-image.nix" ]; image.baseName = lib.mkForce "nixos"; }) ``` 这次我们需要自己运行 QEMU,但也不算太麻烦:`qemu-system-x86_64 --cdrom $(nix-build basic-iso.nix --attr isoImage --log-format bar --no-out-link)/iso/nixos.iso -m 1G --accel kvm`⁵ (https://natkr.com/2026-06-19-nixos-but-smol/#fn-5) ## https://natkr.com/2026-06-19-nixos-but-smol/#too-big天堂里的麻烦 那么我们现在就搞定了,对吧?但是……我们还没碰到标题里那个*真正的*话题呢!所以,呃,关于这个…… ``` $ ls -lh $(nix-build basic-iso.nix --attr isoImage --log-format bar --no-out-link)/iso/nixos.iso -r--r--r-- 1 root root 458M jan 1 1970 /nix/store/kg8mv6296hbhm8als26r400nj1s7ry1n-nixos.iso/iso/nixos.iso ``` 呃哦。458 MiB‽⁶ (https://natkr.com/2026-06-19-nixos-but-smol/#fn-6) 而且它现在还 *什么* 都不做! ``` [root@nixos:~]# vim -bash: vim: command not found ``` 这比我小时候那个 *Damn Small Linux* (https://www.damnsmalllinux.org/old-index.html) 跑一个令人惊讶的完整桌面环境所需的大小还要大将近 10 倍⁷。我们可能达不到那个水平,但肯定有空间做得更好一点。 对比一下,Alpine 的 VM ISO (https://dl-cdn.alpinelinux.org/alpine/v3.24/releases/x86_64/alpine-virt-3.24.1-x86_64.iso) 目前大概是 66 MiB。所以我认为这是一个合理的比较基准。 ## https://natkr.com/2026-06-19-nixos-but-smol/#analysis-begins还有别的什么吗? 好吧,让我们看看我们在为什么买单…… ``` $ sudo mount $(nix-build basic-iso.nix --attr isoImage --log-format bar --no-out-link)/iso/nixos.iso iso --mkdir $ du iso --all --block-size=1M | sort -n | tail -n10 3 iso/isolinux 13 iso/boot/nix/store/92id8yn2g9kj7bskld42p222pmk3y3ms-linux-6.18.35 13 iso/boot/nix/store/92id8yn2g9kj7bskld42p222pmk3y3ms-linux-6.18.35/bzImage 26 iso/boot/nix/store/zvn61hpw86ncvgsr8vjrfi3ahnk2c9hb-initrd-linux-6.18.35 26 iso/boot/nix/store/zvn61hpw86ncvgsr8vjrfi3ahnk2c9hb-initrd-linux-6.18.35/initrd 39 iso/boot 39 iso/boot/nix 39 iso/boot/nix/store 416 iso/nix-store.squashfs 458 iso ``` 所以……主用户空间 416 MiB,早期启动环境 26 MiB,内核本身 13 MiB。至少后面两个可以先不管。⁸ (https://natkr.com/2026-06-19-nixos-but-smol/#fn-8) 那我们把这个 squash 文件也解开来看看,里面藏着什么秘密…… ``` $ sudo mount iso/nix-store.squashfs squash --mkdir $ du squash --max-depth=1 --block-size=1M | sort -n | tail -n20 11 squash/bizyfqdw0h67wzqmp10knmf9s2pqahdb-file-5.47 13 squash/pa0x6m662kr9vr875fcp1cl9wwkswsa8-coreutils-full-9.11 14 squash/dscn63ni6fp8p58y99b3ywk478b4bvmv-texinfo-interactive-7.2 14 squash/gf6i4cbisapj28y2dnqhpk1s95vd2r36-util-linux-2.42-lib 15 squash/qmajd4nyy8zf70g9p4x2lcq45gq5gzy3-boost-1.89.0 16 squash/jlyahda14aya375lv7k9fsin2zk90nxz-glib-2.88.1 21 squash/cfjm0s2jnlaiz9y3byvfa0fc6fp2la20-systemd-minimal-260.1 22 squash/92id8yn2g9kj7bskld42p222pmk3y3ms-linux-6.18.35 24 squash/dis2sflz0lifcji3gb81rbr7896dw39l-nix-manual-2.34.7 26 squash/zvn61hpw86ncvgsr8vjrfi3ahnk2c9hb-initrd-linux-6.18.35 28 squash/ryi73l6ic3pz9vh69nj0i4l188vkqgaq-nixos-manual-html 30 squash/r1nzk3ga4fk9q2xw77md2ln88m98d2vc-grub-2.12 32 squash/cl0a76f140zrrbx0n0qpjx07794q5gzn-grub-2.12 34 squash/57iz36553175g3178pvxjij8z5rcsd4n-glibc-2.42-61 39 squash/clpq5c7bysml4vqpa1x60a5yk3nzkfj4-icu4c-76.1 56 squash/6plwsm6pkq79yjv4xvy8csk2pd4hzr67-perl-5.42.0 60 squash/a8avqfxd649rfgfpqldja6v38ljb8fj5-systemd-260.1 128 squash/60m4rxhg2fldqaak400c0lry96ijrzqn-python3-3.13.13 144 squash/f060awdpif3c41v4wbshd6h6jzbxdv66-linux-6.18.35-modules 1064 squash ``` 嗯,这看起来很像一个 Nix 商店……⁹ (https://natkr.com/2026-06-19-nixos-but-smol/#fn-9) 而且因为我们是在宿主机上构建的,里面的任何东西也应该在宿主机商店里!所以我们可以直接把 `squash/` 替换成 `/nix/store/`,然后用 `nix why-depends` 来找出依赖的来源! 比如,我们可以问 Boost 依赖是从哪里来的: ``` $ nix why-depends $(nix-build basic-iso.nix --attr config.isoImage.storeContents --no-out-link) /nix/store/qmajd4nyy8zf70g9p4x2lcq45gq5gzy3-boost-1.89.0 --precise /nix/store/w27j1xagd7cb5qxm1phmvffdwnk4b3wc-nixos-system-nixos-26.11pre-git └───activate: ...fsx38qi-setup-etc.pl /nix/store/7nvwsgl5cplzal9y5m9nnwjjrbbvzcjv-etc/etc..if (( _localstatus > 0... → /nix/store/7nvwsgl5cplzal9y5m9nnwjjrbbvzcjv-etc └───etc/tmpfiles.d/00-nixos.conf -> /nix/store/0zl414yp4h3ppb63h20pljy5rn5v10w4-tmpfiles.d/00-nixos.conf → /nix/store/0zl414yp4h3ppb63h20pljy5rn5v10w4-tmpfiles.d └───nix-daemon.conf -> /nix/store/hqwkw2nala59avjximpdmn1yi474n4h7-nix-2.34.7/lib/tmpfiles.d/nix-daemon.conf → /nix/store/hqwkw2nala59avjximpdmn1yi474n4h7-nix-2.34.7 └───bin/nix: ...1.3.2.GLIBCXX_3.4.21./nix/store/a90px0h1szz9y9c67ww1sa163z44flf9-nix-util-2.34.7/lib:/nix/store/... → /nix/store/a90px0h1szz9y9c67ww1sa163z44flf9-nix-util-2.34.7 └───lib/libnixutil.so.2.34.7: ...5-libcpuid-0.8.1/lib:/nix/store/qmajd4nyy8zf70g9p4x2lcq45gq5gzy3-boost-1.89.0/lib:/nix/store> → /nix/store/qmajd4nyy8zf70g9p4x2lcq45gq5gzy3-boost-1.89.0 ``` 啊哈!¹⁰ 所以我们安装了 Nix 守护进程,然后它把 Boost 也带进来了。 ## https://natkr.com/2026-06-19-nixos-but-smol/#slimming-begins让减肥开始吧 在 search.nixos.org (https://search.nixos.org/) 上稍微看了看之后,我们应该可以通过设置 `nix.enable = false` 来完全禁用附送 Nix。同时,还有一个总开关可以禁用所有文档,`documentation.enable = false`。那我们两个都试试…… ``` let pkgs = import (fetchTarball { url = "https://github.com/NixOS/nixpkgs/archive/567a49d1913ce81ac6e9582e3553dd90a955875f.tar.gz"; sha256 = "1vq77hlx8mi3z03pw2nf6r5h7473r1p9yxyf58ym3fh01zppmfln"; }) {}; in pkgs.nixos ({ lib, ... }: { system.stateVersion = "26.05"; services.getty.autologinUser = "root"; imports = [ "${pkgs.path}/nixos/modules/installer/cd-dvd/iso-image.nix" ]; image.baseName = lib.mkForce "nixos"; nix.enable = false; documentation.enable = false; }) ``` 那么……大成功,我们可以回家庆祝了? ``` $ ls -lh $(nix-build sans-nix.nix --attr isoImage --log-format bar --no-out-link)/iso/nixos.iso -r--r--r-- 1 root root 384M jan 1 1970 /nix/store/rm5zhcmxmypgg4rl2yb2c9lcpaqda85y-nixos.iso/iso/nixos.iso ``` 好吧,这是一个开始。但我们还没做完。烦人的是,我们还能看到 Boost 并没有完全消失。 ``` $ nix why-depends $(nix-build sans-nix.nix --attr config.isoImage.storeContents --no-out-link) /nix/store/qmajd4nyy8zf70g9p4x2lcq45gq5gzy3-boost-1.89.0 --precise /nix/store/aznzf6j6mjllgxllmx92wgmjc977ryin-nixos-system-nixos-26.11pre-git └───activate: ...fsx38qi-setup-etc.pl /nix/store/3kmvk7vy9gr83xnpsd4kg7zvscpa6nr1-etc/etc..if (( _localstatus > 0... → /nix/store/3kmvk7vy9gr83xnpsd4kg7zvscpa6nr1-etc └───etc/systemd/system -> /nix/store/3h0kw6ryg8d7zqwhj5aza7imi4n94631-system-units → /nix/store/3h0kw6ryg8d7zqwhj5aza7imi4n94631-system-units └───register-nix-paths.service -> /nix/store/8jqrvyk58rfixir8lgz2hxqnabhvss6i-unit-register-nix-paths.service/register-nix-paths.service → /nix/store/8jqrvyk58rfixir8lgz2hxqnabhvss6i-unit-register-nix-paths.service └───register-nix-paths.service: ...nged=false.ExecStart=/nix/store/cc50wj89l6j4fyw43hvnwqwh0zn590q7-unit-script-register-nix-paths-... → /nix/store/cc50wj89l6j4fyw43hvnwqwh0zn590q7-unit-script-register-nix-paths-start └───bin/register-nix-paths-start: ...tabase in the tmpfs../nix/store/bvkx110ylicifcgl0xiid5f100hx3ar7-nix-2.34.7/bin/nix-store --load... → /nix/store/bvkx110ylicifcgl0xiid5f100hx3ar7-nix-2.34.7 └───libexec/nix-nswrapper -> /nix/store/97zxp9j00zcjmkn3zv9karhwj86q7x5w-nix-nswrapper-2.34.7/libexec/nix-nswrapper → /nix/store/97zxp9j00zcjmkn3zv9karhwj86q7x5w-nix-nswrapper-2.34.7 └───libexec/nix-nswrapper: ...X_3.4.GLIBCXX_3.4.20./nix/store/a90px0h1szz9y9c67ww1sa163z44flf9-nix-util-2.34.7/lib:/nix/store/... → /nix/store/a90px0h1szz9y9c67ww1sa163z44flf9-nix-util-2.34.7 └───lib/libnixutil.so.2.34.7: ...5-libcpuid-0.8.1/lib:/nix/store/qmajd4nyy8zf70g9p4x2lcq45gq5gzy3-boost-1.89.0/lib:/nix/store/f15... → /nix/store/qmajd4nyy8zf70g9p4x2lcq45gq5gzy3-boost-1.89.0 ``` 看来我们还有一些路径可以触及 Nix(以及 Boost)。这种情况下,它会在启动时尝试注册 ISO 商店里的内容。 当然,这对于人们启动镜像后能够使用 Nix 来说是必需的……而 Nix 恰恰是我们已经移除的东西! 那我们试着把它清空吧…… ``` let pkgs = import (fetchTarball { url = "https://github.com/NixOS/nixpkgs/archive/567a49d1913ce81ac6e9582e3553dd90a955875f.tar.gz"; sha256 = "1vq77hlx8mi3z03pw2nf6r5h7473r1p9yxyf58ym3fh01zppmfln"; }) {}; in pkgs.nixos ({ lib, ... }: { system.stateVersion = "26.05"; services.getty.autologinUser = "root"; imports = [ "${pkgs.path}/nixos/modules/installer/cd-dvd/iso-image.nix" ]; image.baseName = lib.mkForce "nixos"; nix.enable = false; systemd.services.register-nix-paths = lib.mkForce {}; documentation.enable = false; }) ``` 这样会最终有效吗? ``` $ ls -lh $(nix-build sans-nix-redux.nix --attr isoImage --log-format bar --no-out-link)/iso/nixos.iso -r--r--r-- 1 root root 360M jan 1 1970 /nix/store/gk39b6lnq2dbg85dzksqhwb93ijshhf3-nixos.iso/iso/nixos.iso $ nix why-depends $(nix-build sans-nix-redux.nix --attr config.isoImage.storeContents --no-out-link) /nix/store/qmajd4nyy8zf70g9p4x2lcq45gq5gzy3-boost-1.89.0 --precise '/nix/store/mnn0zxj3ccwkzh4mpj3zfwf2z1spm2xv-nixos-system-nixos-26.11pre-git' does not depend on '/nix/store/qmajd4nyy8zf70g9p4x2lcq45gq5gzy3-boost-1.89.0' ``` 看来成功了,呼!不过大小上的胜利并没有 *那么* 大。 ## https://natkr.com/2026-06-19-nixos-but-smol/#no-moar-ssh没有 SSH 了? 在类似模式下继续操作了一会儿(事实证明,`environment.defaultPackages` 就 *在那里*,而且正好可以清空),`ssh` 比我想象的要更难搞掉。 `modules/programs/ssh.nix` 将其添加到 `environment.corePackages` (https://github.com/NixOS/nixpkgs/blob/25e5e0a2522b9f3ad647743e424416e8198f244f/nixos/modules/programs/ssh.nix#L338),这听起来像是我现在不太想完全乱搞的选项。通常,这会由像 `programs.ssh.enable` 这样的选项控制,但我找不到这样的选项。¹¹ (https://natkr.com/2026-06-19-nixos-but-smol/#fn-11) 我们可以用 `disabledModules` (https://nixos.org/manual/nixos/stable/#sec-replace-modules) ¹² (https://natkr.com/2026-06-19-nixos-but-smol/#fn-12) 来整体排除模块,但这会导致其他期望它存在的模块出现一连串连锁错误。¹³ (https://natkr.com/2026-06-19-nixos-but-smol/#fn-13) ``` error: The option `programs.ssh' does not exist. Definition values: - In `/nix/store/pfwrb65dsv8phlsf1m98bvz11cvgb290-source/nixos/modules/services/desktop-managers/plasma6.nix': { _type = "if"; condition = false; content = { askPassword = { ... Did you mean `programs.zsh', `programs.bash' or `programs.fish'? ``` 我们 *可以* 也禁用 `services/desktop-managers/plasma6.nix`,但它 *也* 有一堆反向依赖。为了一个包就钻进这么深的兔子洞似乎不太值得。相反,我们可以为 `programs.ssh` 选项提供一个存根,但不实际 *使用* 它。这样应该能让我们再坚持一会儿…… ``` let pkgs = import (fetchTarball { url = "https://github.com/NixOS/nixpkgs/archive/567a49d1913ce81ac6e9582e3553dd90a955875f.tar.gz"; sha256 = "1vq77hlx8mi3z03pw2nf6r5h7473r1p9yxyf58ym3fh01zppmfln"; }) {}; in pkgs.nixos ({ lib, ... }: { system.stateVersion = "26.05"; services.getty.autologinUser = "root"; imports = [ "${pkgs.path}/nixos/modules/installer/cd-dvd/iso-image.nix" { # 很多模块假设 programs.ssh 选项存在, # 所以我们为它们提供一个存根。 options.programs.ssh = lib.mkOption {}; } ]; image.baseName = lib.mkForce "nixos"; nix.enable = false; systemd.services.register-nix-paths = lib.mkForce {}; documentation.enable = false; documentation.man.enable = false; networking.firewall.enable = false; disabledModules = [ # 在没有明确关闭开关的情况下,将 openssh 注入为核心包 "programs/ssh.nix" ]; environment.defaultPackages = lib.mkForce []; }) ``` ### https://natkr.com/2026-06-19-nixos-but-smol/#sidequest-option-imports等等,我们为什么要 *导入* 这个选项? 一个 NixOS 模块实际上有三个部分: ``` { lib, ... }: { # 模块级选项 imports = [ ... ]; disabledModules = [ ... ]; # 选项定义 options.programs.ssh = lib.mkOption {}; # 实现(设置其他选项) config.networking.firewall.enable = false; } ``` 但如果一个模块没有定义任何选项,就可以用简写形式,不需要在每个实现属性前加 `config.`。这就是为什么你在生成的 `configuration.nix` 以及本文其余部分都没有看到 `config.` 的原因。我不想放弃这个简写形式,所以我会把所有的选项分离到一个单独的……

相似文章

我喜欢的 NixOS 声明式安装方式

Michael Stapelberg

一份关于使用 nixos-anywhere 等工具通过网络声明式安装 NixOS 的指南,重点强调在版本控制下管理配置文件。

Nix Flakes 及其在 Guix 中的对应物

Lobsters Hottest

详细比较 Nix Flakes 与 Guix 包管理系统中的对应物,涵盖依赖声明、锁定、纯净性、输出、开发环境和系统配置。

将我的 NAS 从 CoreOS/Flatcar Linux 迁移到 NixOS

Michael Stapelberg

Michael Stapelberg 详细介绍了他将一台 NAS 从 CoreOS/Flatcar Linux 迁移到 NixOS 的过程,涵盖了从 Docker 容器逐步过渡到原生 NixOS 模块的步骤,并附有实际示例。