Compare commits

...

27 Commits

Author SHA1 Message Date
soraefir
e1a80bb7ce Add new modules 2026-05-12 21:24:02 +02:00
7d95ba04a9 Add modules/shared/syscfg/user.nix 2026-05-12 18:02:13 +02:00
9169205357 Add modules/shared/syscfg/make.nix 2026-05-12 18:02:01 +02:00
74721f6b09 Add modules/shared/syscfg/net.nix 2026-05-12 18:01:52 +02:00
668c0107f9 Add modules/shared/syscfg/server.nix 2026-05-12 18:01:44 +02:00
331291c54d Update modules/shared/syscfg/default.nix 2026-05-12 18:01:33 +02:00
d10f53e485 Add modules/server/containers/apps/immich.nix 2026-05-12 17:47:01 +02:00
82aea8268f Update modules/shared/syscfg/default.nix 2026-05-12 17:46:23 +02:00
050eaedca2 Update modules/server/sops/example.server.yaml 2026-05-12 17:46:11 +02:00
be9cb270aa Update modules/server/sops/default.nix 2026-05-12 17:45:59 +02:00
a5e0e96b52 Update modules/server/database/default.nix 2026-05-12 17:45:49 +02:00
4366232f18 Update modules/server/containers/apps/umami.nix 2026-05-12 17:45:34 +02:00
4398b1d888 Update modules/server/containers/apps/transmission.nix 2026-05-12 17:45:25 +02:00
c4b5c47aa4 Update modules/server/containers/apps/traefik.nix 2026-05-12 17:45:16 +02:00
ff64e6c231 Update modules/server/containers/apps/servarr.nix 2026-05-12 17:45:07 +02:00
e7d656141a Update modules/server/containers/apps/searxng.nix 2026-05-12 17:44:59 +02:00
cf3c2428fb Update modules/server/containers/apps/nextcloud.nix 2026-05-12 17:44:50 +02:00
a2dc050b1c Update modules/server/containers/apps/jellyfin.nix 2026-05-12 17:44:41 +02:00
8bf332caf2 Update modules/server/containers/apps/gitea.nix 2026-05-12 17:44:25 +02:00
20d3786547 Update modules/server/containers/apps/etherpad.nix 2026-05-12 17:44:18 +02:00
79422c180a Update modules/server/containers/apps/ethercalc.nix 2026-05-12 17:44:11 +02:00
65fc9c6df2 Update modules/server/containers/apps/collabora.nix 2026-05-12 17:44:03 +02:00
a59cbd13a3 Update modules/server/containers/apps/authentik.nix 2026-05-12 17:43:55 +02:00
5f04ef7ae5 Update modules/server/containers/apps/.todo.md 2026-05-12 17:43:48 +02:00
0aff508cda Add modules/server/containers/apps/.template.nix 2026-05-12 17:43:40 +02:00
30df106b94 Update modules/server/containers/default.nix 2026-05-12 17:43:28 +02:00
3abdb6d637 Update modules/server/containers/builder.nix 2026-05-12 17:43:18 +02:00
27 changed files with 650 additions and 281 deletions

View File

@@ -0,0 +1,44 @@
{ config, containerCfg, pkgs, lib, builder, name,... }:
let
serverCfg = config.syscfg.server;
image = pkgs.dockerTools.streamLayeredImage {
name = "EXAMPLE";
tag = "0.0.0";
contents = [ pkgs.bashInteractive ];
config = {
Entrypoint = [ "echo 1" ];
ExposedPorts = { };
};
};
templateData = builder.mkData { name = "template"; dir = "template"; vars = {
_ARGUMENT = "template";
};
};
in {
paths = [{
path="${serverCfg.configPath}/example/";
mode = "0444";
}];
containers = {
server = builder.mkContainer {
subdomain = containerCfg.subdomain;
imageStream = image;
port = 8080;
secret = name;
extraEnv = { };
overrides = {
cmd = [ ];
volumes = [ ];
};
};
};
setup = {
trigger = "server";
envFile = config.sops.secrets."EXAMPLE".path;
script = pkgs.writeShellScript "setup" ''
...
'';
};
}

View File

@@ -2,3 +2,7 @@
RSS: TTRSS / FreshRSS
Monitoring: Telegraf + InfluxDB
https://github.com/tarampampam/error-pages ?
- JellyFin external mkData for config (system.xml)
- Transmission Cfg and API/Token handling

View File

@@ -10,6 +10,8 @@ let
};
};
in {
sops = true;
db = true;
paths = [{
path="${serverCfg.configPath}/authentik/media";
owner = "1000:1000";
@@ -25,7 +27,6 @@ in {
subdomain = containerCfg.subdomain;
image = "ghcr.io/goauthentik/server:${version}";
port = 9000;
ip = containerCfg.ip;
secret = name;
extraEnv = {
"AUTHENTIK_REDIS__HOST" = builder.host;

View File

@@ -3,12 +3,12 @@ let
version = "latest";
serverCfg = config.syscfg.server;
in {
sops = true;
containers = {
server = builder.mkContainer {
subdomain = containerCfg.subdomain;
image = "collabora/code:${version}";
port = 9980;
ip = containerCfg.ip;
secret = name;
extraEnv = {
"aliasgroup1" = "https://${serverCfg.containers.nextcloud.subdomain}.${serverCfg.hostDomain}";

View File

@@ -13,6 +13,7 @@ let
};
};
in {
sops = true;
paths = [{
path="${serverCfg.dataPath}/ethercalc/";
mode = "0666";
@@ -23,7 +24,6 @@ in {
subdomain = containerCfg.subdomain;
imageStream = image;
port = 8080;
ip = containerCfg.ip;
secret = name;
extraEnv = {
ETHERCALC_PORT = "8080";

View File

@@ -76,6 +76,8 @@ let
};
};
in {
sops = true;
db = true;
paths = [{
path="${serverCfg.configPath}/etherpad/";
mode = "0444";
@@ -86,7 +88,6 @@ in {
subdomain = containerCfg.subdomain;
imageStream = image;
port = 8080;
ip = containerCfg.ip;
secret = name;
extraEnv = {
TITLE = "Pad";

View File

@@ -3,7 +3,8 @@ let
version = "latest";
serverCfg = config.syscfg.server;
in {
sops = true;
db = true;
paths = [{
path="${serverCfg.dataPath}/gitea/data";
owner = "1000:1000";
@@ -18,7 +19,6 @@ in {
subdomain = containerCfg.subdomain;
image = "gitea/gitea:${version}";
port = 8080;
ip = containerCfg.ip;
secret = name;
extraEnv = { # app.ini -> GITEA__<section>__<KEY> = "<VALUE>";

View File

@@ -0,0 +1,3 @@
{...}:{
}

View File

@@ -1,3 +1,58 @@
{...}:{
{ config, containerCfg, pkgs, lib, builder, name, ... }:
let
serverCfg = config.syscfg.server;
image = pkgs.dockerTools.streamLayeredImage {
name = pkgs.jellyfin.name;
tag = pkgs.jellyfin.version;
contents = [
pkgs.cacert
];
config = {
Entrypoint = [ "${pkgs.jellyfin}/bin/jellyfin" ];
ExposedPorts = { "8096/tcp" = { }; };
};
};
in {
paths = [
{
path = "${serverCfg.dataPath}/media/";
mode = "0755";
}
{
path = "${serverCfg.configPath}/jellyfin/";
mode = "0755";
}
];
containers = {
server = builder.mkContainer {
subdomain = containerCfg.subdomain;
imageStream = image;
port = 8096;
# secret = name;
extraEnv = {
DOTNET_SYSTEM_GLOBALIZATION_INVARIANT = "1";
# JELLYFIN_WEB_DIR = "${pkgs.jellyfin-web}/share/jellyfin-web";
JELLYFIN_HttpListenerHost__BindAddress= "0.0.0.0"; #we can use settings.xml override
};
extraOptions = [
"--tmpfs=/tmp:rw,noexec,nosuid,size=512m"
];
overrides = {
cmd = [
"--datadir" "/config/data"
"--cachedir" "/config/cache"
"--configdir" "/config/config"
"--logdir" "/config/log"
];
volumes = [
"${serverCfg.dataPath}/movies:/media/movies:ro"
"${serverCfg.dataPath}/series:/media/series:ro"
"${serverCfg.configPath}/jellyfin:/config"
];
# If you have an Intel/AMD GPU for transcoding, add the device:
devices = lib.optionals (builtins.pathExists "/dev/dri") [ "/dev/dri:/dev/dri" ];
};
};
};
}

View File

@@ -3,6 +3,8 @@ let
version = "31";
serverCfg = config.syscfg.server;
in {
sops = true;
db = true;
paths = [{
path="${serverCfg.dataPath}/nextcloud/www";
owner = "33:33";
@@ -19,7 +21,6 @@ in {
subdomain = containerCfg.subdomain;
image = "nextcloud:${version}";
port = 80;
ip = containerCfg.ip;
secret = name;
extraEnv = {
REDIS_HOST = builder.host;

View File

@@ -1,3 +1,92 @@
{...}:{
{ config, containerCfg, pkgs, lib, builder, name,... }:
let
version= "latest";
serverCfg = config.syscfg.server;
settings = pkgs.writeText"settings.yml" (pkgs.lib.generators.toYAML {}{
use_default_settings = true;
brand = {
issue_url = "";
docs_url = "";
public_instances = "";
wiki_url = "";
custom = {
links = {
"Home" = "https://${serverCfg.hostDomain}";
# "Status" = "https://status.${serverCfg.hostDomain}";
};
};
pwa_colors = {
theme_color_light = "${serverCfg.colorScheme.palette.base0C}";
background_color_light = "${serverCfg.colorScheme.palette.base07}";
theme_color_dark = "${serverCfg.colorScheme.palette.base0C}";
background_color_dark = "${serverCfg.colorScheme.palette.base02}";
theme_color_black = "${serverCfg.colorScheme.palette.base0C}";
background_color_black = "${serverCfg.colorScheme.palette.base01}";
};
};
general = {
debug = false;
instance_name = if containerCfg.extra ? instanceName then containerCfg.extra.instanceName else "SearXNG";
privacypolicy_url = false;
donation_url = false;
contact_url = false;
enable_metrics = false;
};
search = {
safe_search = 0;
autocomplete = if containerCfg.extra ? autocomplete then containerCfg.extra.autocomplete else "";
languages = [ "all" "en" "en-US" "ja" "de-CH" "fr-CH" "nb" ];
};
server = {
# secret_key = ""; SET BY ENV VAR
};
ui = {
default_locale = if containerCfg.extra ? defaultLocale then containerCfg.extra.defaultLocale else "en";
# query_in_title = "true";
#default_theme = "custom";
custom_css = "footer { display: none !important; }";
};
# categories_as_tabs = {
# general = {};
# images ={};
# videos = {};
# news = {};
# files = {};
# };
plugins = {
"searx.plugins.infinite_scroll.SXNGPlugin".active = true;
"searx.plugins.tracker_url_remover.SXNGPlugin".active = true;
};
});
in {
sops = true;
# paths = [{
# path="${serverCfg.dataPath}/searxng/";
# mode = "0444";
# }];
containers = {
server = builder.mkContainer {
subdomain = containerCfg.subdomain;
image = "searxng/searxng:${version}";
port = 8080;
secret = name;
extraEnv = {
SEARXNG_BASE_URL = "https://${containerCfg.subdomain}.${serverCfg.hostDomain}";
SEARXNG_PORT = "8080";
SEARXNG_BIND_ADDRESS = "[::]";
SEARXNG_PUBLIC_INSTANCE = "false";
SEARXNG_SETTINGS_PATH = "/etc/searxng/settings.yml";
#SEARXNG_VALKEY_URL = "valkey://user:password@${builder.host}:6379/0}";
};
overrides = {
cmd = [ ];
volumes = [
"${settings}:/etc/searxng/settings.yml"
# "/path/to/your/logo.png:/usr/local/searxng/searx/static/themes/simple/img/searxng.png
# "${serverCfg.dataPath}/searxng:/var/cache/searxng/"
];
};
};
};
}

View File

@@ -1,3 +1,73 @@
{...}:{
{ config, containerCfg, pkgs, lib, builder, name, ... }:
let
serverCfg = config.syscfg.server;
mkServarrImage = appName: appPkg: binaryPath: pkgs.dockerTools.streamLayeredImage {
name = appPkg.name;
tag = appPkg.version;
contents = with pkgs; [ cacert openssl ];
config = {
Cmd = [ "${appPkg}/${binaryPath}" "-nobrowser" "-data=/config" ];
Env = [ "DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1" ];
};
};
images = {
prowlarr = mkServarrImage "prowlarr" pkgs.prowlarr "bin/Prowlarr";
radarr = mkServarrImage "radarr" pkgs.radarr "bin/Radarr";
sonarr = mkServarrImage "sonarr" pkgs.sonarr "bin/Sonarr";
bazarr = mkPythonImage "bazarr" pkgs.bazarr "bin/bazarr";
lidarr = mkServarrImage "lidarr" pkgs.lidarr "bin/Lidarr";
readarr = mkServarrImage "readarr" pkgs.readarr "bin/Readarr";
};
sharedVolumes = [
"${serverCfg.mediaPath or "/mnt/media"}:/media" # Fast hardlinking requires a single shared root
"${serverCfg.configPath}/servarr:/config-root"
];
in {
# Initialize atomic configuration structures
paths = [
{ path = "${serverCfg.configPath}/servarr/prowlarr"; mode = "0755"; }
{ path = "${serverCfg.configPath}/servarr/radarr"; mode = "0755"; }
{ path = "${serverCfg.configPath}/servarr/sonarr"; mode = "0755"; }
];
containers = {
prowlarr = builder.mkContainer {
subdomain = containerCfg.subdomain;
subpath = "prowlarr";
imageStream = images.prowlarr;
port = 9696;
secret = name;
overrides.volumes = sharedVolumes ++ [ "${serverCfg.configPath}/servarr/prowlarr:/config" ];
};
radarr = builder.mkContainer {
subdomain = containerCfg.subdomain;
subpath = "radarr";
imageStream = images.radarr;
port = 7878;
secret = name;
overrides.volumes = sharedVolumes ++ [ "${serverCfg.configPath}/servarr/radarr:/config" ];
};
sonarr = builder.mkContainer {
subdomain = containerCfg.subdomain
subpath = "sonarr";
imageStream = images.sonarr;
port = 8989;
secret = name;
overrides.volumes = sharedVolumes ++ [ "${serverCfg.configPath}/servarr/sonarr:/config" ];
};
};
# setup = {
# trigger = "prowlarr"; # Triggers atomic environment verification on main controller
# envFile = config.sops.secrets."SERVARR".path;
# script = pkgs.writeShellScript "setup-servarr" ''
# echo "Validating multi-container path permission nodes..."
# # mkdir -p ${serverCfg.configPath}/servarr/{prowlarr,radarr,sonarr}
# '';
# };
}

View File

@@ -11,6 +11,7 @@ let
};
};
in {
sops = true;
paths = [{
path="${serverCfg.configPath}/traefik";
owner = "1000:1000";
@@ -21,7 +22,6 @@ in {
server = builder.mkContainer {
imageStream = image;
subdomain = containerCfg.subdomain;
ip = containerCfg.ip;
port = 8080;
secret = name;
extraLabels = {
@@ -55,15 +55,19 @@ in {
"--entrypoints.web.http.redirections.entrypoint.scheme=https"
"--entrypoints.web-secure.transport.respondingtimeouts.readtimeout=0s"
"--entrypoints.web-secure.proxyprotocol.trustedips=127.0.0.1/32,192.168.1.1/16,10.10.0.0/16"
] ++ (if containerCfg.extra ? provider then [
] ++ (if serverCfg.containers ? umami then [
"--experimental.plugins.umami-feeder.moduleName=github.com/astappiev/traefik-umami-feeder"
"--experimental.plugins.umami-feeder.version=v1.4.1"
"--entrypoints.web-secure.http.middlewares=umami-global@docker"
] else []) ++ (if containerCfg.extra ? provider then [
"--certificatesresolvers.default.acme.email=acme@${serverCfg.hostDomain}"
"--certificatesresolvers.default.acme.dnschallenge=true"
"--certificatesresolvers.default.acme.dnschallenge.provider=${containerCfg.extra.provider}"
"--certificatesresolvers.default.acme.storage=/custom/acme.json"
] else (if serverCfg.hostDomain != "localhost" then [
] else []) ++ (if serverCfg.hostDomain != "localhost" then [
"--certificatesresolvers.default.acme.httpchallenge=false"
"--certificatesresolvers.default.acme.tlschallenge=true"
] else [ ]));
] else []);
ports = [ "443:443" "80:80" ] ++ (if containerCfg.port!=null then [ "${toString containerCfg.port}:8080" ] else []);
volumes = [
"/var/run/podman/podman.sock:/var/run/docker.sock"

View File

@@ -1,3 +1,57 @@
{...}:{
{ config, containerCfg, pkgs, lib, builder, name, ... }:
let
serverCfg = config.syscfg.server;
image = pkgs.dockerTools.streamLayeredImage {
name = pkgs.transmission_4.name;
tag = pkgs.transmission_4.version;
contents = [ pkgs.cacert ];
config = {
Cmd = [ "${pkgs.transmission_4}/bin/transmission-daemon" "--foreground" "--config-dir" "/config" ];
ExposedPorts = {
"9091/tcp" = {};
"51413/tcp" = {}; "51413/udp" = {};
};
};
};
in {
paths = [{
path = "${serverCfg.dataPath}/transmission/complete";
owner = "1000:1000";
mode = "0755";
}{
path = "${serverCfg.dataPath}/transmission/incomplete";
owner = "1000:1000";
mode = "0755";
}{
path = "${serverCfg.dataPath}/transmission/config";
owner = "1000:1000";
mode = "0755";
}];
containers = {
server = builder.mkContainer {
subdomain = containerCfg.subdomain;
imageStream = image;
port = 9091;
extraEnv = {
PUID = "1000";
PGID = "1000";
TZ = "Europe/Zurich";
};
extraLabels = { } // (if serverCfg.containers ? authentik then {
"traefik.http.routers.${containerCfg.subdomain}.middlewares" = "authentik";
} else {});
overrides = {
cmd = [ ];
volumes = [
"${serverCfg.dataPath}/transmission/complete:/downloads/complete"
"${serverCfg.dataPath}/transmission/incomplete:/downloads/incomplete"
"${serverCfg.dataPath}/transmission/config:/config"
];
};
};
};
}

View File

@@ -1,3 +1,58 @@
{...}:{
{ config, containerCfg, pkgs, lib, builder, name,... }:
let
serverCfg = config.syscfg.server;
# Umami image built from nixpkgs
image = pkgs.dockerTools.streamLayeredImage {
name = pkgs.umami.name;
tag = pkgs.umami.version;
contents = [ pkgs.cacert ];
config = {
# Umami in nixpkgs typically provides a binary or script to start the server
Entrypoint = [ "${pkgs.umami}/bin/umami-server" ];
ExposedPorts = { "3000/tcp" = {}; };
Env = [ "NODE_ENV=production" ];
};
};
in {
sops = true;
db = true;
paths = [{
path = "${serverCfg.configPath}/umami/";
mode = "0444";
}];
containers = {
server = builder.mkContainer {
subdomain = containerCfg.subdomain;
imageStream = image;
port = 3000;
secret = name;
extraEnv = {
PORT = "3000";
# HOSTNAME = "${containerCfg.subdomain}.${serverCfg.hostDomain}";
DATABASE_TYPE = "postgresql";
REDIS_URL = "redis://${builder.host}";
CLIENT_IP_HEADER = "X-Forwarded-For";
BASE_PATH = lib.optionalString (containerCfg.subpath or null != null) "/${containerCfg.subpath}";
# DISABLE_LOGIN = "1";#(if serverCfg.containers?authentik then "1" else "0");
};
extraLabels = {
"traefik.http.middlewares.umami-global.plugin.umami-feeder.umamiHost" = "http://umami-server:3000";
"traefik.http.middlewares.umami-global.plugin.umami-feeder.umamiUsername" = "admin";
"traefik.http.middlewares.umami-global.plugin.umami-feeder.umamiPassword" = "umami";
"traefik.http.middlewares.umami-global.plugin.umami-feeder.createNewWebsites" = "true";
} // ( if serverCfg.containers?authentik then {
"traefik.http.routers.${containerCfg.subdomain}.middlewares" = if serverCfg.containers?authentik then "authentik" else "";
} else {});
extraOptions = [
"--tmpfs=/tmp:rw,noexec,nosuid,size=512m"
];
overrides = {
cmd = [ "start" ]; # Specific command for the umami binary
};
};
};
}

View File

@@ -1,34 +1,41 @@
{ config, lib, pkgs, serverCfg }:
let
builder =
{ image ? null, imageStream ? null
{ image ? null, imageStream ? null, imageFile ? null
, secret ? null
, subdomain ? null, ip ? null, port ? 0
, subdomain ? null, subpath?null, port ? 0
, extraEnv ? { }, extraLabels ? { }, extraOptions ? [ ]
, overrides ? { }
}:
let base = {
let
routerName = if subpath != null
then "${subdomain}-${lib.strings.sanitizeDerivationName subpath}"
else subdomain;
base = {
image = if imageStream != null then "${imageStream.imageName}:${imageStream.imageTag}"
else image;
imageStream = imageStream;
imageFile = imageFile;
environmentFiles = if secret!=null then [ config.sops.secrets."${lib.toUpper secret}".path ] else [];
environment = {} // extraEnv;
labels = (if subdomain!=null then ({
"traefik.enable" = "true";
"traefik.http.routers.${subdomain}.entrypoints" = "web-secure";
"traefik.http.routers.${subdomain}.rule" = "Host(`${subdomain}.${serverCfg.hostDomain}`)";
"traefik.http.routers.${subdomain}.tls" = "true";
"traefik.http.routers.${routerName}.entrypoints" = "web-secure";
"traefik.http.routers.${routerName}.rule" = if subpath != null
then "Host(`${subdomain}.${serverCfg.hostDomain}`) && PathPrefix(`/${subpath}`)"
else "Host(`${subdomain}.${serverCfg.hostDomain}`)";
"traefik.http.routers.${routerName}.tls" = "true";
} // lib.optionalAttrs (port!=null) {
"traefik.http.services.${subdomain}.loadbalancer.server.port" = toString port;
"traefik.http.services.${routerName}.loadbalancer.server.port" = toString port;
}) else {
"traefik.enable" = "false";
}) // extraLabels;
extraOptions = extraOptions ++ [
"--add-host=host.containers.internal:host-gateway"
] ++ lib.optional (ip!=null) "--ip=${ip}";
];
};
in lib.recursiveUpdate base overrides;
in {

View File

@@ -2,80 +2,77 @@
let
serverCfg = config.syscfg.server;
builder = import ./builder.nix { inherit config lib pkgs serverCfg; };
enabledConfigs = lib.filterAttrs (name: c: c.enable) serverCfg.containers;
containerSetsList = lib.mapAttrsToList (name: containerCfg:
let apps = import (./apps + "/${name}.nix") {inherit config pkgs lib containerCfg builder name;};
in{
name = name;
containers = lib.mapAttrs' (cName: cValue:
lib.nameValuePair "${name}-${cName}" cValue
) apps.containers;
paths = apps.paths or [];
setup = apps.setup or null;
cron = apps.cron or [];
}
) enabledConfigs;
mergedContainers = lib.attrsets.mergeAttrsList (lib.map(e: e.containers) containerSetsList);
allPathConfigs = lib.flatten (lib.map (e: e.paths) containerSetsList);
allCronsConfigs = lib.flatten (lib.map (e: e.cron or []) containerSetsList);
in
{
config = lib.mkIf ( enabledConfigs != {} ) {
virtualisation.oci-containers = {
backend = "podman";
containers = mergedContainers;
};
system.activationScripts.container-setup-dirs = {
deps = [ "users" "groups" ];
text = lib.concatStringsSep "\n" (map (cfg:
let
effectiveCfg = {
owner = "root:root";
mode = "0400";
} // cfg;
in ''
${pkgs.coreutils}/bin/mkdir -p "${effectiveCfg.path}"
${pkgs.coreutils}/bin/chown ${effectiveCfg.owner} "${effectiveCfg.path}"
${pkgs.coreutils}/bin/chmod ${effectiveCfg.mode} "${effectiveCfg.path}"
'') allPathConfigs);
};
in{
config = lib.mkMerge [{
syscfg.server.loadedContainers = lib.mapAttrs (name: containerCfg:
(import (./apps + "/${name}.nix")) { inherit config pkgs lib containerCfg builder name; }
) config.syscfg.server.containers;
} (lib.mkIf ( serverCfg.containers != {} ) (
let
appsList = builtins.attrValues config.syscfg.server.loadedContainers;
mergedContainers = lib.concatMapAttrs (appName: app:
lib.mapAttrs' (cName: cCfg: lib.nameValuePair "${appName}-${cName}" cCfg) app.containers
) config.syscfg.server.loadedContainers;
allPathConfigs = lib.concatMap (app: app.paths) appsList;
allCronsConfigs = lib.concatMap (app: app.cron) appsList;
in{
systemd.services = {
podman-gc = {
description = "Podman garbage collection";
serviceConfig.Type = "oneshot";
script = ''
${pkgs.podman}/bin/podman container prune -f
${pkgs.podman}/bin/podman image prune -f
'';
startAt = "weekly";
virtualisation.oci-containers = {
backend = "podman";
containers = mergedContainers;
};
} // lib.listToAttrs (lib.concatMap (containerSet:
if containerSet.setup != null then [{
name = "${containerSet.name}-setup";
value = {
description = "Run ${containerSet.name} setup";
after = [ "podman-${containerSet.name}-${containerSet.setup.trigger}.service" ];
wants = [ "podman-${containerSet.name}-${containerSet.setup.trigger}.service" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
TimeoutStartSec = "360s";
EnvironmentFile = if (containerSet.setup ? envFile) then containerSet.setup.envFile else [ ];
ExecStart = "${containerSet.setup.script}";
RemainAfterExit = true;
User = "root";
};
system.activationScripts.container-setup-dirs = {
deps = [ "users" "groups" ];
text = lib.concatStringsSep "\n" (map (cfg:
let
effectiveCfg = {
owner = "root:root";
mode = "0400";
} // cfg;
in ''
${pkgs.coreutils}/bin/mkdir -p "${effectiveCfg.path}"
${pkgs.coreutils}/bin/chown ${effectiveCfg.owner} "${effectiveCfg.path}"
${pkgs.coreutils}/bin/chmod ${effectiveCfg.mode} "${effectiveCfg.path}"
'') allPathConfigs);
};
systemd.services = {
podman-gc = {
description = "Podman garbage collection";
serviceConfig.Type = "oneshot";
script = ''
${pkgs.podman}/bin/podman container prune -f
${pkgs.podman}/bin/podman image prune -f
'';
startAt = "weekly";
};
}] else []
) containerSetsList);
} // lib.listToAttrs (lib.concatMap (containerSet:
if containerSet.setup.script != null then [{
name = "${containerSet.name}-setup";
value = {
description = "Run ${containerSet.name} setup";
after = [ "podman-${containerSet.name}-${containerSet.setup.trigger}.service" ];
wants = [ "podman-${containerSet.name}-${containerSet.setup.trigger}.service" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
TimeoutStartSec = "360s";
EnvironmentFile = if (containerSet.setup ? envFile) then containerSet.setup.envFile else [ ];
ExecStart = "${containerSet.setup.script}";
RemainAfterExit = true;
User = "root";
};
};
}] else []
) appsList);
services.cron = {
enable = true;
systemCronJobs = allCronsConfigs;
};
services.cron = {
enable = true;
systemCronJobs = allCronsConfigs;
};
}))];
};
}

View File

@@ -1,14 +1,10 @@
{ config, lib, pkgs, ... }:
let
listNames = config.syscfg.server.db;
containerNames = lib.mapAttrsToList
(name: cfg: name)
(lib.filterAttrs (name: cfg: cfg.db or false) config.syscfg.server.containers);
containerNames = builtins.attrNames (lib.filterAttrs (appName: app: app.db) config.syscfg.server.loadedContainers);
allApps = lib.unique (listNames ++ containerNames);
in {
config = lib.mkIf ( builtins.length allApps > 0) {
services.postgresql = {

View File

@@ -1,8 +1,7 @@
{ config, lib, pkgs, ... }:
let
listNames = config.syscfg.server.db;
containerNames = lib.mapAttrsToList (name: cfg: name)
(lib.filterAttrs (name: cfg: ((cfg.db or false) || (cfg.sops or false))) config.syscfg.server.containers);
containerNames = builtins.attrNames (lib.filterAttrs (appName: app: app.sops) config.syscfg.server.loadedContainers);
allApps = lib.unique (listNames ++ containerNames);
in{
sops.secrets = {

View File

@@ -26,3 +26,9 @@ GITEA: |
GITEA__database__PASSWD=...
GITEA__security__SECRET_KEY=...
GITEA__security__INTERNAL_TOKEN=...
SEARXNG: |
SEARXNG_SECRET=...
UMAMI: |
DB_PASSWORD=...
DATABASE_URL=postgresql://username:mypassword@localhost:5432/mydb
APP_SECRET=...

View File

@@ -6,6 +6,8 @@ COLLABORA: ENC[AES256_GCM,data:cLGEziks5dyxTF1jugfpQE0l0nSkDP7MpROzCxCM94jv49sgu
ETHERPAD: ENC[AES256_GCM,data:PSr06GyOgY0HDNC4Hr2XUjbNUszGlfBjxDbrrKNQOqSMSVfZj4iFIGamrS72WO0un4U7IENx0T6CTBN/ELoq7J/+W9zf879uzKWuNaAulLVtBqrUbbqA7hTJpidnveZXzdwZRvlz/bU8kWAmXyhiDb2Q42Sz3BDb6duM3PO1AgG8Ko1pi2IemCPjO3uzudeT8FAlO8NnCUxKgwIKSz8CodOXFVGk66NX4xJd4ycfdNYXvKBNlzt1+WuWsZeZzeWmF7WD2dt4wWA9fWxB90fnth6ZV5LdeXjyYnzwkFOWoyNazgqV4jBv+aXKVwX4fYvspu13cVdrak3gc698bS2N1guDss4A/sfXMbtaYPGm98xXkqz1LP7sXQzKUdZf9sAS9gtOVv2tmg==,iv:uQ0Roe+XefzMjZCF3It+U2D1MWPMT5f6CPwlz0gQ5W0=,tag:wSgp0CVr6Y6M3eqcoTy8cw==,type:str]
ETHERCALC: ENC[AES256_GCM,data:0ScnDsUNBt6wYJC4hTXn8huuTptBTDKZV4yFVQ4fuBWc6auWNWhDQlTc0ImJoK6efr2uyp3sVu3o+KlCNvUGhDOJ1you6socyTgRP0q7oLPC+Ln+bFP8gWG8v2nyEFY=,iv:YqvVjBFG/WZg1l4aMAiioOruWZ9zcTMr74DVW+1+2DQ=,tag:ePBXd4ddipJtxhFE1amfMg==,type:str]
GITEA: ENC[AES256_GCM,data:TGsye52g8DVOk51o1dWfF6x3m6BFPe0MtUbOayxYejaYSZ4cDCfx02EPAhiL3FJGLfidZt7+WpjhVqJvFeCvJ1OXdYMQyuL3akPBCmEjmBgBhJvEjVtVgV3aNMS21gy3o0RG/MXDXOLpjHeaXn3G/XWKsv5bw+gg94bDirOguLC5eaFxddn5/UGFvfwXzDmkoNmc9zufGvVpUkRy8rju/TjOz4Q5GZk1gXWtJWdkTGhuVYVBDDHIQq9DBS0qXi8tP3FvY8prOi/c5ZbyyZjTOgNpWB9uU4HkHz4aUTMFIrPFwUeWmNMIdyUmarSQ7tDDdhBfIiLhb94jaMATHq+PfwwpCcmiTfBEG3OS/3SXPMWg6jhfUxvxsKNT2HQca31B9IRqlitaio3r6KEPBTYh2nWtFO9d5Is7TUWkGsOaV+0JSiNPNh5uD7kNyBxsKUKcND7JXMxW/TZOqARY64x6IwDrbcypsyMW5i+4Er1/0HCvZ7FBH1XkTul8vBGF5ZGD/tYp/m6Ld26GKYkj967eYIwlNFcOMGNue5PyJfFCq0qWbLWO9dFA9c2e9r81DCD8y/0S3Ts7X7TkXVGShm7JoMqb8dzg9i6CoNlmbPAM5GwoNV3ftQugvqEGMk5UIvf05YsgSNvq4UJbhsT4bZqpV0plnxYD5TswCGF/XQ3nwwFTudmf1OLcgYwM2NckbVui128o1Rw=,iv:vo6l0QirLIUvwLN675LYkffkXejJecvBesLJvoW/bjY=,tag:zyLyiCskF84A3QVoq5X3iw==,type:str]
SEARXNG: ENC[AES256_GCM,data:gtKhEmMemzLRl4c3cYhMAQ+5vUth1IhWQeLvW1YtaG5TbhQHBR4PDREQOlGt+tlfGQrft+FeNhMSN/SKOp8gmScVWa+9qmltzxRGRpLm3m/VuBZvOlGdeUcKAX8zEH6A,iv:B2UEtjTRIjT6W+tH2gtcl6XMvZNgbvZUXTiBePGOu24=,tag:SHIF6eaWBLwy9RrEy1N9kg==,type:str]
UMAMI: ENC[AES256_GCM,data:O3kQ6YTWH3xbu4Tkpyh2S0HZvqymUUDDYQbN3I6TX3OUxoNZcogPn+X8AY9FoaS2/amqo0X4YCQCimEgCfdXqknydmMD5x957H5Lc/XyYxB9zk6DIdU9gN6yO4uZClG34TqOsjpjZkExivykNsTzaFfPRcmCz1EVXpY4GVAAKNWxmr7M/99bnT6mSE8zQUj0g/gskCeWGP4bRbpMFtvPOvGChmwjSoMDp5GmYdISiZsacHSv0Zp0o9XSYBWbux4ePHcFvWP+alnToY0oF9g+NJPyzj2X1ihtbcZxgLLHyfGkI5uYKNsuUjB9K+D8u1xLE2AGRPyBrRQPZftSw/Zu1brIpz13HicDbJkR+0zmWBxG1WsTnyhIbIn12o2lwO6CVPLWhuRIx5hv7xm8hTfBdOwsVZZfXCr/ZWkERSzQhp/yDN2Fq5IpyZHdhmMnpkkg1G1oiWtN,iv:7HPZ7b6OlxCoXSkdDlLNpQT1xkF0Uj8IYs+Cx701CYU=,tag:+PFDVpzkbd2tM5JKnNpIUw==,type:str]
sops:
age:
- recipient: age1sxzuhh2fcd4pmaz4mdqq95t683d32ft22w9t2r7pk258u0s8wymsqdj7lg
@@ -26,8 +28,8 @@ sops:
S1NaTVFTL0FCdm1EQmRsUnlhclZNZlEKEgIe60qkvY8+UocjQU+WM2dTL/1y3Kqk
d4RrlLP9NSozwVsPYI4ntygvMSApbT4v0YvoO7gV90lkGWEvW1YDfA==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-05-11T22:33:41Z"
mac: ENC[AES256_GCM,data:276HHpEW56HOvKKbNPM79QrEBYDM590bOLfsgssSb79jm+LzrgLlYk2QImmXArADWby4Ai4jBPL4EahNm+a3aBazMEbwAu+EorvORE2P12W5C1ztskx5XUI3yDKY96jlZvmpXsqefa2pOQc1USk8ai/Obd5MLK06kMr2w3a7P9s=,iv:NJoe1lvw1hrWNL79Ux065UkSEDEEc0+NqlqB4tk3mAw=,tag:YTjIvEP1BO69Pa0qispMLQ==,type:str]
lastmodified: "2026-05-12T19:20:38Z"
mac: ENC[AES256_GCM,data:j/fFN/zTHHSbH+RUdu/wK80Gu+qfoVDiVSDQWrBfL1Orezohf3hlGWJrXu8UtYa82CRVJ5BHzp589GQJTgI8pMaDirbQfpRA6jnYvLaWIU0lN66cnyZ8x919Lc67ceZPqih1q8CDCjRG40NZSwOpRqQVqpJMiuZjFaJqec0BDMM=,iv:7QIA0DfLPmXOjKvbsquYQIWqXbyPGWa+SLCEHH7IawU=,tag:jNFpqDDKF3mUQclpmwvGQg==,type:str]
pgp:
- created_at: "2026-05-05T23:46:27Z"
enc: |-

View File

@@ -5,161 +5,9 @@ let
(name: type: type == "directory" && builtins.pathExists (systemsDir + "/${name}/cfg.nix"))
(builtins.readDir systemsDir));
userOpt = with lib; {
username = mkOption { type = types.str; };
pubssh = mkOption { type = types.str; default=""; };
wm = mkOption {
type = types.enum [ "Wayland" "X11" "-" ];
default = "-";
};
git = {
username = mkOption { type = types.str; default = "Anonymous";};
email = mkOption { type = types.str; default = "anonymous@domain"; };
key = mkOption { type = types.nullOr types.str; default=null; };
};
};
netOpt = with lib; {
ble = {
enable = mkOption {
type = types.bool;
default = false;
};
};
wlp = {
enable = mkOption {
type = types.bool;
default = false;
};
nif = mkOption {
type = types.str;
default = "";
};
};
wg = {
enable = mkOption {
type = types.bool;
default = false;
};
ip4 = mkOption {
type = types.str;
default = "";
};
ip6 = mkOption {
type = types.str;
default = "";
};
pubkey = mkOption {
type = types.str;
default = "";
};
};
};
makeOpt = with lib; {
cli = mkOption {
type = types.bool;
default = true;
};
gui = mkOption {
type = types.bool;
default = false;
};
virt = mkOption {
type = types.bool;
default = false;
};
power = mkOption {
type = types.bool;
default = false;
};
game = mkOption {
type = types.bool;
default = false;
};
develop = mkOption {
type = types.bool;
default = false;
};
};
serverOpt = with lib; {
hostDomain = mkOption { type = types.str; };
mailDomain = mkOption { type = types.str; };
mailServer = mkOption { type = types.str; };
configPath = mkOption {
type = types.str;
default = "/media/config";
};
dataPath = mkOption {
type = types.str;
default = "/media/data";
};
colorScheme = mkOption {
#type = types.submodule {
# options = {
# slug = mkOption { type = types.str; };
# name = mkOption { type = types.str; };
# palette = mkOption {
type = types.attrs; #default = {};# };
#};
# };
default = (lib.evalModules { modules =[ { freeformType = with lib.types; attrsOf anything; } ../colors ];}).config.colorScheme ;
};
containers = mkOption {
type = types.attrsOf (types.submodule {
options = {
enable = mkOption { type = types.bool;default = false; };
db = mkOption { type = types.bool;default = false; };
sops = mkOption { type = types.bool;default = false; };
ip = mkOption { type = types.nullOr types.str; default = null;};
subdomain = mkOption { type = types.nullOr types.str; default=null;};
port = mkOption { type = types.nullOr types.port; default = null; };
extra = mkOption { type = types.attrs; default = {}; };
};
});
default = {};
};
openssh = mkOption {
type = types.bool;
default = false;
};
wireguard = mkOption {
type = types.bool;
default = false;
};
web = mkOption {
type = types.bool;
default = false;
};
ipfw = {
enable = mkOption {
type = types.bool;
default = false;
};
ifs = mkOption {
type = types.listOf types.str;
default = [ ];
};
ports = mkOption {
type = types.listOf (types.listOf (types.oneOf [ types.str types.int ]));
default = [];
description = "Forwarding rules: [ [srcInterface dstAddr srcPort dstPort] ... ]";
example = [
[ "ens3" "10.10.1.2" "IPV6" 22 2222 ]
[ "ens3" "10.10.1.2" "IPV6" 80 80 ]
[ "ens3" "10.10.1.2" "IPV6" 443 443 ]
];
};
};
db = mkOption {
type = types.listOf (types.str);
default = [ ];
};
};
in with lib; {
options.usercfg = userOpt;
options.usercfg = import ./user.nix {inherit lib;};
options.syscfg = {
hostname = mkOption { type = types.str; };
type = mkOption {
@@ -171,20 +19,17 @@ in with lib; {
default = "x86_64-linux";
};
defaultUser = mkOption { type = types.str; };
make = makeOpt;
net = netOpt;
make = import ./make.nix {inherit lib;};
net = import ./net.nix {inherit lib;};
users = mkOption {
type = types.listOf (types.submodule { options = userOpt; });
type = types.listOf (types.submodule { options = import ./user.nix {inherit lib;}; });
default = [ ];
};
peers = mkOption {
default = map (name: import (systemsDir + "/${name}/cfg.nix")) systemNames;
};
server = mkOption {
type = types.oneOf [
types.bool
(types.submodule { options = serverOpt; })
];
type = types.oneOf [ types.bool (types.submodule { options = import ./server.nix {inherit lib;}; }) ];
default = false;
};
};

View File

@@ -0,0 +1,9 @@
{ lib,... }:
with lib; {
cli = mkOption { type = types.bool; default = true; };
gui = mkOption { type = types.bool; default = false; };
virt = mkOption { type = types.bool; default = false; };
power = mkOption { type = types.bool; default = false; };
game = mkOption { type = types.bool; default = false; };
develop = mkOption { type = types.bool; default = false; };
}

View File

@@ -0,0 +1,14 @@
{ lib,... }:
with lib; {
ble.enable = mkOption { type = types.bool; default = false; };
wlp = {
enable = mkOption { type = types.bool; default = false; };
nif = mkOption { type = types.str; default = ""; };
};
wg = {
enable = mkOption { type = types.bool; default = false; };
ip4 = mkOption { type = types.str; default = ""; };
ip6 = mkOption { type = types.str; default = ""; };
pubkey = mkOption { type = types.str; default = ""; };
};
}

View File

@@ -0,0 +1,92 @@
{ lib,... }:
let
in with lib; {
hostDomain = mkOption { type = types.str; };
mailDomain = mkOption { type = types.str; };
mailServer = mkOption { type = types.str; };
configPath = mkOption {
type = types.str;
default = "/media/config";
};
dataPath = mkOption {
type = types.str;
default = "/media/data";
};
colorScheme = mkOption {
type = types.attrs;
default = (lib.evalModules { modules =[ { freeformType = with lib.types; attrsOf anything; } ../colors ];}).config.colorScheme ;
};
loadedContainers = lib.mkOption {
readOnly = true;
type = lib.types.attrsOf (lib.types.submodule ({ name, ... }: {
options = {
name = lib.mkOption {type = lib.types.str; default = name;};
sops = lib.mkOption {type = lib.types.bool; default = false;};
db = lib.mkOption {type = lib.types.bool; default = false;};
paths = lib.mkOption {type = lib.types.listOf lib.types.attrs; default = [ ];};
containers = lib.mkOption {type = lib.types.attrsOf lib.types.attrs; default = { };};
cron = lib.mkOption {type = lib.types.listOf lib.types.str; default = [ ];};
setup = {
trigger = lib.mkOption {type = lib.types.str; default = "";};
script = lib.mkOption {type = lib.types.nullOr lib.types.package; default = null;};
envFile = lib.mkOption {type = lib.types.nullOr lib.types.str; default = null;};
};
};
}));
};
containers = mkOption {
type = types.attrsOf (types.submodule {
options = {
subdomain = mkOption { type = types.nullOr types.str; default=null;};
subpath = mkOption { type = types.nullOr types.str; default=null;};
port = mkOption { type = types.nullOr types.port; default = null; };
extra = mkOption { type = types.attrs; default = {}; };
};
});
default = {};
};
openssh = mkOption {
type = types.bool;
default = false;
};
wireguard = mkOption {
type = types.bool;
default = false;
};
web = mkOption {
type = types.bool;
default = false;
};
ipfw = {
enable = mkOption {
type = types.bool;
default = false;
};
ifs = mkOption {
type = types.listOf types.str;
default = [ ];
};
ports = mkOption {
type = types.listOf (types.listOf (types.oneOf [ types.str types.int ]));
default = [];
description = "Forwarding rules: [ [srcInterface dstAddr srcPort dstPort] ... ]";
example = [
[ "ens3" "10.10.1.2" "IPV6" 22 2222 ]
[ "ens3" "10.10.1.2" "IPV6" 80 80 ]
[ "ens3" "10.10.1.2" "IPV6" 443 443 ]
];
};
};
db = mkOption {
type = types.listOf (types.str);
default = [ ];
};
}

View File

@@ -0,0 +1,14 @@
{ lib,... }:
with lib; {
username = mkOption { type = types.str; };
pubssh = mkOption { type = types.str; default=""; };
wm = mkOption {
type = types.enum [ "Wayland" "X11" "-" ];
default = "-";
};
git = {
username = mkOption { type = types.str; default = "Anonymous";};
email = mkOption { type = types.str; default = "anonymous@domain"; };
key = mkOption { type = types.nullOr types.str; default=null; };
};
}

View File

@@ -29,37 +29,44 @@
containers = {
traefik = {
enable = true;
sops = true;
subdomain = "traefik";
extra={provider="infomaniak";};
};
authentik = {
enable = true;
db = true;
subdomain = "sso";
port = 9000;
};
nextcloud = {
enable = true;
db = true;
subdomain = "cloud";
};
collabora = {
enable = true;
sops = true;
subdomain = "office";
};
etherpad = {
enable = true;
db = true;
subdomain = "pad";
};
ethercalc = {
subdomain = "pad";
};
gitea = {
enable = true;
db = true;
subdomain = "git";
};
searxng = {
subdomain = "searx";
};
jellyfin = {
subdomain = "flix";
};
transmission = {
subdomain = "rflix";
subpath = "p2p";
};
servarr = {
subdomain = "arr";
};
umami = {
subdomain = "umami";
};
};
};
};