NixOS unutar Hetzner cloud instance na Ubuntu hostu s Incusom


Zašto Incus, a ne KVM?

Hetzner cloud serveri nižih kategorija ne podržavaju KVM (nested virtualization). To znači da klasični alati poput QEMU/KVM, VirtualBox ili libvirt ne mogu kreirati prave virtuelne mašine. Ostaje nam kontejnerizacija.

Incus (nasljednik LXD projekta) omogućava pokretanje sistemskih kontejnera koji simuliraju kompletno Linux okruženje — sa vlastitim init procesom (systemd), mrežnim stackom, korisnicima i servisima. Za razliku od Docker kontejnera koji pakuju jednu aplikaciju, Incus kontejner se ponaša gotovo identično pravoj virtuelnoj mašini: ima sopstveni /etc, systemd, SSH server i IPv4/IPv6 adresu.

Ubuntu host (Hetzner cloud)
  └── Incus
        └── nixos-1 (NixOS 26.05 kontejner)
              ├── systemd kao PID 1
              ├── dhcpcd → 10.9*.*.35 (incusbr0 bridge)
              └── SSH, vim, htop, git...

Priprema: Instalacija slike

Na Ubuntu hostu (root@167.233.x.y) s instaliranim Incusom, preuzimamo NixOS 26.05 sliku:

incus image list images: | grep -i nixos
# Nixos 26.05 amd64 (20260602_01:02) — hash: 595f499fba00

Pokretanje kontejnera mora biti s privilegiranim načinom (objašnjenje slijedi):

incus launch 595f499fba00 nixos-1 -c security.privileged=true

Problemi — jedan za drugim

Problem 1: systemd-networkd se vješa

Odmah pri prvom pokretanju kontejnera, IPv4 adresa nije se pojavila. Istraživanjem unutar kontejnera:

incus exec nixos-1 -- systemctl status systemd-networkd

Servis je bio zaglavljen u stanju activating, a u cgroupima se neprestano rađale nove instance procesa sd-mkuserns.

Uzrok: NixOS 26.05 koristi systemd 260 koji uvodi PrivateUsers=true u systemd-networkd. Ova opcija zahtijeva user namespaces (izolacija korisničkih ID-ova). U neprivilegiranim LXC kontejnerima, kernel ne dozvoljava kreiranje user namespacea, pa systemd-networkd beskonačno pokušava pokrenuti sd-mkuserns helper procese — i nikad ne uspije.

Pokušano rješenje: Prebacivanje na dhcpcd umjesto systemd-networkd:

networking.useNetworkd = false;
networking.interfaces.eth0.useDHCP = true;

Problem 2: Kontejner zaglavljen u “initializing”

Nakon prebacivanja na dhcpcd, kontejner je ostajao u stanju systemctl is-system-running → initializing čak i nakon 60 sekundi. Servisi poput dhcpcd bili su u redu čekanja (job queued), ali nikad se nisu pokrenuli.

Uzrok: Mnogi core sistemski servisi bili su neuspješni još pri startu, blokira li basic.target i sve što ovisi o njemu.


Problem 3: Exit code 243/CREDENTIALS — pravi krivac

Detaljnijom analizom:

systemctl --failed

Otkrivamo niz servisa koji padaju s greškom status=243/CREDENTIALS:

  • systemd-tmpfiles-setup-dev-early.service
  • systemd-journald.service
  • systemd-sysctl.service

Uzrok: systemd 260 uveo je ImportCredential= direktive u ove servise. Mehanizam učitavanja kredencijala koristi kernel keyring i memfd_create() sistemske pozive. U LXC kontejnerima, ova infrastruktura nije dostupna, pa servisi izlaze s kodom 243 (EXIT_CREDENTIALS) — još prije nego što počnu raditi.

Ovo je kaskadni kvar: bez systemd-tmpfiles-setup-dev-early, ne može se montirati /dev; bez systemd-journald, nema logiranja; bez ovih, dhcpcd i svi drugi servisi koji o njima ovise ne mogu se pokrenuti.


Problem 4: Greška pri prebacivanju na privilegirani kontejner

U pokušaju da riješimo user namespace problem, probali smo promijeniti kontejner iz neprivilegiranog u privilegirani nakon što je već bio pokrenut:

incus config set nixos-1 security.privileged=true

Greška: Kada kontejner mijenja UID mapiranje (iz 1000000:root u direktni 0:root), svi fajlovi koji su bili kreirani u starom mapiranju postaju nečitljivi. Kontejner se u potpunosti pokvari.

Rješenje: Uvijek kreirati kontejner od početka s privilegiranim načinom:

incus launch 595f499fba00 nixos-1 -c security.privileged=true

Problem 5: Nedostaje /nix/var/nix/profiles/system

Svježi kontejneri iz NixOS 26.05 slike nemaju postavljenu sistemsku profilnu vezu sve dok ne završi uspješan boot. Budući da boot pada (zbog 243/CREDENTIALS), profil se nikad ne kreira.

Posljedica: pri svakom restartovanju, systemctl i nixos-rebuild ne mogu naći trenutni sistem, a bash nije u PATH-u jer /run/current-system/sw/bin ne postoji.

Ručno rješenje (privremeno, da se dođe do mreže):

incus exec nixos-1 -- /bin/sh << 'EOF'
SYSTEM=/nix/store/<hash>-nixos-system-nixos-lxc-26.05.889.b51242d7d436
$SYSTEM/activate
export PATH=/run/current-system/sw/bin
EOF

Problem 6: nixos-rebuild switch pada na D-Bus koraku

Čak i kada uspijemo izgraditi novi sistem, nixos-rebuild switch završava greškom:

Failed to connect to system scope bus via local transport: No such file or directory
Command 'systemd-run ... switch-to-configuration switch' returned non-zero exit status 1.

Uzrok: switch-to-configuration koristi systemd-run koji zahtijeva aktivni D-Bus. U degradiranom stanju boot procesa, D-Bus nije pokrenut.

Rješenje — zaobilaznica:

# Samo izgradnja, bez prebacivanja:
nixos-rebuild build --option sandbox false

# Ručna aktivacija:
NEW=/nix/store/<novi-hash>-nixos-system-nixos-1-lxc-...
nix-env -p /nix/var/nix/profiles/system --set $NEW
$NEW/activate
ln -sf $NEW/init /sbin/init

# Restart kontejnera:
incus restart nixos-1

Problem 7: Konflikt environment.etc s Nix store-om

Pokušaj direktnog dodavanja drop-in fajlova putem:

environment.etc."systemd/system/service.d/fix.conf".text = ''
  [Service]
  ImportCredential=
'';

Rezultira greškom pri build-u:

mkdir: cannot create directory '.../service.d': Permission denied

Uzrok: /etc/systemd/system/ je simbolički link na direktorij u Nix storeu (read-only). Nije moguće dodati fajlove unutar njega putem environment.etc mehanizma.


Problem 8: Incusov zzz-lxc-service.conf kasni

Incus automatski kreira runtime drop-in:

/run/systemd/system/service.d/zzz-lxc-service.conf

Ovaj fajl ispravno onemogućuje sandboxing za LXC, ali se kreira tek nakon što su servisi već propali pri bootu. Nije prisutan kada systemd prvi put čita konfiguraciju.


Konačno rješenje

Arhitektura fix-a

Koristimo systemd.packages u NixOS konfiguraciji da dodamo vendorski nivo globalnog drop-in fajla. Vendorski fajlovi (u lib/systemd/system/) su dio systemd search path-a i čitaju se pri svim boot ciklusima, uključujući prvi.

systemd.packages = [
  (pkgs.writeTextDir "lib/systemd/system/service.d/zz-lxc-cred-fix.conf" ''
    [Service]
    ImportCredential=
    LoadCredential=
    PrivateMounts=no
    PrivateIPC=no
    PrivateTmp=no
    ...
  '')
];

Kompletna configuration.nix

{ modulesPath, lib, pkgs, ... }:

{
  imports = [
    "${modulesPath}/virtualisation/lxc-container.nix"
  ];

  networking.hostName = "nixos-1";

  # Perzistentni ekvivalent Incusovog zzz-lxc-service.conf.
  # Kreira se pri prvom bootu, PRIJE nego što systemd pokuša pokrenuti servise.
  systemd.packages = [
    (pkgs.writeTextDir "lib/systemd/system/service.d/zz-lxc-cred-fix.conf" ''
      [Service]
      ImportCredential=
      LoadCredential=
      PrivateMounts=no
      PrivateIPC=no
      PrivateTmp=no
      PrivateDevices=no
      PrivateNetwork=no
      ProtectSystem=no
      ProtectHome=no
      ProtectClock=no
      ProtectHostname=no
      ProtectKernelTunables=no
      ProtectKernelModules=no
      ProtectKernelLogs=no
      ProtectControlGroups=no
      ProtectProc=default
      ProcSubset=all
      RestrictNamespaces=no
      NoNewPrivileges=no
      ReadWritePaths=
    '')
  ];

  # Dodatno osiguranje za tmpfiles servise
  systemd.services."systemd-tmpfiles-setup-dev-early".serviceConfig.ImportCredential = lib.mkForce "";
  systemd.services."systemd-tmpfiles-setup-dev".serviceConfig.ImportCredential = lib.mkForce "";
  systemd.services."systemd-tmpfiles-setup".serviceConfig.ImportCredential = lib.mkForce "";

  # Nix buildovi bez kernel namespace sandboxinga (LXC ograničenje)
  nix.settings.sandbox = false;

  services.openssh.enable = true;

  environment.systemPackages = with pkgs; [ vim git htop net-tools curl ];

  time.timeZone = "Europe/Sarajevo";

  system.stateVersion = "26.05";
}

Rezultat

Nakon incus restart nixos-1 i čekanja ~45 sekundi:

incus list nixos-1
+---------+---------+---------------------+---...---+-----------+
|  NAME   |  STATE  |        IPV4         |   IPV6   |   TYPE    |
+---------+---------+---------------------+---...---+-----------+
| nixos-1 | RUNNING | 10.9*.*.35 (eth0) |  ...    | CONTAINER |
+---------+---------+---------------------+---...---+-----------+

Preostala samo 2 “failed” jedinice — obje bezopasne, inherentna LXC ograničenja:

  • dev-hugepages.mount — Huge Pages nije dostupan u kontejnerima
  • run-wrappers.mount — tmpfs konflikt s Incus mount-om

Sve ostalo radi: SSH, ping 8.8.8.8, htop, ifconfig, vim, DNS rezolucija.


Sažetak problema i rješenja

#ProblemUzrokRješenje
1systemd-networkd beskonačna petljaPrivateUsers=true + nema user NS u LXCsecurity.privileged=true
2Boot zaglavljen u “initializing”Kaskadni kvar servisaFix 243/CREDENTIALS
3243/CREDENTIALS exitImportCredential= + LXC bez kernel keyring-aGlobalni drop-in s ImportCredential=
4UID mapping kvarPromjena privilegija na živom kontejneruKreirati kontejner odmah kao privilegirani
5Nedostaje /nix/var/nix/profiles/systemNeuspješan prvi bootRučna aktivacija + ponovni pokušaj
6nixos-rebuild switch padasystemd-run treba D-Busnixos-rebuild build + ručna aktivacija
7environment.etc konflikt/etc/systemd/system/ je symlink u Nix storesystemd.packages s vendor drop-in-om
8zzz-lxc-service.conf kasniKreira se runtime, nakon pada servisasystemd.packages kreira perzistentni ekvivalent

Napomena

Generisano od strane Claude 🤖


Ernad Husremović, hernad@bring.out.ba