A NixOS configuration flake for a Hyprland-based desktop, built with
flake-parts and import-tree.
Every sub-directory of modules/hosts/ is a host: its name becomes the
nixosConfiguration (and hostname), and its private modules live alongside it.
Everything else under modules/ is shared across all hosts. CPU/GPU variant
files (e.g. modules/system/hardware/cpu/amd.nix) are auto-selected per host
from the cpu/gpu options, and each host picks its desktop with the
desktop option (hyprland or kde).
nix develop github:qweered/hyprnixos#installIt prints its own help on entry. The command takes the host name (the directory
under modules/hosts/):
# partition + format the disk and install the whole system (DESTRUCTIVE)
hyprnixos-install new-host
reboot
# from the booted system, every later rebuild is just plain nh:
nh os switch # aliased to nh-switchThe target disk is not a command-line argument — it comes from the host's
filesystems.nix. To retarget, edit disko.devices.disk.<name>.device there
before running the install (hyprnixos-install rejects extra arguments so a
stale sda/nvme0n1 guess can't silently wipe the wrong disk).
hyprnixos-install follows the standard
NixOS install flow in
two phases so the system closure never has to fit in the live ISO's RAM:
- format + mount —
diskopartitions/formats the disk (device fromfilesystems.nix) and mounts it at/mnt. Only the small disko config is evaluated here, so the tmpfs store stays nearly empty. - install —
nixos-installbuilds the closure straight onto/mnt(the real disk) with a live nix-output-monitor progress graph, then the temporary swapfile is torn down (including on failure).
Low-RAM machines are handled by that two-phase split, not by swap alone. A NixOS live ISO keeps
/nix/store's writable layer in a RAM-backed tmpfs, so building a large desktop closure there runs out of memory ("No space left on device"). Streaming the closure to/mntin phase 2 sidesteps that entirely. The swapfile is extra insurance for the flake evaluation and any from-source builds, which still touch RAM; it lives only at/mnt/.install-swapduring the install and isswapoff'd and deleted afterwards, so it never reaches the installed system (no swap in your declarative config). It defaults to 8G — bump thecount=ininstall-shell.nixif a build still runs out.
A bare host installs from the flake in the current directory (the clone you're
in); pass a full <flake#host> to install from elsewhere instead (e.g.
hyprnixos-install github:qweered/hyprnixos#new-host). Once you've rebooted,
nothing extra is needed — modules/system/programs/nix.nix is now live, so the
plain nh os switch (the nh-switch alias) already knows every cache, and the
hostname was set from the host directory name at install time, so nh resolves
the right configuration without a -H flag.
-
new-hostneeds real values before it'll install. The hostname is derived from the directory name, so there's nothing to set for it — butcpu/gpuinmodules/hosts/new-host/options.nixareCHANGE MEplaceholders, anddeviceinmodules/hosts/new-host/filesystems.nixdefaults to/dev/vda(a VM disk). Pointdeviceat the host's real disk before formatting anything for real — it is the only place the install reads the target from. -
sops secrets are keyed to the host's SSH key (converted to an age key). A brand-new host isn't a recipient of anything yet, so secrets won't decrypt at activation — enroll it before the first real switch: copy its
/etc/ssh/ssh_host_ed25519_key.pubintomodules/hosts/<name>/(orssh-keyscan -t ed25519 <host> | cut -d' ' -f2-),git addit, and runnix run .#sops-sync. The.pubis committed verbatim so it stays checkable againstssh-keyscan; the age conversion happens at build time.Hosts are not trusted by default. Each sops file has its own recipient list:
secrets/common.yaml(all hosts),secrets/users/<name>.yaml(only hosts that enable that user),secrets/hosts/<name>.yaml(only that host). A host can never decrypt a secret outside its scopes, and the nix declarations mirror the same boundaries (modules/system/security/sops.nix, themodules/users/<name>.nixprofiles,modules/hosts/<name>/)..sops.yamlis generated from the host and user options bynix run .#sops-sync— never edit it by hand; a pre-commit hook rejects drift.
If you'd rather not use the shell, run the same steps directly — pass the
caches with --option extra-substituters "<urls>" and
--option extra-trusted-public-keys "<keys>" (the exact values are in
modules/system/programs/nix.nix). The wrappers in
modules/flake-parts/install-shell.nix show the full command for each step.
- Replace credentials in
modules/home/programs/programming/git.nix - For new hosts, add a directory under
modules/hosts/<name>/ - For new users, add a profile under
modules/users/<name>.nixand enable it on a host withhyprnixos.users.<name>.enable = true - Add or edit secrets with
sops secrets/<scope>.yaml(decrypts with the GPG admin key from.sops.yaml), then declare them at the matching scope: common ones inmodules/system/security/sops.nix, user ones inmodules/users/<name>.nix, host ones inmodules/hosts/<name>/
Key creation and enrollment are handled automatically by the Limine bootloader:
on the first nixos-rebuild where no keys exist yet, it runs sbctl create-keys,
sbctl enroll-keys, and signs the bootloader. This only triggers when the host
sets secureBootConfigured = true and /var/lib/sbctl does not already exist.
Only the firmware-side steps remain manual, because they require physical presence and cannot be scripted:
systemctl reboot --firmware-setup- In the firmware, enable Secure Boot Setup Mode (or erase the existing
keys).
sbctl enroll-keysonly succeeds while the Platform Key is cleared. Take care on ThinkPad and Framework 13. - Set
secureBootConfigured = truein the host options. - Run
nixos-rebuild boot --flake .— the module now creates, enrolls, and signs with no further input. - Reboot into the firmware once more and confirm Secure Boot is enabled.