能给我更小的NixOS ISO吗?
摘要
关于构建最小化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 声明式安装方式
一份关于使用 nixos-anywhere 等工具通过网络声明式安装 NixOS 的指南,重点强调在版本控制下管理配置文件。
Nix Flakes 及其在 Guix 中的对应物
详细比较 Nix Flakes 与 Guix 包管理系统中的对应物,涵盖依赖声明、锁定、纯净性、输出、开发环境和系统配置。
在Sailfish X(适用于Sony Xperia的Sailfish OS)上使用Nix
一份关于在适用于Sony Xperia设备的Sailfish OS上安装Nix包管理器的技术指南,涵盖文件系统布局、LVM分区以及F2FS相关注意事项。
将我的 NAS 从 CoreOS/Flatcar Linux 迁移到 NixOS
Michael Stapelberg 详细介绍了他将一台 NAS 从 CoreOS/Flatcar Linux 迁移到 NixOS 的过程,涵盖了从 Docker 容器逐步过渡到原生 NixOS 模块的步骤,并附有实际示例。
Guix Nix 的怪异融合:在 Nix 中利用 Guix 派生
一项技术探索,展示了 Nix 如何构建 Guix 派生项,强调了共享底层“输入输出机”架构以及跨生态系统互操作的可能性。