feat(nixos): add pikachu homepage guest

This commit is contained in:
2026-09-15 12:20:55 -04:00
parent fca37666d3
commit 669cbe3540
11 changed files with 268 additions and 5 deletions

View File

@@ -55,5 +55,7 @@
- On Pikachu, `nix copy --to ssh://pikachu` accepted a small `nix store add-file` probe, - On Pikachu, `nix copy --to ssh://pikachu` accepted a small `nix store add-file` probe,
but rejected locally built system paths that lacked trusted signatures. but rejected locally built system paths that lacked trusted signatures.
Use a sudo `nix-store --import` path for unsigned deploy closures. Use a sudo `nix-store --import` path for unsigned deploy closures.
- Public DNS entries are not managed through the NixOS configuration.
Do not describe NixOS route choices as public-DNS management.
- Do not hand-author `hardware-configuration.nix` for a host that has not been generated on that machine. - Do not hand-author `hardware-configuration.nix` for a host that has not been generated on that machine.
Generate it on the target system or live ISO, then stage it before evaluating the flake. Generate it on the target system or live ISO, then stage it before evaluating the flake.

View File

@@ -25,6 +25,14 @@ in
} }
]; ];
registry.homepage.${config.networking.hostName}.actualbudget = {
tab = "Apps";
section = "Other";
name = "Actual Budget";
description = "Finance";
icon = "actual-budget.png";
};
guests.actualbudget = { guests.actualbudget = {
interior = { interior = {
modules.services.actualbudget = { modules.services.actualbudget = {

View File

@@ -90,6 +90,15 @@ in
} }
]; ];
registry.homepage.${config.networking.hostName}.copyparty = {
tab = "Apps";
section = "Files";
name = "Copyparty";
description = "Files";
icon = "copyparty.svg";
href = lib.mkIf cfg.reverseProxy.enable "https://${cfg.reverseProxy.route.domain}/?h";
};
guests.copyparty = { guests.copyparty = {
interior.modules.services.copyparty = { interior.modules.services.copyparty = {
enable = true; enable = true;

View File

@@ -0,0 +1,32 @@
{
config,
lib,
...
}:
let
cfg = config.guests.homepage;
in
{
options.guests.homepage = {
allowedHosts = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ "home" ];
description = "Host headers accepted by Homepage.";
};
};
config = lib.mkIf cfg.enable {
guests.homepage = {
interior.modules.services.homepage = {
enable = true;
inherit (cfg) allowedHosts;
};
limits = {
memory = lib.mkDefault "1G";
cpu = lib.mkDefault "100%";
tasksMax = lib.mkDefault 256;
};
};
};
}

View File

@@ -48,6 +48,14 @@ in
} }
]; ];
registry.homepage.${config.networking.hostName}.navidrome = {
tab = "Apps";
section = "Media";
name = "Navidrome";
description = "Music";
icon = "navidrome.png";
};
guests.navidrome = { guests.navidrome = {
interior.modules.services.navidrome.enable = true; interior.modules.services.navidrome.enable = true;

View File

@@ -144,6 +144,21 @@
address = "10.23.20.109"; address = "10.23.20.109";
}; };
guests.homepage = {
enable = true;
vlan = 20;
address = "10.23.20.46";
reverseProxy = {
enable = true;
route = {
domain = "home";
forceSSL = false;
enableACME = false;
port = 8082;
};
};
};
guests.copyparty = { guests.copyparty = {
enable = true; enable = true;
vlan = 20; vlan = 20;

View File

@@ -0,0 +1,135 @@
{
config,
lib,
registry,
...
}:
let
cfg = config.modules.services.homepage;
mkRegistryPath = registryName: host: entry: "registry.${registryName}.${host}.${entry}";
homepageEntries = lib.concatLists (
lib.mapAttrsToList (
host: entries:
lib.mapAttrsToList (entry: homepage: {
inherit host entry homepage;
composite = registry.join host entry;
}) entries
) registry.homepage
);
mkHref = item:
if item.homepage.href != null then
item.homepage.href
else if (item.composite.route or null) != null && item.composite.route.port != null then
let
scheme = if item.composite.route.forceSSL then "https" else "http";
in
"${scheme}://${item.composite.route.domain}"
else
throw "${mkRegistryPath "homepage" item.host item.entry} needs href or a matching TCP route entry";
mkServiceAttrs = item:
lib.filterAttrs (_: value: value != null) {
href = mkHref item;
inherit (item.homepage) description icon;
};
sortedEntries = lib.sort (left: right: left.homepage.name < right.homepage.name) homepageEntries;
insertEntry = acc: item:
acc
// {
${item.homepage.tab} = (acc.${item.homepage.tab} or { })
// {
${item.homepage.section} = (acc.${item.homepage.tab}.${item.homepage.section} or [ ]) ++ [
{ ${item.homepage.name} = mkServiceAttrs item; }
];
};
};
entriesByTab = lib.foldl' insertEntry { } sortedEntries;
tabOrder = [
"Apps"
"Download"
"System"
];
orderedTabs = tabOrder ++ lib.filter (tab: !lib.elem tab tabOrder) (lib.attrNames entriesByTab);
mkTab = tab:
let
sections = entriesByTab.${tab};
in
{
${tab} = map (section: { ${section} = sections.${section}; }) (lib.attrNames sections);
};
homepageServices = map mkTab (lib.filter (tab: lib.hasAttr tab entriesByTab) orderedTabs);
in
{
options.modules.services.homepage = {
enable = lib.mkEnableOption "Homepage Dashboard";
listenPort = lib.mkOption {
type = lib.types.port;
default = 8082;
description = "Port for Homepage to bind to.";
};
allowedHosts = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ "home" ];
description = "Host headers accepted by Homepage.";
};
};
config = lib.mkIf cfg.enable {
services.homepage-dashboard = {
enable = true;
openFirewall = true;
inherit (cfg) listenPort;
allowedHosts = lib.concatStringsSep "," cfg.allowedHosts;
services = homepageServices;
settings = {
title = "Tracion";
theme = "dark";
color = "slate";
showStats = false;
hideVersion = true;
bookmarksStyle = "icons";
useEqualHeights = true;
layout = {
Apps = {
style = "row";
columns = 1;
tab = "Apps";
Media = {
style = "row";
columns = 2;
};
Files = {
style = "row";
columns = 2;
};
Other = {
style = "row";
columns = 3;
iconsOnly = true;
};
};
Download = {
style = "row";
columns = 1;
tab = "Download";
};
System = {
tab = "System";
};
};
};
};
};
}

View File

@@ -21,8 +21,7 @@ let
inherit host entry; inherit host entry;
name = route.domain; name = route.domain;
value = { value = {
forceSSL = true; inherit (route) forceSSL enableACME;
enableACME = true;
locations."/".proxyPass = mkBackend address route; locations."/".proxyPass = mkBackend address route;
}; };
}; };

View File

@@ -0,0 +1,45 @@
{ lib, ... }:
{
type = lib.types.submodule {
options = {
tab = lib.mkOption {
type = lib.types.str;
example = "Apps";
description = "Homepage tab that contains this entry.";
};
section = lib.mkOption {
type = lib.types.str;
example = "Media";
description = "Homepage section that contains this entry.";
};
name = lib.mkOption {
type = lib.types.str;
example = "Navidrome";
description = "Homepage display name for this entry.";
};
description = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "Music";
description = "Homepage description for this entry.";
};
icon = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "navidrome.png";
description = "Homepage icon for this entry.";
};
href = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "https://music.alexion.dev";
description = "Homepage link override for this entry.";
};
};
};
}

View File

@@ -19,7 +19,19 @@
domain = lib.mkOption { domain = lib.mkOption {
type = lib.types.str; type = lib.types.str;
example = "files.alexion.dev"; example = "files.alexion.dev";
description = "Public domain served by this route entry."; description = "Domain served by this route entry.";
};
forceSSL = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Redirect HTTP requests to HTTPS for this route.";
};
enableACME = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Enable ACME certificate management for this route.";
}; };
port = lib.mkOption { port = lib.mkOption {

View File

@@ -50,10 +50,8 @@
- immich - immich
- romm - romm
- gitea - gitea
- vikunja (?)
- nginxproxymanager (gradual migration to nginx guest) - nginxproxymanager (gradual migration to nginx guest)
- adguard - adguard
- homepage
## raichu proxmox -> nixos ## raichu proxmox -> nixos
- dwn torrent (vpn) - dwn torrent (vpn)