home blog misc about

Building custom NixOS ISOs

TL;DR: NixOS installation ISOs are just normal Nix derivations. You can build them with a custom config like you’d build normal NixOS systems, and Nix makes this stupidly easy. In this post, I’m going to give a quick rundown of the basics.

If you want to install NixOS, the first step would be to head over to nixos.org and grab one of their ISOs. As soon as you boot them up, you’re greated with an unfamiliar system. You might have to change your keyboard layout, monitor settings, themes - and in the case of the official NixOS ISOs, even enable flake support. Every time anew. For me, this quickly got somewhat annoying, so I started to do some research on customizing NixOS ISOs. I had already tried something like this with Ubuntu a couple of years ago, but I remember quickly giving up back then because of the complex tooling required.

Basics

After some searching on the Internet, I found out that NixOS ISOs aren’t built by any sort of special tooling. They’re just derivations in Nixpkgs, although a bit trickier to access. The trick here is that ISO images are basically just normal NixOS systems, with a bit of extra config to package itself as an ISO.

Normally, nixos-rebuild builds the config.system.build.toplevel derivation, which contains the kernel, initrd, systemd services, etc. This is also the derivation that shows up under /nix/var/nix/profiles/system and /run/current-system:

$ tree /run/current-system
/run/current-system
├── activate
├── append-initrd-secrets -> /nix/store/ldkq296p75q35y3bjz4s1qfyn6kmlp16-append-initrd-secrets/bin/append-initrd-secrets
├── bin
│   └── switch-to-configuration
├── boot.json
├── bootspec
├── dry-activate
├── etc -> /nix/store/5qx8vgxjghx9z885nzyqlcrzs4g2lwqr-etc/etc
├── extra-dependencies
├── firmware -> /nix/store/dpxqpb4gjvi4sv7hh5np37qppdi98g7b-firmware/lib/firmware
├── init
├── init-interface-version
├── initrd -> /nix/store/1k0h173hqnslqx7sc81hj05nrmjsrp5w-initrd-linux-6.2/initrd
├── kernel -> /nix/store/w4lnbkdmmrcihgsgrw5pk3hzbbs8wx3n-linux-6.2/bzImage
├── kernel-modules -> /nix/store/1ar1dmlrgrrxwc2xrqps03b99cz8b4mj-kernel-modules
├── kernel-params
├── nixos-version
├── prepare-root
├── specialisation
├── sw -> /nix/store/f2259915wmzq53c9gd3gdaf6r206sjpz-system-path
├── system
└── systemd -> /nix/store/kp51yj0jwdi2b7f8i93ici4c5z0x7bn3-systemd-252.5

What the NixOS ISO configuration modules do is add an extra derivation called config.system.build.isoimage which nicely packages up all of the above stuff into an ISO. These config files are located in nixos/modules/installer/cd-dvd in the Nixpkgs tree:

nixos/modules/installer/cd-dvd/
├── channel.nix
├── installation-cd-base.nix
├── installation-cd-graphical-base.nix
├── installation-cd-graphical-gnome.nix
├── installation-cd-graphical-plasma5-new-kernel.nix
├── installation-cd-graphical-plasma5.nix
├── installation-cd-minimal-new-kernel.nix
├── installation-cd-minimal.nix
├── iso-image.nix
...

The file that actually does the ISO image building is iso-image.nix. If you look into it, you’ll find definitions for for the bootloader configs, filesystems, etc. needed to build the ISO. This file roughly corresponds to hardware-configuration.nix in a normal NixOS config. The other files (installation-cd-*.md) are what you’d normally expect in a configuration.nix, e. g. networking, users or desktop environments. An exception to this is installation-cd-base.nix, which also defines some options related to the ISO file.

Now, we can either take installation-cd-dvd-base.nix as a base and write the rest of the config by ourselves, or slightly extend one of the official ISO editions like installation-cd-graphical-gnome.nix. For now, we’ll try the latter, but I’ll also come back to the former option later.

The rest of the article assumes that you’re using flakes. If you don’t know what they are, here’s a good introduction. This is also possible without flakes, but the process is not as slick and beautiful.

Customizing the official GNOME ISO

We’ll start by writing a simple flake.nix that defines a single NixOS system. The system should include a configuration.nix of us and the installer-cd-graphical-gnome.nix of Nixpkgs:

# flake.nix
{
  inputs.nixpkgs.url = github:NixOS/nixpkgs/nixos-unstable;
  outputs = { self, nixpkgs, ... }: {
    nixosConfigurations."isoimage" = nixpkgs.lib.nixosSystem {
      system = "x86_64-linux";
      modules = [
        ./configuration.nix
        "${nixpkgs}/nixos/modules/installer/cd-dvd/installation-cd-graphical-gnome.nix"
      ];
    };
  };
}

In our configuration.nix, we could for instance enable flakes by default and set the default shell to fish:

# configuration.nix
{ pkgs, ... }: {
  nix.extraOptions = "experimental-features = nix-command flakes";

  programs.fish.enable = true;
  users.defaultUserShell = pkgs.fish;
}

Now, we can build the ISO file with:

$ nix build .#nixosConfigurations.isoimage.config.system.build.isoImage

For convenience, we could also add the isoImage derivation as the default package output:

packages."x86-64_linux".default = self.nixosConfigurations.isoimage.config.system.build.isoImage;

and then just use nix build without an argument.

The build process usually takes a while because the root directory structure is compressed into a SquashFS. As soon as it’s done, we can find the ready-made ISO file in result/iso. Boot it up and enjoy your customized live Linux. And don’t forget to run nix flake update and rebuild occasionally to keep the ISO up to date.

Building your normal system configuration as an ISO

But the near-infinite versatility of Nix doesn’t stop here. Generally speaking, you’ll probably want the same settings and programs on your live ISO as on your main system. Luckily, if you use flakes, it’s very simple to build your system configuration in a completely modular way. Let’s assume that your /etc/nixos/flake.nix looks like this:

{
  inputs.nixpkgs.url = github:NixOS/nixpkgs/nixos-unstable;
  outputs = { self, nixpkgs }: {
    nixosConfigurations."my-system" = nixpkgs.lib.nixosSystem {
      system = "x86_64-linux";
      modules = [
        ./configuration.nix
        ./hardware-configuration.nix
      ];
    };
  };
}

If you’ve generated your config files with nixos-generate-config (as everyone does), you should make sure that your configuration.nix doesn’t import hardware-configuration.nix at the top. This is already done by including ./harware-configuration.nix in modules and would break what we’re doing next. Also, you have to move your bootloader configuration out of configuration.nix, either into a third file or into hardware-configuration.nix.

Now, your hardware-configuration.nix (hopefully) contains everything related to bootloaders and filesystems, while configuration.nix only contains the actual settings you also want on your ISO. Now, you can just add another output to your system flake:

nixosConfigurations.isoimage = nixpkgs.lib.nixosSystem {
  system = "x86_64-linux";
  modules = [
    ./configuration.nix
    "${nixpkgs}/nixos/modules/installer/cd-dvd/installation-cd-base.nix"
  ];
};

That’s it. Now, whenever you update your system settings, you can also build an ISO containing the same programs and settings.

A few side notes