home blog misc links contact about
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.
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.
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:
"x86-64_linux".default = self.nixosConfigurations.isoimage.config.system.build.isoImage; packages.
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.
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 ./hardware-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.
users.users.<name>.initialHashedPassword
so you’re
able to log in.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.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.config.system.build.sdImage
there. 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.