Following a tiny detour of a side project, I was left with a question: how small can a NixOS system get?
Turns out this idea has already been explored. Nixpkgs provides a set of profiles you can import into your NixOS configuration to remove dependencies and features to produce a smaller system, at the cost of losing some features such as e.g built-in docs and manuals.
But I was left wondering: how small can a NixOS system be?
A small working system
The furthest I got while still producing a working system was with a combination of these nixpkgs profiles: a NixOS configuration importing the minimal
, image-based-appliance
and perlless
seems to come up with a closure size of ~500MiB:
❯ nix path-info -Sh .#nixosConfigurations.host.config.system.build.toplevel
/nix/store/jxn1x0sfzd9nlqvncjam6k5glnlwwl26-nixos-system-host-24.05.20241214.bcba2fb 467.7 MiB
This seems like a good value, especially since the baseline seems to be ~700MiB.
After inspecting a configuration with these three profiles with nix-tree
, the largest (and unavoidable) culprits in final closure size seem to be systemd (166MiB) and the kernel (129.66 MiB).
Producing the smallest of nixosConfigurations
I set out to figure out how small I could make a NixOS configuration while still remaining valid - not as a working system this time, but only as far as nixpkgs
and nix
are concerned.
Turns out, it can get really small!
❯ nix path-info -Sh .#nixosConfigurations.host.config.system.build.toplevel
/nix/store/ihzkx8c64fawssld48gxmgqf3i8v6yvr-nixos-system-host-24.05.20241214.bcba2fb 81.4 KiB
The biggest hurdles in getting here came from the NixOS checks/assertions, which enforce some sanity checks that were hard to get around at first.
Here’s the final configurations I used, although some settings only shave off beadcrumbs, while others are probably redundant:
{
pkgs,
lib,
modulesPath,
...
}:
with lib;
{
# Use this configuration on non-NixOS hosts to reduce their derivation's size to ~100KiB.
# Sources:
# - https://github.com/NixOS/nixpkgs/blob/cf795d556068c88a89b3d09348595b5fc226cec8/nixos/modules/virtualisation/container-config.nix#L17
# - https://github.com/NixOS/nixpkgs/tree/107d5ef05c0b1119749e381451389eded30fb0d5/nixos/modules/profiles/
# - https://github.com/NixOS/nixpkgs/blob/bc09dbe4bdd33f915d21b09895c51c066f6f7043/nixos/modules/system/boot/systemd.nix#L481
imports = [
(modulesPath + "/profiles/minimal.nix")
(modulesPath + "/profiles/image-based-appliance.nix")
(modulesPath + "/profiles/perlless.nix")
];
users.allowNoPasswordLogin = true;
users.mutableUsers = lib.mkForce true; # required by perlless profile
# Pretend to be a container to enable nixpkgs minimization options
boot.isContainer = true;
networking.useHostResolvConf = mkForce false; # `isContainer` breaks this.
environment.systemPackages = mkForce [ ];
environment.etc = mkForce { };
# Systemd
# NixOS contains a lot of checks that depend on files/directories
# inside config.systemd.package to exist.
# Not even `systemdMinimal` seems to provide enough to satisfy these assertions.
# This small script mimics the file hierarchy inside systemdMinimal.
systemd.package =
pkgs.runCommand "systemdEmpty"
{
passthru = {
interfaceVersion = "";
};
}
''
mkdir $out
SOURCE_DIR=${pkgs.systemdMinimal}
find "$SOURCE_DIR" -type f | while read -r file; do
target_file="$out/''${file#$SOURCE_DIR/}"
mkdir -p "$(dirname "$target_file")"
touch "$target_file"
done
# This is completely unnecessary,
# but gets the closure size from 102.0 KiB to 81.KiB =)
rm $out/share/locale -r
'';
systemd.coredump.enable = false;
systemd.oomd.enable = false;
systemd.units = mkForce { };
systemd.user.units = mkForce { };
systemd.services = mkForce { };
system.activationScripts = mkForce { users = ""; };
system.activatable = false;
system.disableInstallerTools = true;
system.build.bootStage2 = mkForce "/dev/null"; # reduces size by ~100-200M!
boot.swraid.enable = false;
boot.loader.grub.enable = mkForce false;
boot.kernel.enable = false;
boot.kernelModules = mkForce [ ];
boot.initrd.kernelModules = mkForce [ ];
boot.initrd.availableKernelModules = mkForce [ ];
system.forbiddenDependenciesRegexes = [ "perl" ];
}
It seems like a wortwhile exercise for attempting to create a very minimal NixOS system, perhaps for using in a Raspberry Pi-like device.