Nixos Refactor documentation
On this page 12 sections
- modules/features/myfeature.nix
- hosts/fullmetal-desktop/configuration.nix
- Edit the secrets file
- Add under users:
- users:
- newuser:
- hashedPassword: <generate with mkpasswd>
- On the new system, generate hardware config
- hosts/newhostname/configuration.nix
- flake.nix
- Build the configuration
- On the new system, switch to it
NixOS Flake Configuration - Refactoring Documentation
Overview
This documentation covers the comprehensive refactoring of your NixOS flake configuration from a duplicated, user-specific
setup to a modular, multi-user, multi-system architecture.
What Was Changed and Why
- Feature-Based Module System (modules/features/)
Before: Each host configuration contained 300-400 lines of duplicated code with hardcoded features.
After: Created 9 reusable feature modules:
- gaming.nix - Steam, Gamemode
- nvidia.nix - NVIDIA GPU support with container toolkit
- virtualisation.nix - Docker, libvirt, QEMU
- desktop.nix - Hyprland, GNOME, GDM configuration
- printing.nix - CUPS with optional Epson drivers
- bluetooth.nix - Bluetooth support
- laptop.nix - Power management, fingerprint, YubiKey
- ai.nix - Ollama with GPU acceleration
- services.nix - User-specific systemd services
Why: Enable/disable features with simple flags instead of duplicating configuration. Each module is self-contained and
conditionally enabled.
Location: /home/ed/Flake/modules/features/
- Host Profiles (hosts/profiles/)
Before: Each host duplicated the same base configuration.
After: Created reusable profiles:
- desktop.nix - Desktop workstation profile (gaming, nvidia, virtualisation, no suspend)
- laptop.nix - Laptop profile (power management, printing, fingerprint auth)
Why: Define common configurations for device types, then customize per-host.
Location: /home/ed/Flake/hosts/profiles/
- Centralized Shared Configuration (hosts/common.nix)
Before: Empty/unused file, all config duplicated in each host.
After: Consolidated shared configuration:
- All feature module imports
- User management import
- System packages
- Networking base configuration
- Audio (PipeWire)
- Boot loader defaults
- OpenSSH
Why: DRY (Don't Repeat Yourself) - configure once, apply everywhere.
Location: /home/ed/Flake/hosts/common.nix
- Simplified Host Configurations
Before:
- fullmetal-desktop/configuration.nix: ~350 lines
- fullmetal-lappy/configuration.nix: ~400 lines
After:
- fullmetal-desktop/configuration.nix: ~65 lines
- fullmetal-lappy/configuration.nix: ~67 lines
Why: Each host now only contains:
- Hardware configuration import
- Profile import
- Feature flags (enable/disable)
- Host-specific configuration (e.g., NAS mounts)
Structure:
{
imports = [
./hardware-configuration.nix
../common.nix
../profiles/desktop.nix # or laptop.nix
];
features = {
nvidia.enable = true;
gaming.enable = true;
# ... other features
};
# Host-specific config only
}
- Declarative User Management (users/)
Before: Users hardcoded in each host configuration with duplicated code.
After: Centralized user definitions in users/users.nix:
userSettings = {
ed = {
username = "ed";
name = "FullMetal";
homeDirectory = "/home/ed";
hosts = [ "fullmetal-desktop" "fullmetal-lappy" ]; # Which hosts this user is on
hashedPasswordSecret = "users/fullmetal/hashedPassword";
groups = [ "wheel" "docker" "libvirtd" /* ... */ ];
# Home-manager config
enableHomeManager = true;
homeConfig = ../home/home.nix;
desktopEnvironment = "hyprland";
# Feature flags
enableSops = true;
enableNvf = true;
enableAdvancedPrograms = true;
# Secrets
secretsPath = "users/fullmetal";
secretsList = [ "email" "githubpat" /* ... */ ];
};
sophia = {
# Similar structure
hosts = [ "fullmetal-desktop" ]; # Only on desktop
desktopEnvironment = "gnome";
enableAdvancedPrograms = false; # Simplified setup
};
};
Why:
- Define users once, assign to hosts declaratively
- Automatic user creation, password management (SOPS), and home-manager setup
- Per-user feature flags for different experience levels
Location: /home/ed/Flake/users/users.nix
- Universal Home-Manager Configuration (home/)
Before:
- Separate home.nix for ed
- Separate home-sophia.nix for sophia
- Hard-coded paths (/home/ed/...)
- User-specific parameters everywhere
After:
- Single home/home.nix for all users
- User-agnostic modules that read from config.userSettings
- Dynamic paths using ${config.home.homeDirectory}
- Conditional logic based on user feature flags
Structure:
home/
├── home.nix # Universal entry point
├── base/
│ ├── packages.nix # Conditional on enableAdvancedPrograms
│ ├── programs.nix # Bash, git, starship (conditional)
│ ├── sops.nix # Secrets (conditional on enableSops)
│ ├── nvf.nix # Neovim (conditional on enableNvf)
│ └── stylix.nix # Theming
├── desktops/
│ ├── default.nix # Auto-loader
│ ├── hyprland/ # Conditional on desktopEnvironment == "hyprland"
│ └── gnome/ # Conditional on desktopEnvironment == "gnome"
└── hosts/
├── default.nix # Auto-loader
├── fullmetal-desktop.nix
└── fullmetal-lappy.nix
Why:
- Single source of truth
- Easy to maintain
- Users automatically get appropriate configuration based on their feature flags
- SOPS Integration
System-level (users/user-management.nix):
- Uses SSH host key (/etc/ssh/ssh_host_ed25519_key)
- Manages user password hashes
- Automatically creates secrets for all users on host
Home-level (home/base/sops.nix):
- User-specific secrets (API keys, tokens, SSH keys)
- Conditional on enableSops flag
- Dynamically generates secrets from secretsList
Why: Secure secret management without storing plaintext in git.
How to Add New Features
- Create a new feature module in modules/features/:
modules/features/myfeature.nix
{ lib, config, pkgs, ... }:
{
options.features.myfeature = {
enable = lib.mkEnableOption "my awesome feature";
# Add more options as needed
someOption = lib.mkOption {
type = lib.types.str;
default = "default-value";
description = "Some option for my feature";
};
};
config = lib.mkIf config.features.myfeature.enable {
# Your configuration here
environment.systemPackages = with pkgs; [
# packages
];
services.myservice = {
enable = true;
# ...
};
};
}
- Import the module in hosts/common.nix:
imports = [
../modules/features/myfeature.nix
# ... other imports
];
- Enable it in host configs as needed:
hosts/fullmetal-desktop/configuration.nix
features.myfeature = {
enable = true;
someOption = "custom-value";
};
How to Add New Users
- Add user definition to users/users.nix:
userSettings = {
# ... existing users
newuser = {
# Basic info
username = "newuser";
name = "New User";
homeDirectory = "/home/newuser";
gitHubUser = "newuser-github"; # optional
groups = [ "wheel" ]; # Add appropriate groups
# Host assignment - which systems should this user be on?
hosts = [ "fullmetal-desktop" ];
# Password secret path
hashedPasswordSecret = "users/newuser/hashedPassword";
# Home-manager setup
enableHomeManager = true;
homeConfig = ../home/home.nix;
desktopEnvironment = "hyprland"; # or "gnome"
# Feature flags (customize experience)
enableSops = false; # true if user needs secrets
enableNvf = true; # Enable custom Neovim
enableAdvancedPrograms = true; # Advanced bash config, tools
# Secrets (if enableSops = true)
secretsPath = "users/newuser";
secretsList = [ "email" "githubpat" ];
};
};
- Add password to SOPS if using secrets:
Edit the secrets file
cd /home/ed/Flake
sops sops/secrets.yaml
Add under users:
users:
newuser:
hashedPassword:
Generate hashed password:
mkpasswd -m yescrypt
- Rebuild - the user will be automatically created:
sudo nixos-rebuild switch --flake .
How to Add New Hosts/Systems
- Create Hardware Configuration
On the new system, generate hardware config
nixos-generate-config --show-hardware-config > /tmp/hardware-configuration.nix
Copy this to hosts/newhostname/hardware-configuration.nix
- Create Host Configuration
hosts/newhostname/configuration.nix
{ config, pkgs, lib, hostname, inputs, ... }:
{
imports = [
./hardware-configuration.nix
../common.nix
../profiles/desktop.nix # or laptop.nix, or create custom
];
# Hostname
networking.hostName = "newhostname";
# Feature flags
features = {
gaming.enable = true;
nvidia.enable = false;
virtualisation.enable = true;
desktop = {
hyprland.enable = true;
gnome.enable = false;
autoSuspend = false;
};
# ... other features
};
# Host-specific configuration
# (monitors, custom services, mounts, etc.)
}
- Add to Flake
flake.nix
outputs = { ... }: {
nixosConfigurations = {
# ... existing hosts
newhostname = mkHost {
hostname = "newhostname";
system = "x86_64-linux"; # or aarch64-linux, etc.
};
};
};
- Assign Users to Host
Edit users/users.nix and add the hostname to users' hosts list:
ed = {
# ...
hosts = [ "fullmetal-desktop" "fullmetal-lappy" "newhostname" ];
};
- Build and Deploy
Build the configuration
nixos-rebuild build --flake .#newhostname
On the new system, switch to it
sudo nixos-rebuild switch --flake .#newhostname
Architecture Diagram
flake.nix
├── nixosConfigurations
│ ├── fullmetal-desktop ──┐
│ └── fullmetal-lappy ────┤
│ │
├── modules/features/ ◄──────┤
│ ├── gaming.nix │
│ ├── nvidia.nix │
│ ├── virtualisation.nix │
│ ├── desktop.nix │
│ ├── laptop.nix │
│ └── ... │
│ │
├── hosts/ │
│ ├── common.nix ◄─────────┤ (imports all features)
│ ├── profiles/ │
│ │ ├── desktop.nix │
│ │ └── laptop.nix │
│ ├── fullmetal-desktop/ │
│ │ └── configuration.nix◄┘
│ └── fullmetal-lappy/
│ └── configuration.nix
│
├── users/
│ ├── users.nix ──────────► Declarative user definitions
│ └── user-management.nix ► Auto-wires users to hosts
│
└── home/
├── home.nix ───────────► Universal entry point
├── base/ ──────────────► User-agnostic modules
├── desktops/ ──────────► DE-specific configs
└── hosts/ ─────────────► Host-specific home configs
Key Principles
- Conditional Everything - Use lib.mkIf to enable/disable based on options
- User-Agnostic - Modules read from config.userSettings, never hardcode users
- DRY - Define once, reuse everywhere
- Feature Flags - Toggle features per-host and per-user
- Declarative Users - Define users centrally, assign to hosts with hosts = [ ... ]
- Single Home Config - One home.nix serves all users with conditional logic
Common Tasks
Change a User's Desktop Environment
Edit users/users.nix:
ed = {
# ...
desktopEnvironment = "gnome"; # was "hyprland"
};
Add a Package to All Systems
Edit hosts/common.nix:
environment.systemPackages = with pkgs; [
# ... existing packages
newpackage
];
Add a Package to Specific User
Edit home/base/packages.nix and add conditional logic:
home.packages = with pkgs; [
# ... existing
] ++ lib.optionals (currentUser.username == "ed") [
special-package
];
Disable a Feature on One Host
Edit host configuration:
features.gaming.enable = false;
File Reference
| File | Purpose |
|---|---|
| flake.nix | Entry point, defines all systems |
| modules/features/*.nix | Reusable feature modules |
| hosts/common.nix | Shared configuration for all hosts |
| hosts/profiles/*.nix | Reusable host profiles |
| hosts/*/configuration.nix | Per-host configuration |
| users/users.nix | Declarative user definitions |
| users/user-management.nix | Auto-wires users to hosts |
| home/home.nix | Universal home-manager entry |
| home/base/*.nix | User-agnostic home modules |
| home/desktops/*/ | Desktop environment configs |
| sops/secrets.yaml | Encrypted secrets |