Claude 🤖, evo ti "hetzner" server: Particioniraj, podigni zfs, pa instaliraj NixOS!


Naslov je manje-više ono što sam ukucao u terminal: “Claude, evo ti ‘hetzner’ host — particioniraj, podigni ZFS, pa instaliraj NixOS!”. Rezultat — potpuno nov dedicirani server sa NixOS-om na ZFS-u, priključen na našu colmena flotu i tailnet, instaliran skoro u potpunosti autonomno. Server u tekstu zovemo hetzner-2; svi ostali identifikatori (IP adrese, serijski brojevi diskova, imena susjednih hostova, domeni) su u ovom zapisu generalizovani radi privatnosti.

Polazna tačka

Hetzner dedicirani server, butovan u Rescue System (Debian), dostupan preko SSH ključa:

  • 2× NVMe SSD od 1.92 TB
  • Intel Core i9-13900, 62 GB RAM
  • UEFI boot (bitno — stariji hetzner-1 koristi legacy BIOS)

Cilj: ZFS mirror (RAID1) root, dedicirana swap particija od 32 GB, puna iskorištenost diskova, bez enkripcije.

Napomena: Hetzner-ov installimage ne podržava ZFS root, pa je sve rađeno ručno po OpenZFS / NixOS-on-ZFS receptu.

Korak 1 — skripta za wipe i instalaciju

Prvo je nastala skripta hetzner-2-dedicated-wipe-and-install-nixos-swap.sh, izvedena iz našeg postojećeg recepta za hetzner-1, ali prilagođena novom hardveru. Ključne razlike:

  • ZFS native mirror preko sirovih particija (zpool create ... root_pool mirror <disk1>-part3 <disk2>-part3) — ne ZFS-na-mdraid-u
  • Dedicirana swap particija od 32 GiB, kao mdraid1 mirror (/dev/md/swap)
  • UEFI: nema više bios_grub particije; GRUB se instalira kao EFI sa mirroredBoots na dvije nezavisne ESP particije (jedna po disku), pa svaki disk može samostalno butati
  • Particija za podatke ide do 100% — diskovi se koriste u cijelosti (~1.65 TB iskoristivo u mirroru)

Šema particija po disku:

part1  1 GiB   ESP (FAT32)        -> /boot/efi, /boot/efi2 (mirroredBoots)
part2  32 GiB  swap               -> mdraid1 mirror /dev/md/swap
part3  ostatak ZFS root_pool      -> mirror (native ZFS)

NixOS configuration.nix koji skripta generiše bira GRUB-EFI umjesto systemd-boot (jer /boot živi na ZFS-u, a systemd-boot ne zna čitati kernele sa ZFS-a):

boot.loader.grub = {
  enable = true;
  efiSupport = true;
  zfsSupport = true;
  mirroredBoots = [
    { devices = [ "nodev" ]; path = "/boot/efi";  efiSysMountPoint = "/boot/efi"; }
    { devices = [ "nodev" ]; path = "/boot/efi2"; efiSysMountPoint = "/boot/efi2"; }
  ];
};

Cijela logika (zap diskova, particionisanje parted-om, kreiranje root_pool mirrora i swap mdraid-a, formatiranje ESP-ova, nixos-install) živi u jednoj skripti, koja se pokreće iz Rescue System-a:

# kopiraj skriptu na server i pokreni je u rescue-u (detached, sa logom)
scp hetzner-2-dedicated-wipe-and-install-nixos-swap.sh root@<SERVER_IP>:/root/
ssh root@<SERVER_IP> \
  "nohup bash /root/hetzner-2-dedicated-wipe-and-install-nixos-swap.sh \
     > /root/install.log 2>&1 &"

Skripta je idempotentna koliko može biti — na početku ruši postojeće montiranja/pool-ove i nule mdraid superblokove, pa se može pokretati više puta dok se sve ne slegne. Na kraju radi reboot u svježe instaliran NixOS.

Korak 2 — automatizacija rescue sesije (Robot API + pass)

Da bi instalacija tekla, server mora biti u Rescue System-u. Umjesto ručnog klikanja po Hetzner Robot panelu, Claude je iskoristio Robot webservice kredencijale koje već čuvamo u pass:

WSUSER=$(pass hetzner/ws-user)
WSPASS=$(pass hetzner/ws-password)

Provjera servera i SSH ključeva, aktivacija rescue-a i hardverski reset — sve preko Robot API-ja (robot-ws.your-server.de):

# aktiviraj rescue (linux) sa našim ključem
curl -s -u "$WSUSER:$WSPASS" -X POST \
  "https://robot-ws.your-server.de/boot/<SERVER_IP>/rescue" \
  --data-urlencode "os=linux"

# hardverski reset -> server butuje u rescue
curl -s -u "$WSUSER:$WSPASS" -X POST \
  "https://robot-ws.your-server.de/reset/<SERVER_IP>" \
  --data-urlencode "type=hw"

Otisak našeg SSH ključa je provjeren spram ključeva registrovanih u Robot-u prije reseta, da slučajno ne ostanemo zaključani van rescue sistema. Nakon ~25 sekundi server je bio u rescue-u, ZFS kompajliran (openzfs_install), a skripta pokrenuta detached uz logovanje.

Ova sitnica — čitanje kredencijala iz pass — je ono što cijeli proces čini automatskim: nema ručne prijave na web panel, nema kopiranja lozinki.

Cijeli ciklus se ponavljao više puta dok se sve nije sleglo (jedan raniji pokušaj je čak završio kao standardni installimage Ubuntu na mdraid-u — pa smo se vratili u rescue i odradili ZFS kako treba).

Korak 3 — colmena konfiguracija (infra-hodi)

Server se ne ostavlja “ručno instaliran” — ulazi u našu deklarativnu flotu. Dodali smo novi host u colmena hive:

  • hosts/hetzner/hetzner-2/default.nix — uvozi zajednički _common (isti set paketa kao hetzner-1), GRUB-EFI mirroredBoots, boot.swraid.enable za swap mirror, te stvarni hardware-configuration.nix sa servera (“respect the hardware”)
  • hive/hetzner-2.nix — colmena node, deployment.targetHost = javni IP
  • hive.nix — registracija hosta i pin na nixpkgs-26.05 (najnoviji stable)

Najnoviji NixOS (26.05) smo dodali kao novi pin:

# nixpkgs-26.05/default.nix
import (builtins.fetchTarball {
  url = "https://github.com/NixOS/nixpkgs/archive/<rev>.tar.gz";
  sha256 = "sha256-...";
})
# hive.nix
nodeNixpkgs = {
  hetzner-2 = (import ./nixpkgs-26.05);  # prati najnoviji stable
  ...
};

Eval je odmah uhvatio jedan benigni warning (mdadm: Neither MAILADDR nor PROGRAM has been set ... mdmon ... crash) jer host ima md swap — riješeno sa boot.swraid.mdadmConf = "MAILADDR root@localhost";.

Korak 4 — deploy preko colmena

Build prvo (sigurnosna provjera da se closure uopšte gradi), pa deploy:

nix-shell --run "colmena build --on hetzner-2"
nix-shell --run "colmena apply switch --on hetzner-2"

SSH agent je dobio naš ključ, a stari host-key zapis je osvježen prije konekcije. NixOS 26.05 donosi kernel 6.18 i GCC 15 — GRUB se rekompajlira iz izvora (sa ZFS podrškom), zatim se closure gura na server i aktivira. Rezultat:

$ nixos-version
26.05pre-git (Yarara)
$ zpool list -H -o name,health
root_pool   ONLINE
$ swapon --show
/dev/md127  32G

(colmena na kraju prijavi exit 4 zbog reload-a dbus-broker user unita na bezglavom serveru — kozmetički; sistemski dio aktivacije prolazi bez ijednog neuspjelog unita.)

Korak 5 — priključenje na tailnet (headscale)

hetzner-1 je na tailscale-u, pa i hetzner-2 treba biti. Modul services/tailscale samo podiže tailscaled; prijava na headscale ide out-of-band preauth ključem. Ključ smo generisali na headscale serveru (headscale-server) za korisnika tailuser:

# na headscale-server
headscale preauthkeys create -u tailuser -e 24h
# na hetzner-2
tailscale up \
  --login-server https://headscale.example.com \
  --authkey <preauth-key> \
  --hostname hetzner-2

I server je na mreži:

100.64.0.x   hetzner-2   tailuser   linux   -

Preauth ključ nikad nije ispisan u čitljivom obliku — prebačen je direktno iz generisanja u tailscale up.

Rezultat

Za jednu sesiju, iz jedne rečenice:

  • ✅ NixOS 26.05 na ZFS mirroru, UEFI GRUB sa dvije nezavisne ESP particije
  • ✅ 32 GB swap kao mdraid1 mirror, puna iskorištenost diskova
  • ✅ Host u colmena floti, deklarativno upravljan
  • ✅ Na tailnet-u preko headscale-a
  • ✅ Sve commit-ovano u infra-hodi

Najzanimljiviji dio nije ni ZFS ni colmena — nego to što je kombinacija pass-a i Robot API-ja dozvolila agentu da sam aktivira rescue i resetuje fizički server, bez ijednog klika u web panelu.

Napomena

Generisano od strane Claude 🤖


Ernad Husremović, hernad@bring.out.ba