Skip to main content

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:
1

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; };
}
2

Global Override

Pattern: homes/username@global/Supplementary config for standalone (non-NixOS) installs. Combined with base and exported as homeConfigurations.username.
3

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:
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

1

Scan Directory

The discover function scans a directory for:
  • Subdirectories containing default.nix
  • Standalone .nix files (excluding default.nix)
2

Map to Attribute Set

Each discovered path is mapped to an attribute:
{
  zephyrus = ./systems/zephyrus;
  lg-laptop = ./systems/lg-laptop;
  moonlight = ./systems/moonlight;
}
3

Build Configuration

The flake uses builder functions to construct configurations:
flake.nix
nixosConfigurations = lib.mapAttrs mkSystem (lib.discover ./systems);

Flake Inputs

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:
flake.nix
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

flake.nix
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

flake.nix
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

flake.nix
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:
flake.nix
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

1

Make Changes

Edit configurations in their respective directories.
2

Validate

Run nix flake check to validate all configurations.
3

Test Locally

Build and test changes:
nixos-rebuild build --flake .#hostname
4

Deploy

Apply changes:
nixos-rebuild switch --flake .#hostname
5

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

Build docs developers (and LLMs) love