Alexandra's Hideout

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. 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.

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";
  nix.package = pkgs.nixUnstable; # If you're still on 21.11

  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:

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

and then just use nix build.

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

  • If you haven’t already done so, you should declaratively set your user password with users.users.<name>.initialHashedPassword so you’re able to log in.
  • Depending on how many programs you have installed, your system closure size might get really big. In my case, the config.system.build.toplevel closure is 25GiB. This usually gets cut down to around 25% by SquashFS, but the intermediary build products might fill up a lot more space in your Nix store. Remember to nix-collect-garbage sometimes.
  • It’s a good idea to split up your system config into multiple submodules, like user.nix for your user config, development.nix for your programming stuff or gaming.nix for Steam or its likes. Then, you can decide what to put in your ISO by only adding the important config files to modules. Steam is probably useless on your ISO.
  • If you’re not a fanatic home-manager user (yet), a lot of the settings you made imperatively won’t be present in the live system.
  • It’s totally possible to do the same with SD card images (e. g. for Raspberry Pis). I find it incredibly practical to be able to build an SD image with my Raspi config instead of downloading the official image, logging in, setting up an SSH server and an SSH key, and deploying and building the config I need.