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
doneThis 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-daemon, thermald, upower, 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 networkmanager, wheel, docker, fuse, 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: kitty, micro, htop, fzf, ncdu, brave, opencode, nautilus, obs-studio, brightnessctl, grim/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";
}