Install NixOS on Hetzner Cloud: Difference between revisions

mNo edit summary
Sandro (talk | contribs)
Fix nixos-anywhere install
 
(29 intermediate revisions by 11 users not shown)
Line 9: Line 9:
There are several ways to install NixOS, such as the "traditional" ISO installation, [[nixos-infect]] or [[nixos-anywhere]].
There are several ways to install NixOS, such as the "traditional" ISO installation, [[nixos-infect]] or [[nixos-anywhere]].


=== From NixOS minimal ISO ===
=== Traditional ISO installation ===
The tutorial assumes you already have an account on Hetzner Cloud, and no prior access to a system with NixOS or nix CLI utility installed:
 
# Create a temp folder for future use. Run:<syntaxhighlight lang="shell">
In the Hetzner console, mount the NixOS minimal ISO into your server, and use the console to install NixOS.
mkdir /tmp/my-first-flake
 
</syntaxhighlight>'''Note''': this folder will be mounted into docker container on the next step. Having the folder on the host system enables editing files using a familiar editor, available on the host system, such as VS Code or neovim.
==== x86_64 ====
#Enter docker container. Run:<syntaxhighlight lang="shell">
 
docker run --rm --interactive --tty --mount type=bind,source=/tmp/my-first-flake,target=/tmp/my-first-flake alpine:3.20 ash
At time of writing, Hetnzer's x86_64 servers use legacy boot.
</syntaxhighlight>'''Note''': this is done in a container in order to reduce the "setup footprint and residue", allowing to throw away this setup environment quickly.
 
#Install <code>nix</code> and <code>hcloud</code> CLI utilities. Run:<syntaxhighlight lang="shell">
First, create a new MBR partition table.
apk add nix hcloud openssh-client
 
</syntaxhighlight>
  parted /dev/sda --script mklabel msdos
#Authenticate <code>hcloud</code> CLI utility. Run:<syntaxhighlight lang="shell">
 
hcloud context create my-first-context
Then create a 512MB boot partition with ext4
</syntaxhighlight>
 
#When asked, enter value of the token in the prompt.  '''Note''': the token with "Read/Write" permissions can be obtained on a project page inside Hetzner Cloud: <nowiki>https://console.hetzner.cloud/projects/0000000/security/tokens</nowiki>
  parted /dev/sda --script mkpart primary ext4 1MiB 513MiB
#Create a VM on Hetzner. Run:<syntaxhighlight lang="shell">
  parted /dev/sda --script set 1 boot on
hcloud server create --name my-hetzner-vm --type cpx21 --image ubuntu-24.04 --location fsn1 --start-after-create=false
  mkfs.ext4 -L boot /dev/sda1
</syntaxhighlight>'''Note 1''': this tutorial uses <code>cpx21</code> VM instance type which corresponds to an x86 architecture marchine with 3 CPU cores and 4GB of RAM, and <code>fsn1</code> location which corresponds to a data center in the city of Falkenstein in Germany. A list of all instance types can be obtained by running command <code>hcloud server-type list</code>, while a list of all locations can be obtained by running <code>hcloud location list</code> command. '''Note 2''': Hopefully, Hetzner Cloud team will support NixOS disk images soon, see [https://www.reddit.com/r/NixOS/comments/1desdbv/could_we_convince_hetzner_to_add_nixos_as_a/ Could we convince Hetzner to add Nixos as a standard image choice].
 
#Attach an ISO with NixOS installer. Run:<syntaxhighlight lang="shell">
Create a swap partition. This example uses 8GB, you may want to research the correct amount for your server size. Note the end of the swap partition in this command is 8577MiB, this is the value used in the next command.
hcloud server attach-iso my-hetzner-vm nixos-minimal-24.05.1503.752c634c09ce-aarch64-linux.iso
 
</syntaxhighlight>'''Note''': Hetzner attempts to keep the image as up-to-date as possible, hence the hash of the nixos-minimal image at the time of following this tutorial is highly likely to have changed. Run <code>hcloud iso list</code> and look up an up-to-date name of the nixos-minimal ISO image.
  parted /dev/sda --script mkpart primary linux-swap 513MiB 8577MiB
#Start a VM. Run:<syntaxhighlight lang="shell">
  mkswap -L swap /dev/sda2
hcloud server poweron my-hetzner-vm
  swapon /dev/sda2
</syntaxhighlight>
 
#Open Hetzner Cloud console web page, find the <code>my-hetzner-vm</code> server, open a remote web terminal (aka "VNC over "wss://") and change password of <code>nixos</code> user to <code>my-temp-password-123</code>:[[File:Prompt_with_a_token.png|right|frameless|197x197px]]
Create a root partition using the rest of the disk with ext4.
# On your host computer, create a folder. Run:<syntaxhighlight lang="shell">
 
mkdir -p /tmp/my-first-flake/my-systems/my-hetzner-vm/
  parted /dev/sda --script mkpart primary ext4 8577MiB 100%
</syntaxhighlight>
  mkfs.ext4 -L nixos /dev/sda3
 
If you don't mount the partitions you've just created, the NixOS installer will produce an error in the form `Failed to get blkid info (returned 512) for  on  tmpfs at <path>/<prefix>-install-grub.pl`.
 
  # Mount the partitions to /mnt and /mnt/boot.
  mount /dev/disk/by-label/nixos /mnt
  mkdir /mnt/boot
  mount /dev/disk/by-label/boot /mnt/boot
 
Finally, install. Install from a remote flake:
 
  sudo nixos-install --flake github:<username>/<repo>#<id>
 
Once installed, unmount the ISO and reboot.
 
===== Hetnzer base configuration =====
 
In the example below, the id would be `hetzner-x86_64`.
 
The `flake.nix` file in the repo should be of the form:
 
  {
    inputs = {
      nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
    };
 
    outputs = { nixpkgs, ... }: {
      nixosConfigurations = {
        hetzner-x86_64 = nixpkgs.lib.nixosSystem {
          system = "x86_64-linux";
          modules = [
            ./configuration.nix
          ];
        };
      };
    };
  }
 
With the referenced `configuration.nix` in the form.
 
Note the filesystems configuration, which matches the partition scheme, and the `availableKernelModules` section which includes modules that enable ext4 at boot.
 
Also note the user `username` which is configured to be part of the `wheel` group, and can therefore use `sudo`.
 
  { pkgs, ... }:
 
  {
    nix.settings = {
      experimental-features = "nix-command flakes";
    };
   
    environment.systemPackages = [
      pkgs.vim
      pkgs.git
    ];
   
    fileSystems."/" = {
      device = "/dev/disk/by-label/nixos";
      fsType = "ext4";
    };
    fileSystems."/boot" = {
      device = "/dev/disk/by-label/boot";
      fsType = "ext4";
    };
    swapDevices = [
      {
        device = "/dev/disk/by-label/swap";
      }
    ];
   
    time.timeZone = "Europe/London";
    i18n.defaultLocale = "en_US.UTF-8";
    console.keyMap = "us";
   
    boot.loader.grub.enable = true;
    boot.loader.grub.device = "/dev/sda";
    boot.initrd.availableKernelModules = [ "ahci" "xhci_pci" "virtio_pci" "virtio_scsi" "sd_mod" "sr_mod" "ext4" ];
   
    users.users = {
      root.hashedPassword = "!"; # Disable root login
      username = {
        isNormalUser = true;
        extraGroups = [ "wheel" ];
        openssh.authorizedKeys.keys = [
          ''ssh-rsa <your_ssh_public_key>''
        ];
      };
    };
   
    security.sudo.wheelNeedsPassword = false;
   
    services.openssh = {
      enable = true;
      settings = {
        PermitRootLogin = "no";
        PasswordAuthentication = false;
        KbdInteractiveAuthentication = false;
      };
    };
   
    networking.firewall.allowedTCPPorts = [ 22 ];
   
    system.stateVersion = "24.11";
  }
 
 
To access the VM, you will need to ensure that port 22 on the VM is opened via the Hetzner firewall if that is configured.
 
=== nixos-anywhere ===
The tutorial assumes you already have an account on Hetzner Cloud, and no prior access to a system with NixOS or nix CLI utility installed.
#First upload your SSH key via the Hetzner Web UI
#Then click yourself a VM. For the OS choose Ubuntu but anything should work. This guide was tested with x86_64-linux but aarch64 should work with the note from below.
#Using a code editor on your host computer, create 4 files. File contents, as well as the location of where to put corresponding file are indicated below:<syntaxhighlight lang="nix">
#Using a code editor on your host computer, create 4 files. File contents, as well as the location of where to put corresponding file are indicated below:<syntaxhighlight lang="nix">
# /tmp/my-first-flake/my-systems/my-hetzner-vm/hardware-configuration.nix
# /tmp/my-hetzner-vm/hardware-configuration.nix


{ config, lib, pkgs, modulesPath, ... }:
{ config, lib, pkgs, modulesPath, ... }:
Line 47: Line 158:
   ];
   ];


  boot.initrd.availableKernelModules = [ "ahci" "xhci_pci" "virtio_pci" "virtio_scsi" "sd_mod" "sr_mod" ];
  boot.initrd.kernelModules = [ ];
  boot.kernelModules = [ ];
  boot.extraModulePackages = [ ];
  swapDevices = [ ];
   networking.useDHCP = lib.mkDefault true;
   networking.useDHCP = lib.mkDefault true;
   nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
   nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
}
}
</syntaxhighlight><syntaxhighlight lang="nix">
</syntaxhighlight><syntaxhighlight lang="nix">
# /tmp/my-first-flake/my-systems/my-hetzner-vm/disko-config.nix
# /tmp/my-hetzner-vm/disko-config.nix


{
{
Line 96: Line 202:
}
}
</syntaxhighlight><syntaxhighlight lang="nix">
</syntaxhighlight><syntaxhighlight lang="nix">
# /tmp/my-first-flake/my-systems/my-hetzner-vm/configuration.nix
# /tmp/my-hetzner-vm/configuration.nix


{ config, lib, pkgs, ... }:
{ config, lib, pkgs, ... }:
Line 120: Line 226:
     enable = true;
     enable = true;
     defaultEditor = true;
     defaultEditor = true;
    configure = {
      customRC = ''
        colorscheme base16-ashes
      '';
      packages.packages = {
        start = [
          pkgs.vimPlugins.nvim-base16
        ];
      };
    };
   };
   };


   system.stateVersion = "24.05";
   system.stateVersion = "24.11";
}         
}         
</syntaxhighlight>'''Note''': the value of <code>initialHashedPassword</code> was obtained using <code>mkpasswd</code> command in Linux, and corresponds to <code>Password.123</code> string used as password.<syntaxhighlight lang="nix">
</syntaxhighlight>'''Note''': the value of <code>initialHashedPassword</code> above was obtained using <code>mkpasswd</code> command in Linux, and corresponds to <code>Password.123</code> string used as password.<syntaxhighlight lang="nix">
# /tmp/my-first-flake/flake.nix
# /tmp/my-hetzner-vm/flake.nix


{
{
   inputs = {
   inputs = {
     nixpkgs = {
     nixpkgs = {
       url = "github:NixOS/nixpkgs/nixos-24.05";
       url = "github:NixOS/nixpkgs/nixos-24.11";
     };
     };


Line 160: Line 255:


         modules = [
         modules = [
           ./my-systems/my-hetzner-vm/configuration.nix
           ./configuration.nix
           inputs.disko.nixosModules.disko
           inputs.disko.nixosModules.disko
         ];
         ];
Line 167: Line 262:
   };
   };
}
}
</syntaxhighlight>'''Note''': all these files constitute what's known as a ''nix [[flake]]''. The flake in question is small, though not exactly a minimal one.
#Get the service IP address. Run:<syntaxhighlight lang="shell">
hcloud server ip my-hetzner-vm
</syntaxhighlight>
#Build NixOS from flake. Run:<syntaxhighlight lang="shell">
nix run --extra-experimental-features 'nix-command flakes' github:nix-community/nixos-anywhere -- --flake /tmp/my-first-flake#my-hetzner-vm nixos@0.0.0.0 --build-on-remote
</syntaxhighlight>'''Note''': replace <code>0.0.0.0</code> with an IP address obtained during the previous step.
#Detach ISO from VM. Run:<syntaxhighlight lang="shell">
hcloud server detach-iso my-hetzner-vm
</syntaxhighlight>
#Reboot VM. Run:<syntaxhighlight lang="shell">
hcloud server reboot my-hetzner-vm
</syntaxhighlight>
The NixOS on Hetzner is installed! Let's do a few more steps to customize the installation.
#First, "forget" existing key fingerprint for the Hetzner host. Run:<syntaxhighlight lang="shell">
ssh-keygen -f /root/.ssh/known_hosts -R 0.0.0.0
</syntaxhighlight>'''Note''': again, here and below, replace <code>0.0.0.0</code> with an IP address obtained via <code>hcloud server ip my-hetzner-vm</code>.
#Copy flake files onto the server. Run:<syntaxhighlight lang="shell">
scp -r /tmp/my-first-flake eugene@0.0.0.0:~/
</syntaxhighlight>
#Using <code>neovim</code> editor on the VM, modify <code>configuration.nix</code> to include a package containing Elixir programming language runtime for <code>eugene</code> user. Run:<syntaxhighlight lang="shell">
nvim my-first-flake/my-vms/my-hetzner-vm/configuration.nix
</syntaxhighlight>Edit the <code>configuration.nix</code> so that <code>users</code> block looks like this:<syntaxhighlight lang="shell">
# ~/my-first-flake/my-vms/my-hetzner-vm/configuration.nix
# ...
users.users.eugene = {
  isNormalUser = true;
  extraGroups = [ "wheel" ];
  initialHashedPassword = "$y$j9T$2DyEjQxPoIjTkt8zCoWl.0$3mHxH.fqkCgu53xa0vannyu4Cue3Q7xL4CrUhMxREKC"; # Password.123
  packages = [
    pkgs.beam.packages.erlang_26.elixir_1_16
  ];
};
# ...
</syntaxhighlight>
#Re-build NixOS. Run:<syntaxhighlight lang="shell">
sudo nixos-rebuild switch --flake ./my-first-flake#my-hetzner-vm
</syntaxhighlight>
</syntaxhighlight>
#To build NixOS from the flake run:<syntaxhighlight lang="shell">
nix run --extra-experimental-features 'nix-command flakes' github:nix-community/nixos-anywhere -- --flake /tmp/my-hetzner-vm#my-hetzner-vm --target-host root@0.0.0.0 --build-on-remote
</syntaxhighlight>'''Note''': replace <code>0.0.0.0</code> with an IP address obtained during an earlier step.
The NixOS on Hetzner is installed!


=== disko ===
=== disko ===
Line 215: Line 274:
references:
references:


* [[Disko]]
* [https://github.com/feelssexy/hetzner-auto-nixos/blob/main/hardware-configuration.nix sample regular hardware config]
* [https://github.com/feelssexy/hetzner-auto-nixos/blob/main/hardware-configuration.nix sample regular hardware config]
* [https://github.com/LGUG2Z/nixos-hetzner-cloud-starter/blob/master/disk-config.nix sample config using disko]
* [https://github.com/LGUG2Z/nixos-hetzner-cloud-starter/blob/master/disk-config.nix sample config using disko]
Line 233: Line 293:
* Run following script. Replace <code>NIX_CHANNEL</code> variable with the version string you wish to install.
* Run following script. Replace <code>NIX_CHANNEL</code> variable with the version string you wish to install.
<syntaxHighlight lang=bash>
<syntaxHighlight lang=bash>
curl https://raw.githubusercontent.com/elitak/nixos-infect/master/nixos-infect | NIX_CHANNEL=nixos-22.11 bash -x
curl https://raw.githubusercontent.com/elitak/nixos-infect/master/nixos-infect | NIX_CHANNEL=nixos-24.11 bash -x
</syntaxHighlight>
</syntaxHighlight>
* Reboot into NixOS
* Reboot into NixOS
Line 248: Line 308:


<syntaxhighlight lang="nix">
<syntaxhighlight lang="nix">
  systemd.network.enable = true;
systemd.network.enable = true;
  systemd.network.networks."30-wan" = {
systemd.network.networks."30-wan" = {
    matchConfig.Name = "ens3"; # either ens3 (amd64) or enp1s0 (arm64)
  matchConfig.Name = "ens3"; # either ens3 or enp1s0, check 'ip addr'
    networkConfig.DHCP = "ipv4";
  networkConfig.DHCP = "ipv4";
    address = [
  address = [
      # replace this subnet with the one assigned to your instance
    # replace this subnet with the one assigned to your instance
      "2a01:4f8:aaaa:bbbb::1/64"
    "2a01:4f8:aaaa:bbbb::1/64"
    ];
  ];
    routes = [ { routeConfig.Gateway = "fe80::1"; } ];
  routes = [
  };
    { Gateway = "fe80::1"; }
  ];
};
</syntaxhighlight>
</syntaxhighlight>


Line 264: Line 326:


<syntaxhighlight lang="nix">
<syntaxhighlight lang="nix">
  systemd.network.networks."30-wan" = {
systemd.network.networks."30-wan" = {
    matchConfig.Name = "ens3"; # either ens3 (amd64) or enp1s0 (arm64)
  matchConfig.Name = "ens3"; # either ens3 (amd64) or enp1s0 (arm64)
    networkConfig.DHCP = "no";
  networkConfig.DHCP = "no";
    address = [
  address = [
      # replace this address with the one assigned to your instance
    # replace this address with the one assigned to your instance
      "A.B.C.D/32"
    "A.B.C.D/32"
      # replace this subnet with the one assigned to your instance
    # replace this subnet with the one assigned to your instance
      "2a01:4f8:AAAA:BBBB::1/64"
    "2a01:4f8:AAAA:BBBB::1/64"
    ];
  ];
    routes = [ {
  routes = [
      routeConfig = { Gateway = "172.31.1.1"; GatewayOnLink = true; }; }
    { Gateway = "172.31.1.1"; GatewayOnLink = true; }
      { routeConfig.Gateway = "fe80::1"; }
    { Gateway = "fe80::1"; }
    ];
  ];
  };
};
</syntaxhighlight>
</syntaxhighlight>


Line 290: Line 352:


[[Category:Cookbook]]
[[Category:Cookbook]]
[[Category:Deployment]]