NixOS + Noctalia V5 + LabWC: Real Hardware

NixOS + Noctalia V5 + LabWC: Real Hardware

NixOS on paper is everything a Linux user could dream of, a purely declarative OS where your entire system configuration lives in a single file, reproducible down to the kernel module. In practice, it's a labyrinth of unknown unknowns, especially if you're not running GNOME or KDE.

I tinkered with NixOS in a VM before. That's the easy part. You can afford to rebuild a hundred times, brick the bootloader, or chase a missing extra- prefix for an hour. Real hardware strips away that safety net and that's where the real learning begins.

The Hard Part: Non-Standard Desktops

The NixOS manual assumes you're running one of the "blessed" desktop environments. If you want LabWC — a minimalist, stacking Wayland compositor in the Openbox tradition — you're stitching things together yourself. Same goes for Noctalia V5, a lightweight shell and bar that isn't in nixpkgs. You pull it from a Cachix tarball, import it as a flake-less module, and wire up the systemd user service yourself.

Then there are daemons like voxtype, a voice-to-text engine that ships with a traditional installer script — the kind that copies files all over your filesystem. In NixOS, that's sacrilege. You don't run installers; you translate them into the Nix language. Every binary path, every environment variable, every autostart entry becomes a line of Nix.

The Wayland Socket Dance

The most finicky part was service ordering. Systemd user services marked for graphical-session.target start before the compositor has even created the Wayland socket. Without it, any Wayland-native binary crashes immediately. The fix is a brutal but effective pattern in ExecStartPre:

while [ ! -S "$XDG_RUNTIME_DIR/${WAYLAND_DISPLAY-wayland-0}" ]; do
  sleep 0.2
done

This polling loop blocks until the socket exists. It's not elegant, but it's reliable. I use this for Noctalia, for polkit-gnome, and for voxtype. Never once has it failed since.

What's in the Config

Let's walk through what this configuration actually does:

Boot. Limine instead of systemd-boot. It's modern, fast, and handles EFI beautifully. The kernel is linuxPackages_7_1 — the latest stable branch. Tmpfs on /tmp at 4G keeps disk writes down, a small mercy for laptop SSDs.

Display. LY as the display manager, configured to auto-login a user called g straight into LabWC. No X11 at all — xwayland.enable = false. Pure Wayland. This simplifies the graphics stack and eliminates an entire class of security surface.

Noctalia V5. Imported from noctalia.cachix.org via fetchTarball. The module injects the bar and shell, and the systemd user service waits for the Wayland socket before launching. It's minimal but functional — a single bar at the top, a launcher, and nothing else.

Audio. PipeWire with full ALSA, PulseAudio compatibility, and WirePlumber for session management. rtkit gives realtime priority. It just works.

Voxtype. The voice-to-text daemon runs as a user service, gated on the Wayland socket. This was the hardest to get right because voxtype's installer expects to create config files at specific paths. In NixOS, those paths don't exist until the service starts, and the service can't start until the display is ready. Chicken-and-egg, solved by the polling loop.

Power Management. AMD microcode updates, power-profiles-daemonthermaldupower, lid-close suspend via logind, and fstrim for the SSD. Zram swap at 30% with zstd compression. This is a laptop-ready configuration.

The User. Just one user, in networkmanagerwheeldockerfuse, and input groups. Docker support means I can run containers without sudo. FUSE means I can mount remote filesystems as a regular user.

Packages. Nothing flashy: kittymicrohtopfzfncdubraveopencodenautilusobs-studiobrightnessctlgrim/slurp/swappy for screenshots. The kind of tools you reach for daily, not the stuff you install and forget.

Nix tuning. Weekly GC with --delete-older-than 7d. Experimental features for flakes and nix-command enabled. Cachix as a binary cache for Noctalia. auto-optimise-store is commented out — it's too slow on virtio HDDs. Instead, I run nix store optimise manually.

Would I Do It Again?

Setting this up took a full evening. Three rebuilds bricked the bootloader. When I switched from systemd-boot to limine i really had weird issues sometime it booted in a bootloader some other times it booted in the past bootloader at a previous NixOS generation. One misconfigured systemd.services block caused a 40-second delay on every login. But the result is a machine where every configuration detail is captured in a single, version-controllable file. If the SSD dies tomorrow, I install NixOS, drop in my configuration.nix and hardware-configuration.nix, and I'm back.

That's the promise of NixOS. It's just harder to collect on when you're not using the defaults.

# /etc/nixos/configuration.nix
{ config, pkgs, lib, ... }:

let
  noctalia = import (fetchTarball "https://github.com/noctalia-dev/noctalia/archive/cachix.tar.gz") {};
in

{
  imports = [
    ./hardware-configuration.nix
    noctalia.nixosModule
  ];

#swapDevices = [{
#  device = "/swapfile";
#  size = 4096; #mb
#}];

  nix.package = pkgs.nixVersions.stable;

  nix.settings = {
    experimental-features = [ "nix-command" "flakes" ];
    extra-substituters = [ "https://noctalia.cachix.org" ];
    extra-trusted-public-keys = [ "noctalia.cachix.org-1:pCOR47nnMEo5thcxNDtzWpOxNFQsBRglJzxWPp3dkU4=" ];
    # auto-optimise-store = true;  # disabled: slow on virtio HDD, run `nix store optimise` periodically
  };
  # system.autoUpgrade.enable = true;  # disabled: consumes CPU/RAM on single-core VM

  nix.gc = {
    automatic = true;
    dates = "weekly";
    options = "--delete-old --delete-older-than 7d";
  };

  # Bootloader & kernel
  #boot.loader.systemd-boot.enable = true;
  # Limine: modern, fast bootloader with great EFI support
  boot.loader.limine.enable = true;
  boot.loader.efi.canTouchEfiVariables = true;
  boot.kernelPackages = pkgs.linuxPackages_7_1;
  boot.tmp.useTmpfs = true;
  boot.tmp.tmpfsSize = "4G";

  # Networking
  networking.hostName = "nixos";
  networking.networkmanager.enable = true;
  networking.firewall.enable = true;
  networking.firewall.allowedTCPPorts = [ 3000 ];
  networking.firewall.allowedUDPPorts = [ 137 138 5353 ];

  # Bluetooth
  hardware.bluetooth.enable = true;

  # Time & locale
  time.timeZone = "Europe/Rome";

  i18n.defaultLocale = "en_US.UTF-8";
  i18n.extraLocaleSettings = {
    LC_ADDRESS = "it_IT.UTF-8";
    LC_IDENTIFICATION = "it_IT.UTF-8";
    LC_MEASUREMENT = "it_IT.UTF-8";
    LC_MONETARY = "it_IT.UTF-8";
    LC_NAME = "it_IT.UTF-8";
    LC_NUMERIC = "it_IT.UTF-8";
    LC_PAPER = "it_IT.UTF-8";
    LC_TELEPHONE = "it_IT.UTF-8";
    LC_TIME = "it_IT.UTF-8";
  };

  # Display server & WM
  services.displayManager.ly.enable = true;

  services.displayManager.autoLogin.enable = true;
  services.displayManager.autoLogin.user = "g";
  services.displayManager.defaultSession = "labwc";
  services.displayManager.sessionPackages = [ pkgs.labwc ];

  programs.noctalia = {
    enable = true;
    systemd.enable = true;
  };

  systemd.user.services.noctalia = {
    description = "Noctalia - A lightweight Wayland shell and bar";
    serviceConfig = {
      Type = "simple";
      Restart = "on-failure";
      RestartSec = "5";
      ExecStartPre = "${pkgs.writeShellScriptBin "noctalia-pre" ''
        while [ ! -S "$XDG_RUNTIME_DIR/''${WAYLAND_DISPLAY-wayland-0}" ]; do
          sleep 0.2
        done
      ''}/bin/noctalia-pre";
      ExecStart = lib.mkForce "${noctalia.package}/bin/noctalia";
    };
  };

  programs.xwayland.enable = false;
  programs.dconf.enable = true;
  security.polkit.enable = true;

  xdg.portal = {
    enable = true;
    extraPortals = [ pkgs.xdg-desktop-portal-wlr pkgs.xdg-desktop-portal-gtk ];
    configPackages = [ pkgs.labwc ];
  };

  # Audio (PipeWire)
  services.pipewire = {
    enable = true;
    alsa.enable = true;
    alsa.support32Bit = true;
    pulse.enable = true;
    wireplumber.enable = true;
  };
  security.rtkit.enable = true;

  systemd.user.services.polkit-gnome = {
    description = "PolicyKit authentication agent (GNOME)";
    wantedBy = [ "graphical-session.target" ];
    serviceConfig = {
      Type = "simple";
      Restart = "on-failure";
      RestartSec = "3";
      ExecStartPre = "${pkgs.writeShellScriptBin "polkit-gnome-pre" ''
        while [ ! -S "$XDG_RUNTIME_DIR/''${WAYLAND_DISPLAY-wayland-0}" ]; do
          sleep 0.2
        done
      ''}/bin/polkit-gnome-pre";
      ExecStart = "${pkgs.polkit_gnome}/libexec/polkit-gnome-authentication-agent-1";
    };
  };

  systemd.user.services.voxtype = {
    description = "Voxtype voice-to-text daemon";
    wantedBy = [ "graphical-session.target" ];
    serviceConfig = {
      Type = "simple";
      Restart = "on-failure";
      RestartSec = "5";
      ExecStartPre = "${pkgs.writeShellScriptBin "voxtype-pre" ''
        while [ ! -S "$XDG_RUNTIME_DIR/''${WAYLAND_DISPLAY-wayland-0}" ]; do
          sleep 0.2
        done
      ''}/bin/voxtype-pre";
      ExecStart = "${pkgs.voxtype-vulkan}/bin/voxtype";
    };
  };

  # Hardware & power
  services.power-profiles-daemon.enable = true;
  services.upower.enable = true;
  services.thermald.enable = true;
  services.logind.settings.Login.HandleLidSwitch = "suspend";
  hardware.cpu.amd.updateMicrocode = true;
  hardware.graphics.enable = true;
  services.fstrim.enable = true;
  zramSwap = {
    enable = true;
    memoryPercent = 30;
    algorithm = "zstd";
  };

  # User accounts
  programs.fuse.userAllowOther = true;

  users.users.g = {
    isNormalUser = true;
    description = "user";
    extraGroups = [ "networkmanager" "wheel" "docker" "fuse" "input" ];
  };

  # Link GSettings schemas so Nautilus/GVFS can find them
  environment.pathsToLink = [ "/share/glib-2.0/schemas" "/share/icons" ];

  fonts.packages = with pkgs; [
    noto-fonts
    fira-code
  ];

  environment.systemPackages = with pkgs; [
	# Dev utils
	curl
	bash
    python3
    git
    unzip
    zip
    gzip

	# User utils
    voxtype-vulkan
    wl-clipboard
    wlr-randr

    labwc

    kitty
    micro
    htop
    fzf
    ncdu
    fastfetch
    nano
    nautilus
    polkit_gnome
    brave
    opencode
    gnome-text-editor
    fuse3
    psmisc


    brightnessctl
    wireplumber
    swappy
    slurp
    grim
    obs-studio
    adwaita-icon-theme
    #glib
    #gsettings-desktop-schemas
  ];

  nixpkgs.config.allowUnfree = true;

  system.stateVersion = "26.11";
}