Introduction
Recently I’ve been playing with NixOS and I am trying to learn new tools from the ecosystem. I was able to use disko and nixos-anywhere to re-provision a Debian machine in Hetzner Cloud and I document the steps below.
Quick explanation of what disko and nixos-anywhere are:
-
disko: tool to define disk layouts and filesystems as code;
-
nixos-anywhere: tool to install NixOS on any machine (NixOS or not) over SSH;
Throughout this blog post I use the expression host machine
to indicate the one where I run the commands, although technically the term might not be correct. I also use target machine
to indicate the machine I want to provision.
Environment information
Before I start, I think it is important to document the environment where this has been successful and where I could not replicate it.
In Hetzner Cloud, I created a server (target machine
) from scratch with the following specs:
- Date of this successful attempt: 2024-10-05
- Location: Nuremberg (`eu-central`)
- Image: Debian 12
- Type: Shared vCPU
- Architecture: x86 (Intel/AMD)
- Model: CX22
- VCPUS: 2 (Intel)
- RAM: 4GB
- SSD: 40GB
- Traffic: 20TB
- Price/h: 0.005 EUR
- Price/mo: 3.29 EUR
- Networking:
- [x] Public IPv4 (0.0008 EUR/h)
- [x] Public IPv6 (free)
- [ ] Private Networks
- SSH Keys: Add the ssh key of the host machine
- Volumes: empty
- Firewalls: empty
- Backups: empty
- Placement groups: empty
- Labels: empty
- Cloud config: empty
- Name: debian-4gb (can be anything)
I first tried to provision using my MacBook as a host machine, but it errored with the following message:
error: a 'x86_64-linux' with features {} is required to build '/nix/store/z33f5rg30amnmpxz4g1qdz0kh42cn7k1-disko.drv', but I am a 'aarch64-darwin' with features {apple-virt, benchmark, big-parallel, nixos-test}
Since I could not figure out if it was possible to do the process using a different architecture, I decided to make things simpler for the sake of learning and do the process in a NixOS-installed machine I have at home.
Host machine spec:
nix-info -m
- system: `"x86_64-linux"`
- host os: `Linux 6.6.36, NixOS, 24.11 (Vicuna), 24.11.20240703.9f4128e`
- multi-user?: `yes`
- sandbox: `yes`
- version: `nix-env (Nix) 2.18.4`
- channels(root): `"nixos-23.11, unstable"`
- nixpkgs: `/nix/store/dk2rpyb6ndvfbf19bkb2plcz5y3k8i5v-source`
Necessary files
Prepare the following 3 files inside an empty directory:
flake.nix
{
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
inputs.disko.url = "github:nix-community/disko";
inputs.disko.inputs.nixpkgs.follows = "nixpkgs";
inputs.nixos-facter-modules.url = "github:numtide/nixos-facter-modules";
outputs =
{
nixpkgs,
disko,
nixos-facter-modules,
...
}:
{
nixosConfigurations = {
# The name `hetzner-cloud` is arbitrary, and should match the final command in the Provisioning section of this post
hetzner-cloud = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
disko.nixosModules.disko
./configuration.nix
./hardware-configuration.nix
];
};
};
};
}
configuration.nix
{
modulesPath,
lib,
pkgs,
...
}:
{
imports = [
(modulesPath + "/installer/scan/not-detected.nix")
(modulesPath + "/profiles/qemu-guest.nix")
./disk-config.nix
];
boot.loader.grub = {
# no need to set devices, disko will add all devices that have a EF02 partition to the list already
# devices = [ ];
efiSupport = true;
efiInstallAsRemovable = true;
};
services.openssh.enable = true;
environment.systemPackages = map lib.lowPrio [
pkgs.curl
pkgs.gitMinimal
];
users.users.root.openssh.authorizedKeys.keys = [
# TODO: change this to your ssh key
"ADD YOUR SSH PUBLIC KEY CONTENT HERE"
];
system.stateVersion = "24.05";
}
disk-config.nix
# Example to create a bios compatible gpt partition
{ lib, ... }:
{
disko.devices = {
disk.disk1 = {
device = lib.mkDefault "/dev/sda";
type = "disk";
content = {
type = "gpt";
partitions = {
boot = {
name = "boot";
size = "1M";
type = "EF02";
};
esp = {
name = "ESP";
size = "500M";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
};
};
root = {
name = "root";
size = "100%";
content = {
type = "lvm_pv";
vg = "pool";
};
};
};
};
};
lvm_vg = {
pool = {
type = "lvm_vg";
lvs = {
root = {
size = "100%FREE";
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/";
mountOptions = [
"defaults"
];
};
};
};
};
};
};
}
For the disk-config.nix
, it is necessary to verify if the target system (in my case, the Debian machine in Hetzner) has the same block device as the one configured in the file (/dev/sda
). To do this, I run the following command in the Debian machine (after SSH into it):
lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
sda 8:0 0 38.1G 0 disk
├─sda1 8:1 0 37.9G 0 part /
├─sda14 8:14 0 1M 0 part
└─sda15 8:15 0 244M 0 part /boot/efi
sr0 11:0 1 1024M 0 rom
The above confirms I have the same block device (sda
), so I don’t need to change anything.
Provisioning
To start the operation, first I make sure I can log in to the server using SSH.
ssh root@IP_PROVIDED_BY_HETZNER
The attempt is successful and it doesn’t ask for my password, so the SSH key is working.
Next, I run the following final command:
nix run github:nix-community/nixos-anywhere -- \
--flake .#hetzner-cloud \
--generate-hardware-config nixos-generate-config \
./hardware-configuration.nix root@IP_PROVIDED_BY_HETZNER
Quick explanation of some of the parts:
--flake .#hetzner-cloud
defines that the flake.nix in question is located in the current directory (.
), and the block of configuration I wish to run is calledhetzner-cloud
(those 2 parts are separated by a#
).--generate-hardware-config nixos-generate-config ./hardware-configuration.nix
instructs to investigate the current hardware of the server and generate thehardware-configuration.nix
in the current directory. This file doesn’t exist yet, but it is already included inside the fileflake.nix
in its modules.
It will take a couple of minutes and it finishes with the following final words in the log:
Installing for x86_64-efi platform.
Installation finished. No error reported.
installation finished!
### Waiting for the machine to become unreachable due to reboot ###
ssh: connect to host IP_PROVIDED_BY_HETZNER port 22: Connection refused
### Done! ###
I then confirm the system is working by SSH-ing into it again from my host using:
ssh root@IP_PROVIDED_BY_HETZNER
And I am inside, greeted by the NixOS PS1: [root@nixos:/]#
in red.
PS: If you SSH-ed into the machine while it was still running Debian, the signature used for SSH will have changed and will not match the one you authorized which is saved inside the file known_hosts
. You can remove all the entries for that IP using:
ssh-keygen -R IP_PROVIDED_BY_HETZNER
and retry the SSH connection, reauthorizing it.
From now on, any time I wish to quickly provision a new VPS with NixOS to test something, I can just use the method above, replacing the IP of the machine I would like to provision. In less than 10 minutes, I have a whole new provisioned machine with the basics already configured for me, without the need to babysit the installation process.