Skip to main content

Overview

The homelab repository uses a carefully designed directory structure that separates concerns, enables automatic discovery, and scales from a single machine to dozens of systems.

Root Directory Layout

.
├── systems/          # NixOS host configurations
├── homes/            # Home Manager user configurations
├── droids/           # Nix-on-Droid Android configurations
├── modules/          # Reusable Nix modules
├── environments/     # Non-NixOS system configurations (planned)
├── pkgs/             # Custom packages and overrides
├── lib/              # Utility functions and helpers
├── templates/        # Scaffolding for new configs
├── flake.nix         # Main flake entry point
├── flake.lock        # Pinned input versions
└── secrets.nix       # Secret definitions (encrypted)

Directory Details

systems/ - NixOS Hosts

Role: Top-level NixOS configurations for physical and virtual machines. Each subdirectory represents a single host. Any directory containing a default.nix is automatically discovered and exposed as a nixosConfiguration. Structure:
systems/
├── hostname1/
│   ├── default.nix       # Main system configuration
│   ├── meta.json         # System metadata (optional)
│   ├── hardware.nix      # Hardware-specific settings
│   └── facter.json       # Auto-detected hardware facts
└── hostname2/
    ├── default.nix
    └── meta.json
Example meta.json:
{
  "system": "x86_64-linux",
  "description": "Main server"
}
The system field in meta.json determines the target architecture. If omitted, defaults to x86_64-linux.
Deployment:
nixos-rebuild switch --flake .#hostname1

homes/ - Home Manager Users

Role: User environment configurations. Supports both standalone installations and system-bound configurations. The discovery logic recognizes three naming patterns:
  1. username - Base configuration used everywhere
  2. username@global - Supplementary config for non-NixOS/standalone installs
  3. username@hostname - Machine-specific overrides
Structure:
homes/
├── alice/                # Base config
│   ├── default.nix
│   └── meta.json
├── alice@global/         # Standalone additions
│   └── default.nix
├── alice@workstation/    # Machine-specific
│   └── default.nix
└── bob/
    └── default.nix
How it works:
PatternExported AsUsed By
aliceBase for all alice configsAll systems
alice@globalhomeConfigurations.aliceStandalone installs
alice@workstation-NixOS system workstation only
Machine-specific homes (user@hostname) are imported by the NixOS system itself and are not exported as standalone home configurations.
Deployment:
# Standalone home (combines alice + alice@global)
home-manager switch --flake .#alice

# System-bound home (uses alice + alice@workstation)
nixos-rebuild switch --flake .#workstation
Example user discovery logic from flake.nix:
homeConfigurations = let
  allEntries = builtins.readDir ./homes;
  homeDirs = builtins.attrNames (lib.filterAttrs (_n: v: v == "directory") allEntries);

  # Identify valid user directories (no @, or ending in @global)
  validUsers = lib.filter (
    name: (! lib.hasInfix "@" name) || (lib.hasSuffix "@global" name)
  ) homeDirs;

  # Normalize to username
  usernames = lib.unique (map (name: lib.removeSuffix "@global" name) validUsers);
in
  lib.genAttrs usernames mkHome;

droids/ - Android Devices

Role: Nix-on-Droid configurations for Android devices with Termux. Every directory in droids/ is automatically exposed as a nixOnDroidConfiguration. Structure:
droids/
├── phone/
│   └── default.nix
└── tablet/
    └── default.nix
Deployment:
nix-on-droid switch --flake .#phone

modules/ - Reusable Modules

Role: Encapsulated functionality that can be imported by multiple configurations. Organized by target environment:
modules/
├── nixos/           # NixOS system modules
│   ├── services/
│   ├── hardware/
│   └── networking/
├── home/            # Home Manager user modules
│   ├── shell/
│   ├── editors/
│   └── desktop/
└── droid/           # Nix-on-Droid modules
    └── termux/
Modules are automatically imported via modules/<type>/default.nix, so you can enable them directly in your configurations without manual imports.

environments/ - Non-NixOS Systems

Role: Planned support for managing non-NixOS Linux systems using System Manager.
This directory is reserved for future use and not yet implemented.

pkgs/ - Custom Packages

Role: Custom package definitions and package overrides. Structure:
pkgs/
├── default.nix          # Package index
├── custom-app/
│   └── default.nix
└── overrides/
    └── modified-pkg.nix
Packages defined here are available in all configurations through the overlay system.

lib/ - Utility Functions

Role: Custom helper functions used throughout the flake. Contents:
lib/
└── default.nix          # Discovery functions
The lib/ directory extends nixpkgs.lib with custom functions:
  • discover - Automatic configuration discovery
  • discoverTests - Test file discovery
  • readMeta - Metadata reader for configurations
See Dynamic Discovery for detailed explanations.

templates/ - Scaffolding

Role: Boilerplate templates for creating new systems, modules, or configurations. Usage:
# List available templates
nix flake show

# Initialize from template
nix flake init -t .#template-name

Discovery Patterns

Systems Discovery

From flake.nix line 348:
nixosConfigurations = lib.mapAttrs mkSystem (lib.discover ./systems);
The discover function finds all directories in systems/ containing default.nix.

Droids Discovery

From flake.nix line 351:
nixOnDroidConfigurations = lib.mapAttrs mkDroid (lib.discover ./droids);
Same discovery logic applied to Android configurations.

Homes Discovery

From flake.nix lines 355-374:
homeConfigurations = let
  allEntries = builtins.readDir ./homes;
  homeDirs = builtins.attrNames (lib.filterAttrs (_n: v: v == "directory") allEntries);

  validUsers = lib.filter (
    name: (! lib.hasInfix "@" name) || (lib.hasSuffix "@global" name)
  ) homeDirs;

  usernames = lib.unique (map (name: lib.removeSuffix "@global" name) validUsers);
in
  lib.genAttrs usernames mkHome;
More complex logic to handle the three naming patterns.

Metadata System

Configurations can include a meta.json file to provide additional information: Common fields:
{
  "system": "x86_64-linux",        // Target architecture
  "description": "Web server",      // Human-readable description
  "tags": ["production", "web"]    // Classification tags
}
The metadata is read by lib.readMeta and can influence build parameters like target architecture.

File Organization Best Practices

Keep Configurations Modular

Split large configuration files into logical components:
systems/webserver/
├── default.nix          # Imports other files
├── networking.nix       # Network settings
├── services.nix         # Service definitions
└── security.nix         # Firewall, hardening

Use Meta Files

Always include meta.json for non-standard architectures:
{
  "system": "aarch64-linux"  // Required for ARM systems
}

Document Machine-Specific Overrides

When creating user@hostname homes, document why the override is needed:
# homes/alice@workstation/default.nix
{
  # Workstation needs 4K display scaling
  home.pointerCursor.size = 48;
}

Summary

The directory structure provides:
  • Clear separation between systems, users, and reusable code
  • Automatic discovery eliminating manual import maintenance
  • Scalability from one machine to dozens
  • Flexibility with multiple deployment patterns
  • Metadata support for architecture and classification
This organization makes it easy to navigate the repository and understand where each piece of configuration lives.

Build docs developers (and LLMs) love