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 = "soriphoono@gmail.com";
    };
  };
  
  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