An airgap nixos installer with batteries included

Very new to Nix, still struggling to intuitively understand some concepts, sorry if there are stupid questions.

I am trying to make a builder of vm, and an iso image with my custom configuration, which has included a few containers defined in configuration.nix, and what’s important, the images have to be baked in and automatically started after installation, with zero user input and no network requirements.

After some trial and error I came up with a boilerplate snippet that kinda works:

{
  description = "Minimal NixOS installation media";
  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
  outputs = { self, nixpkgs }: {
    nixosConfigurations = {
      iso = nixpkgs.lib.nixosSystem {
        system = "x86_64-linux";
        modules = [
          ({ pkgs, modulesPath, ... }: {
            imports = [
              # switch between building an installer iso and qemu vm
              (modulesPath + "/installer/cd-dvd/installation-cd-graphical-calamares-gnome.nix")
              #(modulesPath + "/virtualisation/qemu-vm.nix")
            ];
            environment.systemPackages = [ pkgs.neovim ];
            virtualisation.containers.enable = true;
            virtualisation = {
              podman = {
                enable = true;
                defaultNetwork.settings.dns_enabled = true;
              };
              oci-containers.containers = {
                test = {
                  image = "test";
                  imageFile = fetchTree {
                    type = "file";
                    url = "file:///home/<me>/Documents/Nix-ISO/test.tar";
                  };
                  autoStart = false;
                };
              };
            };
            nix = {
              settings.experimental-features = ["nix-command" "flakes"];
              extraOptions = "experimental-features = nix-command flakes";
            };
          })
        ];
      };
    };
  };
}

And I’d like to continue to build on top of that. However, there are problems:

  • When I build a VM, the service fails to load image due to no space left on device (it is only creating 1024Mb sized root volume for some reason regardless of settings) and config.virtualisation.diskSize does not affect produced volume size even though it probably should. However, the rest works as expected.
  • When I build an ISO, the installer contains the tarball with an image as expected, but fails to start a service due to no space left on device, again, as expected. However, when I run the installer and finish the installation, the result operating system does not have a tarball copied there, nor does it reflect any config I included in the flake. Looks like the flake parameters only affect the installer and nothing does pass to an installed os.
  • And finally, the installer is not airgap ready. It still reaches out to the internet to get nixpkgs, but I want all the dependencies already included and no attempts to update them no matter what, I want a frozen in time build

Any idea what is the right approach to achieve all that? What am I doing wrong here?

I’d do something like this:

{
  description = "Minimal NixOS installation media";
  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
  outputs = inputs: {
    nixosConfigurations = {
      airgapped = inputs.nixpkgs.lib.nixosSystem {
        system = "x86_64-linux";
        modules = [
         ./configuration.nix # should include hardware-configuration.nix 
        ];
      };
      iso = inputs.nixpkgs.lib.nixosSystem {
        system = "x86_64-linux";
        modules = [
          ({ pkgs, modulesPath, ... }: {
            imports = [
              (modulesPath + "/installer/cd-dvd/installation-cd-minimal.nix")
            ];
            boot.kernelParams = [
              # start only getty
              "systemd.unit=getty.target"
            ];
            systemd.services."getty@tty1.service" = {
              overrideStrategy = "asDropin";
              serviceConfig = {
                ExecStart = let
                  installer = system: pkgs.writeShellApplication {
                    name = "installer";
                    runtimeInputs = with pkgs; [
                      dosfstools
                      e2fsprogs
                      util-linux
                      nixos-install-tools
                    ];
                    text = ''
                      set -euo pipefail

                      echo "Setting up disks..."
                      # partition, format and mount your disk here, the layout must be the same as in hardware-configuration.nix

                      echo "Installing the system..."
                      nixos-install --no-channel-copy --no-root-password --system ${system.config.system.build.toplevel}

                      echo "Done! Rebooting..."
                      reboot
                    '';
                  };
                in [
                  ""
                  "${lib.getExe (installer inputs.self.nixosConfigurations.airgapped)}"
                ];
                Restart = "no";
                StandardInput = "null";
                StandardOutput= "tty";
              };
            };
          })
        ];
      };
    };
  };
}

The airgapped is the system which will be installed, the iso is the installer itself. You should take care of partitioning, formatting and mounting yourself or use disko.

1 Like

Here’s a working implementation of this idea (uefi-only though): misuzu / nixos-unattended-install-iso · GitLab

1 Like

I use this to reinitialize training laptops in our nix classes:

Make sure to use a USB3 stick, otherwise it takes a really long time to install.

2 Likes