No description
Find a file
josephembrey 97a27818c6
All checks were successful
Check / eval (push) Successful in 4s
feat(networking): add headscale module for self-hosted mesh networking
Wraps headscale (server) and tailscale (client). Any host with
enable=true joins the tailnet. One host runs the coordination server.
mesh=true ACL preset allows all-to-all; disable for custom ACLs.

Gateway submodule for VPS port forwarding via DNAT to tailscale IPs
with selective/all modes, per-port destinations, and explicit
masqueradeTcp/masqueradeUdp control.
2026-03-27 01:30:43 -04:00
.forgejo/workflows refactor(modules): add CI checks, fix GID, clean guards, add plex group 2026-03-24 15:28:08 -04:00
docs feat(modules): add auth, localDomain, localAuth options to web services 2026-03-24 15:55:00 -04:00
lib feat(devshell): add python3 to prevent uv downloading standalone python 2026-03-18 20:25:44 -04:00
modules feat(networking): add headscale module for self-hosted mesh networking 2026-03-27 01:30:43 -04:00
.gitignore chore: gitignore TODO.md 2026-03-23 20:22:33 -04:00
CHANGELOG.md chore(release): v0.1.4 2026-03-24 16:46:05 -04:00
CLAUDE.md docs(CLAUDE.md): update key rules for conditional auth pattern 2026-03-24 16:23:33 -04:00
flake.lock refactor: remove newsletter-rss and waylog (personal project modules) 2026-03-06 02:49:17 -05:00
flake.nix refactor(modules): add CI checks, fix GID, clean guards, add plex group 2026-03-24 15:28:08 -04:00
LICENSE chore: add MIT license 2026-03-06 12:30:53 -05:00
README.md fix(modules): smart local domain filter, jellyfin compat shim 2026-03-24 16:08:14 -04:00
TODO.md refactor: rename updatekeys.nix, add optional path arg, add README notes 2026-03-05 23:48:28 -05:00

nix-homelab

Opinionated NixOS module framework for self-hosted infrastructure.

Not a composable library — importing means opting into opinions. The value is the wiring: service modules that handle users, groups, containers, reverse proxying, firewall rules, impermanence, and secrets so you don't have to. Escape hatch is always "stop using that module and build your own."

Quick Start

Add nix-homelab as a flake input and import nixosModules.default:

# flake.nix
{
  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-25.11";
    nix-homelab.url = "...";
    # Your own inputs — nix-homelab doesn't carry these:
    sops-nix.url = "github:Mic92/sops-nix";
    impermanence.url = "github:nix-community/impermanence";
    disko.url = "github:nix-community/disko";
  };

  outputs = {self, nixpkgs, nix-homelab, ...}: {
    nixosConfigurations.myserver = nixpkgs.lib.nixosSystem {
      specialArgs = {
        inherit self;
        hosts = nix-homelab.lib.discover {dir = "${self}/hosts";};
        users = nix-homelab.lib.discover {dir = "${self}/users";};
      };
      modules = [
        ./hosts/myserver
        nix-homelab.nixosModules.default
        # your other modules (sops-nix, impermanence, etc.)
      ];
    };
  };
}

Then in your host config:

# hosts/myserver/default.nix
{hosts, users, ...}: {
  josephembrey.registry = {
    enable = true;
    self = "myserver";
    admins = [ "joseph" ];
    inherit hosts users;
  };

  # Enable what you want — nothing turns on by default
  josephembrey.basics.enable = true;
  josephembrey.shell.enable = true;
  josephembrey.jellyfin = {
    enable = true;
    localDomain = "jellyfin.local.example.com";
  };
}

Discovery

lib.discover scans a directory for subdirectories, imports meta.nix from each, and returns an attrset:

hosts = nix-homelab.lib.discover {dir = "${self}/hosts";};
# → { mercury = { ip.local = "10.0.10.20"; keys.ssh = "ssh-ed25519 ..."; }; }

users = nix-homelab.lib.discover {dir = "${self}/users";};
# → { joseph = { keys = { ssh = "ssh-ed25519 ..."; age = "age1..."; }; }; }

Directories prefixed with _ are skipped. The metaFile parameter defaults to meta.nix but is configurable.

Host meta.nix

Plain Nix attrset — no imports, no function arguments. Schema is enforced by module options, not the file itself.

# hosts/myserver/meta.nix
{
  ip.local = "10.0.10.20";
  ip.public = "";
  ssh.port = 22;
  keys.ssh = "ssh-ed25519 AAAA...";
  keys.age = "age1...";
  wg.mesh.pubkey = "base64key=";
  wg.gateway.pubkey = "base64key=";
  wg.gateway.port = 51821;
}

User meta.nix

# users/joseph/meta.nix
{
  keys = {
    ssh = "ssh-ed25519 AAAA...";
    age = "age1...";
  };
}

User SSH keys are automatically added to root's authorized_keys on all hosts. Age keys prefixed age (e.g. age, age-wsl) are used as admin keys for sops.

Modules

All modules live under josephembrey.* and gate behind enable = true. Importing nixosModules.default loads everything but activates nothing.

System Modules

Module Purpose
josephembrey.registry Host/user metadata, hostname, admin privileges, automatic group membership
josephembrey.basics Opinionated defaults: flakes, SSH hardening, GC, common CLI tools
josephembrey.impermanence Ephemeral root with persistent storage (persistPath option)
josephembrey.podman Container runtime with Docker compat, auto-prune, defaults helper
josephembrey.shell Zsh, Starship prompt, neovim, tmux, fzf, zoxide, aliases
josephembrey.motd SSH login banner with system stats (rust-motd)

Service Modules

~42 service modules covering media, networking, productivity, infrastructure, and more. Services are organized as single .nix files — domain categories (media/, networking/) group large clusters, everything else sits flat in services/. Each module declares its implementation style via meta.type: "nixos" (wraps a NixOS service module), "container" (OCI/podman), or "custom" (original logic).

Each follows the same pattern: enable it, set a domain if web-exposed, point secrets at sops paths.

josephembrey.immich = {
  enable = true;
  domain = "photos.example.com";
  mediaLocation = "/data/photos";
};

Web-exposed services automatically get a Caddy reverse proxy with Authelia authentication (import auth).

Services using OCI containers automatically pick up Podman defaults (timezone, auto-start, image pull policy) via config.josephembrey.podman.defaults.

Media services (jellyfin, sonarr, radarr, etc.) share a configurable group option (default "media") — the core module auto-creates the group and adds all users to it.

Guard Patterns

Modules never assume external dependencies are present. Three patterns handle this:

Required — fail with a clear message:

assertions = [{
  assertion = options ? environment.persistence;
  message = "josephembrey.impermanence requires nix-community/impermanence.";
}];

Optional external — enhance if available, skip silently:

(lib.mkIf (config ? sops) {
  sops.age.sshKeyPaths = ["/persist/root/.ssh/id_ed25519"];
})

Optional internal — enhance if another josephembrey module is enabled:

(lib.mkIf (config.josephembrey ? impermanence && config.josephembrey.impermanence.enable) {
  environment.persistence.${config.josephembrey.impermanence.persistPath}.directories = [
    {directory = "/var/lib/jellyfin"; user = "jellyfin"; group = "jellyfin";}
  ];
})

This means you choose which NixOS modules to import (sops-nix, impermanence, etc.) and nix-homelab adapts automatically.

Automatic Groups

Services declare the groups they need via meta.groups. The core module collects all groups from enabled services, creates them, and adds all registered users:

# If audiobookshelf (media), sonarr (media), and backups (backups) are enabled:
# → groups "media" (GID 1200) and "backups" (GID 1100) are created
# → all users from josephembrey.registry.users are added to both groups

No manual group management needed.

Dev Shell

lib.mkDevShell provides a batteries-included development environment. Consumer flakes compose on top:

devShells.default = nix-homelab.lib.mkDevShell {
  inherit pkgs system;
  inherit (inputs) deploy-rs;
  prekConfig = ./prek.toml;  # optional pre-commit config
};

Included tools: alejandra, deadnix, shellcheck, shfmt, typos (linting), age, sops, ssh-to-age (secrets), deploy-rs, prek (pre-commit), curl, fd, git, jq, just, mdsh, openssl, tmux, unzip, wget, wireguard-tools (utilities).

Sops Integration

nix-homelab auto-generates .sops.yaml from your configuration. sopsGenerateYaml scans all nixosConfigurations for sops.secrets.* usage and produces creation rules where each host can only decrypt secrets it actually uses. Admin keys (from user age* metadata) can decrypt everything.

# Regenerate .sops.yaml and re-encrypt all secrets
sops-updatekeys

The sops-updatekeys script is included in the dev shell.

Service Table Generation

lib.generateServicesTable produces a markdown table of all enabled services across hosts, using metadata from each module's meta option:

table = nix-homelab.lib.generateServicesTable {
  inherit lib nixosConfigurations;
  readme = builtins.readFile ./README.md;  # optional: splice into existing README
  # darkMode = true;  # optional: simple icons adapt to light/dark (black/white via prefers-color-scheme)
};

Insert <!-- BEGIN SERVICE LIST --> and <!-- END SERVICE LIST --> markers in your README and the table is placed between them.

Flake Outputs

Output Description
nixosModules.default All system and service modules
lib.discover Directory scanner for meta.nix files
lib.mkDevShell Parameterized dev shell builder
lib.sopsGenerateYaml Generate .sops.yaml from NixOS configs
lib.mkSopsUpdatekeys Shell script derivation for secret key updates
lib.generateServicesTable Markdown service table generator
lib.mkFormatter Alejandra formatter wrapper
devShells.default Base dev shell

Inputs

nix-homelab only carries inputs it directly uses: nixpkgs and deploy-rs. Everything else (sops-nix, impermanence, disko, home-manager) is the consumer's responsibility. Modules detect what's available at eval time via guard patterns.