Compare commits

..

13 Commits

13 changed files with 407 additions and 60 deletions

View File

@@ -15,6 +15,8 @@ let
};
};
in {
sops = false;
db = false;
paths = [{
path="${serverCfg.configPath}/example/";
mode = "0444";

View File

@@ -7,6 +7,7 @@ let
NEXTCLOUD_DOMAIN = "${serverCfg.containers.nextcloud.subdomain or "nextcloud"}.${serverCfg.hostDomain}";
AUTHENTIK_DOMAIN = "${containerCfg.subdomain}.${serverCfg.hostDomain}";
COOKIE_DOMAIN = "${serverCfg.hostDomain}";
AUTHENTIK_LDAP_DC_DOMAIN = "dc=ldap," + (lib.concatMapStringsSep "," (x: "dc=${x}") (lib.splitString "." serverCfg.hostDomain));
};
};
in {
@@ -29,19 +30,20 @@ in {
port = 9000;
secret = name;
extraEnv = {
"AUTHENTIK_REDIS__HOST" = builder.host;
"AUTHENTIK_POSTGRESQL__HOST" = builder.host;
"AUTHENTIK_POSTGRESQL__USER" = "authentik_user";
"AUTHENTIK_POSTGRESQL__NAME" = "authentik_db";
"AUTHENTIK_EMAIL__HOST" = serverCfg.mailDomain;
"AUTHENTIK_EMAIL__PORT" = "587";
"AUTHENTIK_EMAIL__USERNAME" = "noreply@${serverCfg.hostDomain}";
"AUTHENTIK_EMAIL__USE_TLS" = "true";
"AUTHENTIK_EMAIL__USE_SSL" = "false";
"AUTHENTIK_EMAIL__TIMEOUT" = "10";
"AUTHENTIK_EMAIL__FROM" = "sso@noreply.${serverCfg.hostDomain}";
"AUTHENTIK_DISABLE_UPDATE_CHECK" = "true";
"AUTHENTIK_POSTGRESQL__SSLMODE" = "disable";
AUTHENTIK_REDIS__HOST = builder.host;
AUTHENTIK_POSTGRESQL__HOST = builder.host;
AUTHENTIK_POSTGRESQL__USER = "authentik_user";
AUTHENTIK_POSTGRESQL__NAME = "authentik_db";
AUTHENTIK_POSAUTHENTIK_POSTGRESQL__SSLMODE = "false";
AUTHENTIK_EMAIL__HOST = serverCfg.mailDomain;
AUTHENTIK_EMAIL__PORT = "587";
AUTHENTIK_EMAIL__USERNAME = "noreply@${serverCfg.hostDomain}";
AUTHENTIK_EMAIL__USE_TLS = "true";
AUTHENTIK_EMAIL__USE_SSL = "false";
AUTHENTIK_EMAIL__TIMEOUT = "10";
AUTHENTIK_EMAIL__FROM = "sso@noreply.${serverCfg.hostDomain}";
AUTHENTIK_DISABLE_UPDATE_CHECK = "true";
AUTHENTIK_POSTGRESQL__SSLMODE = "disable";
};
overrides = {
cmd = [ "server" ];
@@ -58,12 +60,13 @@ in {
image = "ghcr.io/goauthentik/server:${version}";
secret = "authentik";
extraEnv = {
"AUTHENTIK_REDIS__HOST" = builder.host;
"AUTHENTIK_POSTGRESQL__HOST" = builder.host;
"AUTHENTIK_POSTGRESQL__USER" = "authentik_user";
"AUTHENTIK_POSTGRESQL__NAME" = "authentik_db";
"AUTHENTIK_DISABLE_UPDATE_CHECK" = "true";
"AUTHENTIK_POSTGRESQL__SSLMODE" = "disable";
AUTHENTIK_REDIS__HOST = builder.host;
AUTHENTIK_POSTGRESQL__HOST = builder.host;
AUTHENTIK_POSTGRESQL__USER = "authentik_user";
AUTHENTIK_POSTGRESQL__NAME = "authentik_db";
AUTHENTIK_POSAUTHENTIK_POSTGRESQL__SSLMODE = "false";
AUTHENTIK_DISABLE_UPDATE_CHECK = "true";
AUTHENTIK_POSTGRESQL__SSLMODE = "disable";
};
overrides = {
cmd = [ "worker" ];
@@ -74,8 +77,20 @@ in {
];
};
};
};
ldap = builder.mkContainer {
subdomain = containerCfg.subdomain;
image = "ghcr.io/goauthentik/ldap:${version}";
secret = name;
extraEnv = {
"AUTHENTIK_HOST" = "http://${builder.host}:9000";
"AUTHENTIK_INSECURE" = "false";
};
overrides = {
ports = [ "389:3389" "636:6636" ];
};
};
};
setup = {
trigger = "worker";
@@ -85,6 +100,7 @@ in {
$AK apply_blueprint /blueprints/custom/authentik.yaml
$AK apply_blueprint /blueprints/custom/traefik.yaml
$AK apply_blueprint /blueprints/custom/ldap.yaml
${lib.optionalString (serverCfg.containers ? nextcloud) ''$AK apply_blueprint /blueprints/custom/nextcloud.yaml''}
echo "Completed Authentik Setup"

View File

@@ -1,3 +1,95 @@
{...}:{
}
{ config, containerCfg, pkgs, lib, builder, name, ... }:
let
serverCfg = config.syscfg.server;
# Ensure the package is available (Nixpkgs includes frigate)
frigatePkg = pkgs.frigate;
image = pkgs.dockerTools.streamLayeredImage {
name = "frigate";
tag = frigatePkg.version;
contents = [
pkgs.bashInteractive
frigatePkg
pkgs.ffmpeg # Explicitly included for video stream processing
];
config = {
Entrypoint = [ "${frigatePkg}/bin/frigate" ];
Cmd = [ "start" ];
ExposedPorts = {
"5000/tcp" = {}; # Web UI / API
"8554/tcp" = {}; # RTSP Feeds
"8555/tcp" = {}; # WebRTC
};
Env = [
"FRIGATE_RTSP_PASSWORD=secret" # Base fallback, overridden by envFile/sops
];
};
};
in {
sops = true; # Enabled to safeguard sensitive camera RTSP stream credentials
db = false; # Internal SQLite is used by default in Frigate
paths = [
{
path = "${serverCfg.configPath}/frigate/";
mode = "0755";
}
{
path = "/var/lib/frigate/storage/";
mode = "0755"; # Dedicated path for heavy video recordings and media
}
];
containers = {
server = builder.mkContainer {
subdomain = containerCfg.subdomain;
imageStream = image;
port = 5000;
secret = name;
extraEnv = {
PLUS_API_KEY = ""; # Optional: For Frigate Plus users
};
overrides = {
cmd = [ ];
volumes = [
"${serverCfg.configPath}/frigate:/config"
"/var/lib/frigate/storage:/media/frigate"
"/dev/bus/usb:/dev/bus/usb" # Passes Google Coral USB TPU to the container
"/dev/dri:/dev/dri" # Passes Intel/AMD GPU for hardware video decoding
];
};
};
};
setup = {
trigger = "server";
envFile = config.sops.secrets."FRIGATE_ENV".path;
script = pkgs.writeShellScript "setup-frigate" ''
mkdir -p "${serverCfg.configPath}/frigate"
mkdir -p "/var/lib/frigate/storage"
# Bootstrap a standard configuration layout if missing
if [ ! -f "${serverCfg.configPath}/frigate/config.yml" ]; then
cat <<EOF > "${serverCfg.configPath}/frigate/config.yml"
mqtt:
enabled: False # Set to True and define host if connecting to Home Assistant
database:
path: /config/frigate.db
cameras:
dummy_camera: # Replace with your actual RTSP stream details
enabled: false
ffmpeg:
inputs:
- path: rtsp://127.0.0.1:554/live
roles:
- detect
detect:
enabled: false
EOF
fi
'';
};
}

View File

@@ -1,3 +1,43 @@
{...}:{
{ config, containerCfg, pkgs, lib, builder, name, ... }:
let
serverCfg = config.syscfg.server;
image = pkgs.dockerTools.streamLayeredImage {
name = pkgs.home-assistant.name;
tag = pkgs.home-assistant.version;
contents = [ ];
config = {
Entrypoint = [ "${pkgs.home-assistant}/bin/hass" ];
ExposedPorts = {
"8123/tcp" = {};
};
};
};
in {
sops = true;
db = false;
paths = [{
path = "${serverCfg.configPath}/homeassistant/";
mode = "0755";
}];
containers = {
server = builder.mkContainer {
subdomain = containerCfg.subdomain;
imageStream = image;
port = 8123;
secret = name;
extraEnv = {
TZ = config.time.timeZone or "UTC";
};
overrides = {
cmd = [ "--config" "/config" ];
volumes = [
"${serverCfg.configPath}/homeassistant/:/config"
"/run/dbus:/run/dbus:ro" # Required for Bluetooth/mDNS service discovery
];
};
};
};
}

View File

@@ -1,3 +1,57 @@
{...}:{
{ config, containerCfg, pkgs, lib, builder, name,... }:
let
serverCfg = config.syscfg.server;
immichServerImage = pkgs.dockerTools.pullImage {
imageName = "ghcr.io/immich-app/immich-server";
imageDigest = "sha256:d5cb251d7c3bcbb8e5bb974c8de53f3e1f0efcb71e21bde1b66df872a0a2df3d";
sha256 = "0000000000000000000000000000000000000000000000000000";
};
immichMachineLearningImage = pkgs.dockerTools.pullImage {
imageName = "ghcr.io/immich-app/immich-machine-learning";
imageDigest = "sha256:4a25fdcd11c13bc33e8b4e7eef118bc23dbd4df012012ec6d7fb1eeef872ad4d";
sha256 = "0000000000000000000000000000000000000000000000000000";
};
in {
sops = false;
db = true;
paths = [{
path = "${serverCfg.configPath}/immich/cache";
mode = "0750";
}{
path = "${serverCfg.dataPath}/immich/";
mode = "0750";
}];
containers = {
server = builder.mkContainer {
subdomain = containerCfg.subdomain;
imageStream = immichServerImage;
port = 2283;
secret = name;
extraEnv = {
DB_URL = "postgresql://immich_user:\${DB_PASS}@${builder.host}/immich_db";
IMMICH_MACHINE_LEARNING_URL = "http://immich-ml:3003";
REDIS_HOSTNAME = builder.host;
};
overrides = {
volumes = [
"${serverCfg.dataPath}/immich:/usr/src/upload"
];
};
};
immich-ml = builder.mkContainer {
imageStream = immichMachineLearningImage;
port = 3003;
overrides = {
volumes = [
"${serverCfg.configPath}/immich/cache:/cache"
];
};
};
};
}

View File

@@ -0,0 +1,45 @@
{ config, containerCfg, pkgs, lib, builder, name, ... }:
let
serverCfg = config.syscfg.server;
influxPkg = pkgs.influxdb2;
image = pkgs.dockerTools.streamLayeredImage {
name = influxPkg.name;
tag = influxPkg.version;
contents = [ ];
config = {
Entrypoint = [ "${influxPkg}/bin/influxd" ];
ExposedPorts = {
"8086/tcp" = {}; # Combined Engine and UI port
};
};
};
in {
sops = true; # Highly recommended for initial admin passwords and setup tokens
db = false; # Using InfluxDB directly as the primary database
paths = [{
path = "${serverCfg.configPath}/influxdb/";
mode = "0700"; # Strict database permissions
}];
containers = {
server = builder.mkContainer {
subdomain = containerCfg.subdomain;
imageStream = image;
port = 8086;
secret = name;
extraEnv = {
INFLUXD_CONFIG_PATH = "var/lib/influxdb2/config";
INFLUXD_BOLT_PATH = "/var/lib/influxdb2/influxdb.bolt";
INFLUXD_ENGINE_PATH = "/var/lib/influxdb2/engine";
};
overrides = {
volumes = [
"${serverCfg.configPath}/influxdb/:/var/lib/influxdb2"
];
};
};
};
}

View File

@@ -1,3 +1,47 @@
{...}:{
{ config, containerCfg, pkgs, lib, builder, name, ... }:
let
serverCfg = config.syscfg.server;
invidiousImage = pkgs.dockerTools.pullImage {
imageName = "quay.io/invidious/invidious";
imageDigest = "sha256:7b5cfca1b369cbb87a6c983a54d588cb375ff60c6d71b3e1f0e2f59265f2a1b9"; # Pin tag digest
sha256 = lib.fakeSha256;
};
companionImage = pkgs.dockerTools.pullImage {
imageName = "quay.io/invidious/inv-sig-helper";
imageDigest = "sha256:2d150b07b1406b3a0c25a5f1e8e25d6b46efbb12dbfde6125026bc9812a647ad";
sha256 = lib.fakeSha256;
};
in {
sops = true;
db = true;
containers = {
server = builder.mkContainer {
subdomain = containerCfg.subdomain;
imageStream = invidiousImage;
port = 3000;
secret = name;
extraEnv = {
INVIDIOUS_DATABASE_URL = "postgres://invidious_user:\${DB_PASS}@${builder.host}/invidious_db";
INVIDIOUS_HMAC_KEY = "\${HMAC_KEY}";
INVIDIOUS_COMPANION_URL = "http://invidious-companion:12999";
INVIDIOUS_PO_TOKEN = "\${PO_TOKEN}";
INVIDIOUS_VISITOR_DATA = "\${VISITOR_DATA}";
INVIDIOUS_PORT = "3000";
INVIDIOUS_COMPANION_KEY = "\${INVIDIOUS_KEY}";
INVIDIOUS_DOMAIN = "${containerCfg.subdomain}.${serverCfg.hostDomain}";
#registration_enabled: false
};
};
companion = builder.mkContainer {
imageStream = companionImage;
port = 12999;
overrides = {
cmd = [ "--tcp" "0.0.0.0:12999" ];
};
};
};
}

View File

@@ -1,7 +1,7 @@
{ config, containerCfg, pkgs, lib, builder, name, ... }:
let
serverCfg = config.syscfg.server;
image = pkgs.dockerTools.streamLayeredImage {
image = pkgs.dockerTools.streamLayeredImage { # pkgs.dockerTools.buildImage{#
name = pkgs.jellyfin.name;
tag = pkgs.jellyfin.version;
contents = [
@@ -12,12 +12,7 @@ let
ExposedPorts = { "8096/tcp" = { }; };
};
};
#LDAP_DC_DOMAIN = "dc=ldap,dc=helcel,dc=net"
#HOST=...
#LDAP_BIND_USER=ldap-sa
#LDAP_BIND_PASSWORD=...
#LDAP_GROUP=flix
#LDAP_ADMIN=admin
in {
paths = [
{
@@ -34,6 +29,7 @@ in {
server = builder.mkContainer {
subdomain = containerCfg.subdomain;
imageStream = image;
# imageFile = image;
port = 8096;
# secret = name;
extraEnv = {
@@ -52,7 +48,7 @@ in {
"--logdir" "/config/log"
];
volumes = [
"${serverCfg.dataPath}:/media:ro"
"${serverCfg.dataPath}/media:/media:ro"
"${serverCfg.configPath}/jellyfin:/config"
];
# If you have an Intel/AMD GPU for transcoding, add the device:
@@ -60,4 +56,13 @@ in {
};
};
};
#LDAP_DC_DOMAIN = "dc=ldap,dc=helcel,dc=net"
#HOST=...
#LDAP_BIND_USER=ldap-sa
#LDAP_BIND_PASSWORD=...
#LDAP_GROUP=flix
#LDAP_ADMIN=admin
}

View File

@@ -8,7 +8,7 @@ let
contents = with pkgs; [ cacert openssl ];
config = {
Cmd = [ "${appPkg}/${binaryPath}" "-nobrowser" "-data=/config" ];
Env = [ "DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1" ];
Env = [ "DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1" "HOME=/tmp" ];
};
};
@@ -22,12 +22,13 @@ let
};
sharedVolumes = [
"${serverCfg.mediaPath or "/mnt/media"}:/media" # Fast hardlinking requires a single shared root
"${serverCfg.dataPath}/media:/media" # Fast hardlinking requires a single shared root
"${serverCfg.configPath}/servarr:/config-root"
];
in {
sops = true;
paths = [
{ path = "${serverCfg.dataPath}/media/"; mode = "0755"; }
{ path = "${serverCfg.configPath}/servarr/prowlarr"; mode = "0755"; }
{ path = "${serverCfg.configPath}/servarr/radarr"; mode = "0755"; }
{ path = "${serverCfg.configPath}/servarr/sonarr"; mode = "0755"; }
@@ -40,6 +41,9 @@ in {
imageStream = images.prowlarr;
port = 9696;
secret = name;
extraOptions = [
"--tmpfs=/tmp:rw,noexec,nosuid,size=512m"
];
overrides.volumes = sharedVolumes ++ [ "${serverCfg.configPath}/servarr/prowlarr:/config" ];
};
@@ -49,6 +53,9 @@ in {
imageStream = images.radarr;
port = 7878;
secret = name;
extraOptions = [
"--tmpfs=/tmp:rw,noexec,nosuid,size=512m"
];
overrides.volumes = sharedVolumes ++ [ "${serverCfg.configPath}/servarr/radarr:/config" ];
};
@@ -58,6 +65,9 @@ in {
imageStream = images.sonarr;
port = 8989;
secret = name;
extraOptions = [
"--tmpfs=/tmp:rw,noexec,nosuid,size=512m"
];
overrides.volumes = sharedVolumes ++ [ "${serverCfg.configPath}/servarr/sonarr:/config" ];
};
};

View File

@@ -13,7 +13,7 @@ let
else subdomain;
base = {
image = if imageStream != null then "${imageStream.imageName}:${imageStream.imageTag}"
else image;
else if imageFile != null then "${imageFile.imageName}:${imageFile.imageTag}" else image;
imageStream = imageStream;
imageFile = imageFile;

View File

@@ -0,0 +1,41 @@
version: 1
metadata:
name: Pre-configured LDAP Outpost
entries:
# 1. Define the LDAP Provider
- model: authentik_providers_ldap.ldapprovider
identifiers:
name: ldap-provider
attrs:
base_dn: "DC=ldap,@AUTHENTIK_LDAP_DC_DOMAIN@"
search_group: null
authorization_flow:
!Find [
authentik_flows.flow,
[slug, default-provider-authorization-implicit-consent],
]
invalidation_flow:
!Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]]
# 2. Define the Token with a static Key
- model: authentik_core.token
identifiers:
identifier: ldap-outpost-static-token
attrs:
intent: api
# MANDATORY: Explicitly set your long, secure pre-shared token here
key: !Env AUTHENTIK_LDAP
user: 1 # Assigns to default akadmin user
# 3. Define the Outpost linking the Provider and the Token
- model: authentik_outposts.outpost
identifiers:
name: LDAP Outpost
attrs:
type: ldap
providers:
- !Find [authentik_providers_ldap.ldapprovider, [name, ldap-provider]]
token:
!Find [authentik_core.token, [identifier, ldap-outpost-static-token]]
config:
log_level: info

View File

@@ -15,14 +15,13 @@ in{
lib.mapAttrs' (cName: cCfg: lib.nameValuePair "${appName}-${cName}" cCfg) app.containers
) config.syscfg.server.loadedContainers;
allPathConfigs = lib.concatMap (app: app.paths) appsList;
allSetupConfigs = lib.concatMap (app: if app.setup?script then [({name = app.name; envFile="";} // app.setup)] else []) appsList;
allCronsConfigs = lib.concatMap (app: app.cron) appsList;
in{
virtualisation.oci-containers = {
backend = "podman";
containers = mergedContainers;
};
system.activationScripts.container-setup-dirs = {
deps = [ "users" "groups" ];
text = lib.concatStringsSep "\n" (map (cfg:
@@ -48,25 +47,23 @@ in{
'';
startAt = "weekly";
};
} // 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";
};
} // lib.listToAttrs (lib.concatMap (e: [{
name = "${e.name}-setup";
value = {
description = "Run ${e.name} setup";
after = [ "podman-${e.name}-${e.trigger}.service" ];
wants = [ "podman-${e.name}-${e.trigger}.service" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
TimeoutStartSec = "360s";
EnvironmentFile = e.envFile;
ExecStart = e.script;
RemainAfterExit = true;
User = "root";
};
}] else []
) appsList);
};
}]) allSetupConfigs );
services.cron = {
enable = true;

View File

@@ -9,6 +9,7 @@ AUTHENTIK: |
POSTGRES_PASSWORD=...
AUTHENTIK_SECRET_KEY=...
AUTHENTIK_EMAIL__PASSWORD=...
AUTHENTIK_TOKEN=...
NEXTCLOUD: |
DB_PASSWORD=...
POSTGRES_PASSWORD=...