Skip to content

A NixOS system with 100KiB closure size

Published: at 09:12 PM

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.