Nixos hetzner image

Has anyone ever built a NixOS image that can be uploaded to hcloud?

What I am currently doing with Talos is:

    hcloud-upload-image -v upload \
        --server-type cpx21 \
        --image-path metal-amd64.raw.xz \
        --compression xz \
        --labels "talos=v1.9.3,type=cpx21"

And I now would like to have an image like that for NixOS.

I have cobbled together a unattended install iso that has worked great for some bare metal installs.

And it feels like I am at least half way there.

But I am currently lost in the machinery for the raw version

Has anyone looked into this before?

cheers,
Torsten

1 Like

Someone else running NixOS on Hetzner cloud? What approach do you use?
Infecting a Debian machine isnā€™t the very ideal way.

1 Like

Whatā€™s causing the don't know how to build these paths?
Any pointers what might be missing here?

disk-image> Installing the system
disk-image> warning: the group 'nixbld' specified in 'build-users-group' does not exist
disk-image> warning: the group 'nixbld' specified in 'build-users-group' does not exist
disk-image> don't know how to build these paths:
disk-image>   /nix/store/chwpfw7ihh7xj1ip1qx1g3grmiqk6n95-nixos-system-nixos-25.05.20250208.a79cfe0
disk-image> error: build of '/nix/store/chwpfw7ihh7xj1ip1qx1g3grmiqk6n95-nixos-system-nixos-25.05.20250208.a79cfe0' failed
disk-image> [    5.223603] Kernel panic - not syncing: Attempted to kill init! exitcode=0x00000100

Hi, I did an install there once, I first looked if I could upload a qcow2 image but didnā€™t find a way, so I simply uploaded the ISO with the NixOS graphical installer, following this doc.

Thanks for the pointer. But a manual install is exactly what I want to avoid.

It seems nixpkgs has some definitions for cloud images (aws, azure,ā€¦)

I thought I just give them a try - but I have no idea where to find them.

I assume they should be available somewhere via hydra? But I didnā€™t find them here

https://hydra.nixos.org/project/nixpkgs

ā€¦or in the next step - how to customize them and build them via my flake.

Yeah I also wanted to avoid manual install, on other providers (exoscale, infomaniak public cloud) I could use make-disk-image to create an ā€œready to bootā€ qcow2 image (with cloud-init enabled), but never used those cloud images definitions. Hope youā€™ll find a way.

edit: I think you could use make-disk-image to build a raw image and upload it with this tool

It seems like getting make-disk-image to build and then customized would be the first 2 steps for me.

But I havenā€™t found much more than

Thatā€™s the tool I mentioned in the original post :wink:
It works great with Talos and I would love to have an image for NixoOS, too.

1 Like

Here is an example flake.nix, you can build the image with nix build .#qcow2

{
  description = "A NixOS base image for QEMU using cloud-init";

  inputs = {
    nixpkgs.url = "nixpkgs/nixos-24.11";
  };

  outputs = { self, nixpkgs }:
  let
    system = "x86_64-linux";
    pkgs = nixpkgs.legacyPackages.${system};
    inherit (pkgs) lib;

    baseModule = { lib, config, pkgs, ...}: {
      nixpkgs.hostPlatform = "x86_64-linux";
      imports = [
        "${nixpkgs}/nixos/modules/profiles/qemu-guest.nix" 
        # "${nixpkgs}/nixos/modules/virtualisation/azure-image.nix"
      ];

      boot.loader.grub.device = "/dev/sda";

      fileSystems."/" = {
        label = "nixos";
        fsType = "ext4";
      };

      networking.useDHCP = false;

      services = {
        cloud-init = {
          enable = true;
          network.enable = true;
          config = ''
            system_info:
              distro: nixos
              network:
                renderers: [ 'networkd' ]
            datasource_list: [ "Exoscale" ]
            cloud_init_modules:
              - migrator
              - seed_random
              - growpart
              - resizefs
            cloud_config_modules:
              - disk_setup
              - mounts
            cloud_final_modules: []
          '';
        };

        qemuGuest.enable = true;

        openssh = {
          enable = true;
          settings.PasswordAuthentication = false;
          settings.KbdInteractiveAuthentication = false;
        };
      };

      users.users.root.openssh.authorizedKeys.keys = [
        # ...
      ];
    };

    nixos = nixpkgs.lib.nixosSystem {
      modules = [ baseModule ];
    };

    make-disk-image = import "${nixpkgs}/nixos/lib/make-disk-image.nix";

  in {
    inherit pkgs;
    qcow2 = make-disk-image {
      inherit pkgs lib;
      config = nixos.config;
      name = "nixos-cloudinit";
      format = "qcow2-compressed";
      copyChannel = false;
      additionalSpace = "10G";
    };
  };
}
1 Like

Thanks a lot, @MatthieuB

Unfortunately my nix language skills are failing me to integrate that :frowning:
Some aspects of this language are just puzzling. I think I will never become a fan of that aspect of nixOS.

  outputs =
    inputs:
    let
      # pkgs = nixpkgs.legacyPackages.${system};
      # inherit (pkgs) lib;
      make-disk-image = import "${inputs.nixpkgs}/nixos/lib/make-disk-image.nix";
    in
    {
      nixosConfigurations = {

        nixos-x86 = inputs.nixpkgs.lib.nixosSystem {
          system = "x86_64-linux";
          modules = [
            # inputs.disko.nixosModules.disko
            ./configuration.nix
            # ./make-disk-image.nix
          ];
          # disko.devices.disk.main.device = "/dev/sda";
        };

        disk-x86 = make-disk-image {
          inherit pkgs lib; # where should these come from?
          config = inputs.self.nixosConfigurations.nixos-x86.config;
          name = "nixos-cloud-x86";
          format = "qcow2-compressed";
          copyChannel = false;
          additionalSpace = "10G";
        };

      };
    };

Anyone pointers on how to fix this?

baseModule in ./configuration.nix and nix build .#disk-x86 did not work ?

Iā€™m running a few machines on Hetzner cloud that I update ā€˜on-the-flyā€™ ā€˜from outsideā€™ with nixos-rebuild --target-host ... switch

The initial deployment goes like this:

  • Create the nix definition of the machine (importing the qemu-guest.nix and using disko for the partition layout, and in my case agenix for secrets)
  • Create a machine with the default Ubuntu image
  • Add the IP to DNS
  • Get the machineā€™s public key with ssh-keyscan <IP> and add it to the local agenix config so the secrets become available to the machine
  • Initial deploy with nixos-anywhere --copy-host-keys root@<IP>

After this subsequent updates can be pushed with nixos-rebuild --target-host ... switch

I agree itā€™s a bit of a weird bootstrap step, but nixos-anywhere at least hides it fairly nicely. Apparently hcloud-upload-image does something somewhat similar (though itā€™s nice it creates the machine for you and uses the rescue system). Of course thereā€™s additional trade-offs here, for example you might prefer to bring your own host key instead of relying on the one provisioned by Hetzner.

1 Like

./make-disk-image.nix is basically where I moved the baseModule to.

ā€¦but that gives me an infinite recursion.

I also tried with my old ./configuration.nix but that gives me

attribute 'config' in selection path 'config.system.build.toplevel' not found

And TBH I a little lost when it comes to reasoning about these errors.

I am also lost on the need for

pkgs = inputs.nixpkgs.legacyPackages.x86_64-linux;

As I said - I feel a little in over my head.
But I really would love an image.
So I keep trying.

But the big difference is - it bootstraps the server only once for the image.
I really want to get to the point where I can use terraform/opentofu to create my servers.

After that itā€™s pretty much just running the flake butā€¦

Iā€™m not sure I understand what you mean by ā€˜bootstrapsā€™ here - that it boots into the rescue system instead of the Ubuntu image? or something else?

How do you manage secrets?

(that sounds neat indeed, though for my use cases I donā€™t think the added complexity weighs up to the advantages yet)

i didnā€™t bother with any custom image for hetzner cloud to do this, i just pick an ubuntu image and run nixos-infect with terraform, wait a few minutes, and then i have a nixos machine which is ready to use

Hi,

Iā€™ve just done this using nixos-generators.

Essentially what you do is:

  • Import the nixos-generator module for the desired format into the NixOS configuration. Note that hcloud-upload-image only supports raw disk images, optionally compressed with bzip2 or xz. In my config this looks something like this:
{
  lib,
  modulesPath,
  flake,
  ...
}: {
  imports = [
    (modulesPath + "/profiles/qemu-guest.nix") # Hetzner uses QEMU/KVM
    flake.inputs.nixos-generators.nixosModules.raw-efi
  ];
  # Required for CAX (ARM) servers
  boot.initrd.kernelModules = ["virtio_gpu"];
  boot.kernelParams = ["console=tty"];
}

(I pass specialArgs.flake = self; to nixosSystem. self is one of the arguments always passed to a Flakeā€™s output function. )

  • Access the attribute that has the name of config.formatAttr at config.system.build; in my case this is config.system.build.raw (and not raw-efi!):
{flake, ...}: {
    hetzner-disk-image = let
      inherit (flake.nixosConfigurations.hetzner) config;
    in
      config.system.build.${config.formatAttr};
}

I then uploaded the resulting image using hcloud-upload-image.

I compressed it with xz before, but as it turns out, hcloud-upload-image just pipes the image through xz on the remote server when uploading. So this only saves bandwidth during the upload, but not any space on the Hetzner snapshot.

Do note that I use the image to directly create a single autoscaled Kubernetes node connected to my homelab cluster; and not to have a generic NixOS installer.
But I guess you could achieve that by:

  • creating an image for a minimal working NixOS configurations
  • creating a server with that image (duh)
  • upload the system closure (config.system.build.toplevel) for the actual configuration to it
  • activate it

You could of course still use nixos-anywhere, but it would kexec into a live image and reformat the disk first.