An aggressively minimized, bootable NixOS ISO, continuing where natkr's "NixOS but smol" left off. The post took a stock minimal ISO from 458 MiB → 183 MiB. This takes that 183 MiB result and pushes it much further while keeping it a real NixOS system that boots to an autologin root shell.
# Build the ISO
nix-build smallest-iso/iso.nix --attr isoImage --no-out-link
# Boot it headless in QEMU and screenshot/serial-capture the result
bash smallest-iso/boottest.sh <path>/iso/nixos.iso 60boottest.sh boots the ISO under QEMU/KVM, captures the serial console (the
config adds console=ttyS0) and a framebuffer screenshot, and is how every
step below was verified to still reach root@nixos:~]#.
Starting from a faithful reproduction of the post's config (183 MiB):
- xz everything.
isoImage.squashfsCompression = "xz -Xdict-size 100% -Xbcj x86"and an xz initrd. Gotcha: the kernel's built-in xz initramfs decompressor cannot read multi-threaded (-T0) streams — it silently truncates the initramfs and panics at root mount. Use-9 -e --check=crc32. - Cheap closure trims. Drop sudo, nano (drags in
file/libmagic), and empty out the 13 MiB udevhwdb.bin. - Slim systemd, globally. A
withXxx = falseoverride turns off ~30 subsystems (tpm2, repart, importd, homed, networkd, analyze, …). Applied as an overlay (not justsystemd.package) so library consumers (dbus-broker, hostname, util-linux) link the slim build instead of pulling a second, full ~63 MiB systemd. Matching NixOS modules (coredump, oomd, timesyncd) are disabled, and the initrd's hardcodedsystemd-bsod(a qrencode feature we stripped) is suppressed. - Drop pure-PATH tools nothing else references: the bind
hosttool, ping, curl, the hostname binary. - Trim the initrd modules to just what a QEMU/KVM guest needs to find and
mount the CD (
ata_piix ahci sr_mod sd_mod virtio_*); the iso-image and etc-overlay modules still contribute squashfs/iso9660/overlay/erofs. - Smaller kernel image via XZ self-compression (
KERNEL_XZ). - Drop
switch-to-configuration(system.switch.enable = false) — an ISO is immutable, and it dragged in the 10 MiB C++ stdlib. - util-linux minimal, globally — the full build (its own 14 MiB lib + a 9.8 MiB bin + a 5.9 MiB sqlite) is referenced directly by agetty/mount, so only an overlay swap removes it.
| Step | ISO size |
|---|---|
| Post reproduction (baseline) | 183 MiB |
| xz squashfs + initrd | 160 MiB |
| sudo/nano/hwdb trims | 156 MiB |
| global slim-systemd overlay | 137 MiB |
| drop host/curl/iputils/hostname | 135 MiB |
| kernel XZ + initrd module trim + no switch-to-configuration | 128 MiB |
| global util-linuxMinimal overlay | 125 MiB |
| disable networking, drop iproute2/elfutils | 118 MiB |
| disable lvm2 + console setup (drop kbd from initrd) | 114 MiB |
| coreutils-full → coreutils + C.UTF-8 locale | 111 MiB |
| dbus → slim systemd (drop systemd-minimal) | 106 MiB |
| global util-linuxMinimal overlay | 125 → applied earlier |
disable lastlog (drop sqlite) + systemd withOpenSSL=false |
98 MiB |
systemd withLibarchive=false (drops openssl) |
95 MiB |
systemd withVConsole=false (drop kbd) + su xauth off |
93 MiB |
dbus x11Support=false (drop libX11 stack) |
92 MiB |
| trim optional PATH tools (gnutar/bzip2/cpio/…) | 91 MiB |
Every row was verified to boot to root@nixos:~]# in QEMU (serial + VGA).
91 MiB is exactly half the post's 183 MiB, and ~80% below the original
stock 458 MiB minimal ISO the post started from.
The big irreducibles left: glibc (35), the slim systemd (35), the kernel
bzImage + initrd (≈32 combined), the C++ stdlib pulled by nixos-init (10),
two util-linux builds, bash, and linux-headers-static (leaked by the suid
wrappers we must keep for unix_chkpwd). Further wins would need a custom
minimal kernel config (high risk/effort) or replacing glibc — out of scope here.
- security.wrappers — pam_unix needs
unix_chkpwd; nuking the wrappers makes login fail with "Authentication service cannot retrieve authentication info" and getty respawns forever. The wrappers share onelinux-headers-staticreference (~7 MiB nar, but compresses small). - systemd-in-initrd — required by
system.etc.overlay, which is what lets the post (and us) drop perl.