Architecture Overview
The homelab uses a modern Flake-based architecture with automatic discovery logic, enabling a truly declarative infrastructure-as-code approach.
Flake Structure
The repository is organized around Nix Flakes with intelligent automatic discovery:
homelab/
├── flake.nix # Main flake definition
├── flake.lock # Locked dependency versions
├── systems/ # NixOS configurations
├── homes/ # Home Manager configurations
├── droids/ # Nix-on-Droid configurations
├── modules/ # Reusable modules
│ ├── nixos/ # System-level modules
│ ├── home/ # User-level modules
│ └── droid/ # Android-level modules
├── pkgs/ # Custom packages
├── lib/ # Helper functions
├── overlays/ # Package overlays
├── templates/ # Scaffolding templates
└── secrets/ # Encrypted secrets (agenix)
Core Philosophy
The architecture is built on four key principles:
Declarative Everything All infrastructure is defined in code. No imperative configuration steps.
Single Command Invocation Deploy with one command. Updates are atomic and rollback-safe.
Dynamic Discovery Configurations are automatically discovered. No manual imports needed.
Stability nix flake check validates everything before deployment.
Directory Roles
Systems Directory
Location: systems/
Purpose: Top-level NixOS configurations for physical and virtual machines.
Each subdirectory in systems/ with a default.nix is automatically exposed as a nixosConfigurations output.
Structure:
systems/
├── zephyrus/ # ASUS ROG laptop
│ ├── default.nix # Main configuration
│ ├── meta.json # System architecture metadata
│ ├── disko.nix # Disk partitioning
│ └── secrets.yaml # Encrypted secrets
├── lg-laptop/ # LG Gram laptop
├── moonlight/ # Home server
└── docker-node/ # Container host
Example system configuration:
systems/zephyrus/default.nix
{ pkgs , ... }: {
core = {
boot = {
enable = true ;
plymouth . enable = true ;
};
hardware = {
enable = true ;
reportPath = ./facter.json ;
gpu = {
integrated . amd . enable = true ;
dedicated . nvidia = {
enable = true ;
laptopMode = true ;
};
};
bluetooth . enable = true ;
};
networking = {
network-manager . enable = true ;
tailscale . enable = true ;
};
users . soriphoono = {
admin = true ;
shell = pkgs . fish ;
publicKey = "ssh-ed25519 AAAA..." ;
};
};
desktop = {
environments . kde . enable = true ;
features = {
virtualisation . enable = true ;
gaming . enable = true ;
};
};
}
Homes Directory
Location: homes/
Purpose: Home Manager configurations for user environments.
Discovery Pattern:
The flake scans for three naming patterns:
Base Configuration
Pattern: homes/username/Base user configuration used everywhere. homes/soriphoono/default.nix
{ pkgs , ... }: {
core = {
git = {
userName = "soriphoono" ;
userEmail = "[email protected] " ;
};
};
userapps . development . editors . neovim . settings =
import ./nvim { inherit pkgs ; };
}
Global Override
Pattern: homes/username@global/Supplementary config for standalone (non-NixOS) installs. Combined with base and exported as homeConfigurations.username.
Host-Specific Override
Pattern: homes/username@hostname/Machine-specific overrides imported by the NixOS system. Not exported as standalone homeConfiguration. homes/soriphoono@zephyrus/default.nix
{ ... }: {
# Zephyrus-specific user configuration
programs . kitty . settings . font_size = 12 ;
}
Host-specific configurations (user@hostname) are automatically imported by NixOS systems and should not be deployed standalone with home-manager switch.
Droids Directory
Location: droids/
Purpose: Nix-on-Droid configurations for Android devices.
Each directory in droids/ is automatically exposed as a nixOnDroidConfigurations output.
Example:
droids/soriphoono/default.nix
{ pkgs , ... }: {
system . stateVersion = "24.05" ;
core . user . shell = pkgs . fish ;
android-integration = {
am . enable = true ;
termux-open-url . enable = true ;
termux-setup-storage . enable = true ;
xdg-open . enable = true ;
};
}
Modules Directory
Location: modules/
Purpose: Reusable, composable configuration modules organized by scope.
modules/
├── nixos/ # System-level modules
│ ├── core/ # Essential system config
│ ├── desktop/ # Desktop environments
│ └── hosting/ # Server services
├── home/ # User-level modules
│ ├── core/ # Essential user config
│ └── userapps/ # User applications
└── droid/ # Android-level modules
└── core/ # Droid essentials
Module exports:
modules/nixos/default.nix
{ lib , self , ... }: {
default = { ... }: {
imports = lib . discover ./core ++ lib . discover ./desktop ++ lib . discover ./hosting ;
};
}
Automatic Discovery Logic
The heart of the architecture is the automatic discovery system defined in lib/default.nix:
_ : self : _super : {
# Reads a directory and returns { name = path; }
discover = dir :
self . mapAttrs' ( name : _ : {
name = self . removeSuffix ".nix" name ;
value = dir + "/ ${ name } " ;
}) (
self . filterAttrs (
name : type :
( type == "directory" && builtins . pathExists ( dir + "/ ${ name } /default.nix" ))
|| ( type == "regular" && name != "default.nix" && self . hasSuffix ".nix" name )
) ( builtins . readDir dir )
);
# Reads meta.json from a path
readMeta = path :
if builtins . pathExists ( path + "/meta.json" )
then builtins . fromJSON ( builtins . readFile ( path + "/meta.json" ))
else {};
}
How Discovery Works
Scan Directory
The discover function scans a directory for:
Subdirectories containing default.nix
Standalone .nix files (excluding default.nix)
Map to Attribute Set
Each discovered path is mapped to an attribute: {
zephyrus = ./systems/zephyrus ;
lg-laptop = ./systems/lg-laptop ;
moonlight = ./systems/moonlight ;
}
Build Configuration
The flake uses builder functions to construct configurations: nixosConfigurations = lib . mapAttrs mkSystem ( lib . discover ./systems ) ;
The homelab leverages a curated set of flake inputs:
Core
nixpkgs-weekly - Rolling NixOS packages
flake-parts - Modular flake structure
home-manager - User environment management
Secrets
agenix - Age-encrypted secrets
sops-nix - SOPS secrets management
System
disko - Declarative disk partitioning
lanzaboote - Secure Boot with TPM
nixos-facter-modules - Hardware detection
Deployment
comin - GitOps continuous deployment
nix-on-droid - Android support
Input declaration:
inputs = {
nixpkgs-weekly . url = "https://flakehub.com/f/DeterminateSystems/nixpkgs-weekly/0.1.948651" ;
home-manager = {
url = "github:nix-community/home-manager" ;
inputs . nixpkgs . follows = "nixpkgs-weekly" ;
};
agenix = {
url = "github:ryantm/agenix" ;
inputs . nixpkgs . follows = "nixpkgs-weekly" ;
};
disko = {
url = "github:nix-community/disko" ;
inputs . nixpkgs . follows = "nixpkgs-weekly" ;
};
# ... more inputs
} ;
All inputs follow nixpkgs-weekly to ensure consistent package versions across the entire homelab.
System Builders
The flake defines specialized builder functions for each configuration type:
NixOS System Builder
mkSystem = hostName : path : let
meta = lib . readMeta path ;
systemArch = meta . system or "x86_64-linux" ;
pkgs = pkgsFor . ${ systemArch } ;
in
lib . nixosSystem {
inherit pkgs ;
specialArgs = {
inherit inputs self lib hostName ;
};
modules = nixosModules ++ [
path
{ networking . hostName = hostName ; }
];
} ;
Home Manager Builder
mkHome = username : let
basePath = ./homes + "/ ${ username } " ;
globalPath = ./homes + "/ ${ username } @global" ;
hasBase = builtins . pathExists basePath ;
hasGlobal = builtins . pathExists globalPath ;
meta = if hasBase then lib . readMeta basePath else {};
systemArch = meta . system or "x86_64-linux" ;
pkgs = pkgsFor . ${ systemArch } ;
in
inputs . home-manager . lib . homeManagerConfiguration {
inherit pkgs ;
modules = homeManagerModules
++ lib . optional hasBase ( basePath + "/default.nix" )
++ lib . optional hasGlobal ( globalPath + "/default.nix" );
} ;
Nix-on-Droid Builder
mkDroid = name : path : let
pkgs = import nixpkgs-droid {
system = "aarch64-linux" ;
config . allowUnfree = true ;
};
in
nix-on-droid . lib . nixOnDroidConfiguration {
inherit pkgs ;
modules = droidModules ++ [
path
{ core . user . userName = name ; }
];
} ;
Validation and Checks
The flake includes comprehensive validation:
checks = let
# Evaluation checks for all systems
evalSystems = lib . mapAttrs' ( name : conf : {
name = "system-eval- ${ name } " ;
value = conf . config . system . build . toplevel ;
}) self . nixosConfigurations ;
# Evaluation checks for all homes
evalHomes = lib . mapAttrs' ( name : conf : {
name = "home-eval- ${ name } " ;
value = conf . activationPackage ;
}) self . homeConfigurations ;
# Evaluation checks for all droids
evalDroids = lib . mapAttrs' ( name : conf : {
name = "droid-eval- ${ name } " ;
value = conf . activationPackage ;
}) self . nixOnDroidConfigurations ;
in
evalSystems // evalHomes // evalDroids ;
Run nix flake check to validate all configurations before deploying.
Secrets Management
Secrets are managed using agenix with encryption based on host SSH keys:
systems/zephyrus/default.nix
core . secrets = {
enable = true ;
defaultSopsFile = ./secrets.yaml ;
} ;
Encrypted secrets are stored in the repository and automatically decrypted during system activation using the host’s SSH key.
Development Workflow
Make Changes
Edit configurations in their respective directories.
Validate
Run nix flake check to validate all configurations.
Test Locally
Build and test changes: nixos-rebuild build --flake .#hostname
Deploy
Apply changes: nixos-rebuild switch --flake .#hostname
Commit
Commit to Git for GitOps deployment (if using Comin).
Next Steps
Quick Start Deploy your first configuration
Module Reference Explore available modules and options