feat(nixos): add pikachu homepage guest
This commit was merged in pull request #48.
This commit is contained in:
@@ -55,5 +55,7 @@
|
||||
- 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.
|
||||
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.
|
||||
Generate it on the target system or live ISO, then stage it before evaluating the flake.
|
||||
|
||||
@@ -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 = {
|
||||
interior = {
|
||||
modules.services.actualbudget = {
|
||||
|
||||
@@ -90,6 +90,15 @@ in
|
||||
}
|
||||
];
|
||||
|
||||
registry.homepage.${config.networking.hostName}.copyparty = {
|
||||
tab = "Apps";
|
||||
section = "Files";
|
||||
name = "Copyparty";
|
||||
description = "Files on ${config.networking.hostName}";
|
||||
icon = "copyparty.svg";
|
||||
href = lib.mkIf cfg.reverseProxy.enable "https://${cfg.reverseProxy.route.domain}/?h";
|
||||
};
|
||||
|
||||
guests.copyparty = {
|
||||
interior.modules.services.copyparty = {
|
||||
enable = true;
|
||||
|
||||
46
configs/nixos/guests/homepage.nix
Normal file
46
configs/nixos/guests/homepage.nix
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
let
|
||||
cfg = config.guests.homepage;
|
||||
environmentFile = "/run/secrets/homepage.env";
|
||||
in
|
||||
{
|
||||
config = lib.mkIf cfg.enable {
|
||||
guests.homepage = {
|
||||
interior.modules.services.homepage = {
|
||||
enable = true;
|
||||
hostName = config.networking.hostName;
|
||||
inherit environmentFile;
|
||||
};
|
||||
|
||||
mounts.${environmentFile} = {
|
||||
file = config.sops.templates."homepage.env".path;
|
||||
readOnly = true;
|
||||
};
|
||||
|
||||
limits = {
|
||||
memory = lib.mkDefault "1G";
|
||||
cpu = lib.mkDefault "100%";
|
||||
tasksMax = lib.mkDefault 256;
|
||||
};
|
||||
};
|
||||
|
||||
sops.secrets = {
|
||||
"services/metadata/latitude" = { };
|
||||
"services/metadata/longitude" = { };
|
||||
};
|
||||
|
||||
sops.templates."homepage.env" = {
|
||||
owner = "root";
|
||||
group = "storage";
|
||||
mode = "0440";
|
||||
content = ''
|
||||
HOMEPAGE_VAR_LATITUDE=${config.sops.placeholder."services/metadata/latitude"}
|
||||
HOMEPAGE_VAR_LONGITUDE=${config.sops.placeholder."services/metadata/longitude"}
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -48,6 +48,14 @@ in
|
||||
}
|
||||
];
|
||||
|
||||
registry.homepage.${config.networking.hostName}.navidrome = {
|
||||
tab = "Apps";
|
||||
section = "Media";
|
||||
name = "Navidrome";
|
||||
description = "Music";
|
||||
icon = "navidrome.png";
|
||||
};
|
||||
|
||||
guests.navidrome = {
|
||||
interior.modules.services.navidrome.enable = true;
|
||||
|
||||
|
||||
@@ -144,6 +144,19 @@
|
||||
address = "10.23.20.109";
|
||||
};
|
||||
|
||||
guests.homepage = {
|
||||
enable = true;
|
||||
vlan = 20;
|
||||
address = "10.23.20.110";
|
||||
reverseProxy = {
|
||||
enable = true;
|
||||
route = {
|
||||
domain = "home";
|
||||
http.port = 8082;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
guests.copyparty = {
|
||||
enable = true;
|
||||
vlan = 20;
|
||||
|
||||
219
configs/nixos/modules/services/homepage.nix
Normal file
219
configs/nixos/modules/services/homepage.nix
Normal file
@@ -0,0 +1,219 @@
|
||||
{
|
||||
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 then
|
||||
if item.composite.route.port != null then
|
||||
"https://${item.composite.route.domain}"
|
||||
else if item.composite.route.http.port != null then
|
||||
"http://${item.composite.route.domain}"
|
||||
else
|
||||
throw "${mkRegistryPath "homepage" item.host item.entry} needs href or a matching TCP route entry"
|
||||
else
|
||||
throw "${mkRegistryPath "homepage" item.host item.entry} needs href or a matching 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);
|
||||
|
||||
sectionOrderByTab = {
|
||||
Apps = [
|
||||
"Media"
|
||||
"Files"
|
||||
"Other"
|
||||
];
|
||||
Download = [
|
||||
"Library Managers"
|
||||
"Other Utilities"
|
||||
"Download Clients"
|
||||
];
|
||||
System = [
|
||||
"Monitoring"
|
||||
"Network"
|
||||
"Machines"
|
||||
"Docker"
|
||||
];
|
||||
};
|
||||
|
||||
orderedSections = tab: sections:
|
||||
let
|
||||
sectionOrder = sectionOrderByTab.${tab} or [ ];
|
||||
in
|
||||
sectionOrder ++ lib.filter (section: !lib.elem section sectionOrder) (lib.attrNames sections);
|
||||
|
||||
mkTab = tab:
|
||||
let
|
||||
sections = entriesByTab.${tab};
|
||||
in
|
||||
{
|
||||
${tab} = map (section: { ${section} = sections.${section}; }) (orderedSections tab sections);
|
||||
};
|
||||
|
||||
homepageServices = map mkTab (lib.filter (tab: lib.hasAttr tab entriesByTab) orderedTabs);
|
||||
|
||||
selfEntry = registry.join cfg.hostName cfg.entryName;
|
||||
selfAddress = selfEntry.address or null;
|
||||
selfRoute = selfEntry.route or null;
|
||||
|
||||
derivedAllowedHosts = lib.unique (
|
||||
lib.optional (selfRoute != null) selfRoute.domain
|
||||
++ lib.optionals (selfAddress != null) [
|
||||
selfAddress
|
||||
"${selfAddress}:${toString cfg.listenPort}"
|
||||
]
|
||||
);
|
||||
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.";
|
||||
};
|
||||
|
||||
hostName = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "Host registry key that owns the Homepage guest entry.";
|
||||
};
|
||||
|
||||
entryName = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "homepage";
|
||||
description = "Registry entry name for this Homepage service.";
|
||||
};
|
||||
|
||||
allowedHosts = lib.mkOption {
|
||||
type = lib.types.nullOr (lib.types.listOf lib.types.str);
|
||||
default = null;
|
||||
description = "Host headers accepted by Homepage. Defaults to matching address and route registry entries.";
|
||||
};
|
||||
|
||||
environmentFile = lib.mkOption {
|
||||
type = lib.types.path;
|
||||
default = "/run/secrets/homepage.env";
|
||||
description = "Environment file passed to Homepage.";
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
services.homepage-dashboard = {
|
||||
enable = true;
|
||||
openFirewall = true;
|
||||
inherit (cfg) listenPort;
|
||||
environmentFiles = [ cfg.environmentFile ];
|
||||
allowedHosts = lib.concatStringsSep "," (if cfg.allowedHosts == null then derivedAllowedHosts else cfg.allowedHosts);
|
||||
services = homepageServices;
|
||||
widgets = [
|
||||
{
|
||||
greeting = {
|
||||
text_size = "xl";
|
||||
text = "tracion";
|
||||
};
|
||||
}
|
||||
{
|
||||
openmeteo = {
|
||||
units = "metric";
|
||||
latitude = "{{HOMEPAGE_VAR_LATITUDE}}";
|
||||
longitude = "{{HOMEPAGE_VAR_LONGITUDE}}";
|
||||
cache = 5;
|
||||
};
|
||||
}
|
||||
{
|
||||
datetime = {
|
||||
text_size = "xl";
|
||||
locale = "en";
|
||||
format = {
|
||||
dateStyle = "full";
|
||||
timeStyle = "medium";
|
||||
hour12 = false;
|
||||
};
|
||||
};
|
||||
}
|
||||
];
|
||||
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";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -9,33 +9,47 @@ let
|
||||
|
||||
mkRegistryPath = registryName: host: entry: "registry.${registryName}.${host}.${entry}";
|
||||
|
||||
mkBackend = address: route: "http://${address}:${toString route.port}";
|
||||
mkBackend = address: port: "http://${address}:${toString port}";
|
||||
|
||||
mkVirtualHost = host: entry:
|
||||
mkVirtualHost = host: entry: mode:
|
||||
let
|
||||
compositeEntry = registry.join host entry;
|
||||
route = compositeEntry.route;
|
||||
address = compositeEntry.address or (throw "no matching entry in registry.address for ${mkRegistryPath "route" host entry}");
|
||||
isHttp = mode == "http";
|
||||
port = if isHttp then route.http.port else route.port;
|
||||
in
|
||||
{
|
||||
inherit host entry;
|
||||
registryName = "route";
|
||||
name = route.domain;
|
||||
value = {
|
||||
forceSSL = true;
|
||||
enableACME = true;
|
||||
locations."/".proxyPass = mkBackend address route;
|
||||
forceSSL = !isHttp;
|
||||
enableACME = !isHttp;
|
||||
locations."/".proxyPass = mkBackend address port;
|
||||
};
|
||||
};
|
||||
|
||||
generatedVirtualHostList = lib.concatLists (
|
||||
generatedHttpsVirtualHostList = lib.concatLists (
|
||||
lib.mapAttrsToList (
|
||||
host: routes:
|
||||
lib.mapAttrsToList (entry: _route: mkVirtualHost host entry) (
|
||||
lib.mapAttrsToList (entry: _route: mkVirtualHost host entry "https") (
|
||||
lib.filterAttrs (_entry: route: route.port != null) routes
|
||||
)
|
||||
) registry.route
|
||||
);
|
||||
|
||||
generatedHttpVirtualHostList = lib.concatLists (
|
||||
lib.mapAttrsToList (
|
||||
host: routes:
|
||||
lib.mapAttrsToList (entry: _route: mkVirtualHost host entry "http") (
|
||||
lib.filterAttrs (_entry: route: route.http.port != null) routes
|
||||
)
|
||||
) registry.route
|
||||
);
|
||||
|
||||
generatedVirtualHostList = generatedHttpsVirtualHostList ++ generatedHttpVirtualHostList;
|
||||
|
||||
virtualHostsByDomain = lib.foldl' (
|
||||
acc: virtualHost:
|
||||
acc
|
||||
@@ -49,7 +63,7 @@ let
|
||||
domain: virtualHosts:
|
||||
if lib.length virtualHosts > 1 then
|
||||
[
|
||||
"duplicate route domain ${domain} in ${lib.concatMapStringsSep " and " (virtualHost: mkRegistryPath "route" virtualHost.host virtualHost.entry) virtualHosts}"
|
||||
"duplicate route domain ${domain} in ${lib.concatMapStringsSep " and " (virtualHost: mkRegistryPath virtualHost.registryName virtualHost.host virtualHost.entry) virtualHosts}"
|
||||
]
|
||||
else
|
||||
[ ]
|
||||
|
||||
45
configs/nixos/registry/homepage.nix
Normal file
45
configs/nixos/registry/homepage.nix
Normal 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.";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -12,6 +12,10 @@
|
||||
);
|
||||
message = "Route UDP ports must be unique.";
|
||||
}
|
||||
{
|
||||
assertion = entries: lib.all (entry: entry.value.port == null || entry.value.http.port == null) entries;
|
||||
message = "Route entries must not set both port and http.port.";
|
||||
}
|
||||
];
|
||||
|
||||
type = lib.types.submodule {
|
||||
@@ -19,14 +23,21 @@
|
||||
domain = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
example = "files.alexion.dev";
|
||||
description = "Public domain served by this route entry.";
|
||||
description = "Domain served by this route entry.";
|
||||
};
|
||||
|
||||
port = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.port;
|
||||
default = null;
|
||||
example = 3923;
|
||||
description = "Backend TCP port served by the matching address registry entry.";
|
||||
description = "HTTPS backend TCP port served by the matching address registry entry.";
|
||||
};
|
||||
|
||||
http.port = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.port;
|
||||
default = null;
|
||||
example = 8082;
|
||||
description = "HTTP backend TCP port served by the matching address registry entry.";
|
||||
};
|
||||
|
||||
udp.ports = lib.mkOption {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
alexion-password: ENC[AES256_GCM,data:EGTsJvW4/zA+FfhUtZD4E6HIsI0JQ3LLL511XjI9hDl3NaHNe4g+wWy7P5DQnW29G5W4TaPZIetUcV7OO+1btuRxTuwbWVOulA==,iv:Ihf1kz5DFtvy1bUnfQGAU0/BQrwUvqKwqnK1JbKnWHU=,tag:K0N/6H/IWesNMxzXhQQO/w==,type:str]
|
||||
services:
|
||||
metadata:
|
||||
latitude: ENC[AES256_GCM,data:AB6HQjDVghSv,iv:lDTrdg5Bc0xCygOLPlDHrm/eFoHW9rmZ35CEcVXqusg=,tag:BOTXwWYNL7erD9aH6x3wGQ==,type:str]
|
||||
longitude: ENC[AES256_GCM,data:+iOmY92f7QQ+Ew==,iv:7fSpaHTSB1l9EbhRR+GrErmuZxfTxeEKy5rEWW6Th0I=,tag:zFtou1itlH4Iec8obgXV6g==,type:str]
|
||||
copyparty:
|
||||
users:
|
||||
alexion:
|
||||
@@ -46,7 +49,7 @@ sops:
|
||||
28uWUpKzzgErq1aEbKNgh4cU5TH2RlAHjtzRC3ARUu9hlW6lnB4pSw==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1wf5s0n0tgt6ld2ysgu9dc67mj8ylwecgl4utzg7hqwy3kut9zyms7aglmh
|
||||
lastmodified: "2026-09-10T18:32:33Z"
|
||||
mac: ENC[AES256_GCM,data:g3xIylSvyn5Z0JBA8siFmMrZxoM3a3U5c+f9qFmb3zXDLF03/feCUlvG2jjB7P2N9phIGSIEOPk1dzZDBMwv6D2hK6MppupV+MIe9diIt0gGARIOJNphrxLDxMHtwcWg6Z20D6tbPl0ECtLI0K9GOtwvI0OXeyYzu5VgFaFlZHw=,iv:TAUVJRXFTKNVA0ORMD7x56WVGjG+zimj3iqEn7urh64=,tag:SeOOV9ZUY4x5Ns0A16F1uw==,type:str]
|
||||
lastmodified: "2026-09-16T00:35:15Z"
|
||||
mac: ENC[AES256_GCM,data:VdYFSoulJMU0QjfzdBUVnXHruLbMkyYA4mirYwl2y4RijyfSBjtgv+bn+p6PiSQSAkpe7kQ8qJhfZVVm0MtgJvaRm3r3fj2lMUH930UhyMCK2F8uypBychwZJvn+Jo3m1rBOJTicd0qQvSA9heVBxQ+TnVGCwhnQWWFj/GsIV5s=,iv:kr7LtcDdT7VVV78qIBtFZYLSoq2x0AOltlKjCUMxPuM=,tag:+MXD2DbI34gv8gpaDhobmg==,type:str]
|
||||
unencrypted_suffix: _unencrypted
|
||||
version: 3.13.3
|
||||
|
||||
@@ -50,10 +50,9 @@
|
||||
- immich
|
||||
- romm
|
||||
- gitea
|
||||
- vikunja (?)
|
||||
- nginxproxymanager (gradual migration to nginx guest)
|
||||
- adguard
|
||||
- homepage
|
||||
- manage DNS rewrites for local service names such as `home`
|
||||
|
||||
## raichu proxmox -> nixos
|
||||
- dwn torrent (vpn)
|
||||
|
||||
Reference in New Issue
Block a user