Migrated from multi repos to monorepo architecture.

This commit is contained in:
Raphaël Numbus
2026-05-02 12:52:08 +02:00
parent 72668492f5
commit 73adb395c0
218 changed files with 9639 additions and 57 deletions
+13
View File
@@ -0,0 +1,13 @@
{ ... }:
{
imports = [
./hardware/default.nix
./mail/default.nix
./misc/default.nix
./networking/default.nix
./packages/default.nix
./services/default.nix
./global.nix
];
}
+11
View File
@@ -0,0 +1,11 @@
{
description = "Numbus Server Module";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
};
outputs = { self, nixpkgs }: {
nixosModules.numbus = import ./modules/default.nix;
};
}
+53
View File
@@ -0,0 +1,53 @@
{ lib, ... }:
with lib;
{
options.numbus = {
owner = mkOption {
type = types.str;
example = "Alex";
default = "Numbus";
description = "The name of the person who owns this server";
};
language = mkOption {
type = types.str;
example = "FR";
default = "FR";
description = "The language for this server";
};
locale = mkOption {
type = types.str;
example = "fr_FR";
default = "fr_FR";
description = "The default locale for this server";
};
services = {
domain = mkOption {
type = types.str;
example = "numbus.eu";
description = "The root domain name (i.e. example.com) that your services will use";
};
dns = mkOption {
type = types.enum [ "pi-hole" "adguard" ];
default = "pi-hole";
example = "pi-hole";
description = "The preferred DNS resolver service (pi-hole or adguard) that other services should depend on";
};
};
email = {
administratorEmail = mkOption {
type = types.str;
example = "admin@your-domain.com";
description = "The email that will be used to send critical notifications such as hardware failures, services errors, ACME updates, etc";
};
userEmail = mkOption {
type = types.str;
example = "user@your-domain.com";
description = "The email that will be used by services to send notifications";
};
};
};
}
+13
View File
@@ -0,0 +1,13 @@
{ config, ... }:
{
config = {
boot.initrd.systemd.enable = true;
boot.loader.systemd-boot.enable = true;
boot.loader.efi.canTouchEfiVariables = true;
boot.kernel.sysctl = {
"vm.overcommit_memory" = 1;
};
};
}
+9
View File
@@ -0,0 +1,9 @@
{ config, ... }:
{
config = {
hardware.enableRedistributableFirmware = true;
hardware.cpu.intel.updateMicrocode = true;
hardware.cpu.amd.updateMicrocode = true;
};
}
+10
View File
@@ -0,0 +1,10 @@
{ ... }:
{
imports = [
./boot.nix
./cpu.nix
./disks.nix
./pcie-coral.nix
];
}
+328
View File
@@ -0,0 +1,328 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.numbus.hardware;
mkDataDisk = idx: device: {
name = "content-${toString idx}";
value = {
type = "disk";
device = device;
content = {
type = "gpt";
partitions = {
luks = {
size = "100%";
content = {
type = "luks";
name = "crypted-content-${toString idx}";
initrdUnlock = false;
settings = {
keyFile = "/etc/secrets/disks/content-${toString idx}";
allowDiscards = true;
crypttabExtraOpts = [ "nofail" ];
};
content = {
type = "filesystem";
format = cfg.dataDisksFilesystem;
mountpoint = "/mnt/content-${toString idx}";
mountOptions = [ "nofail" "noauto" ];
};
};
};
};
};
};
};
mkParityDisk = idx: device: {
name = "parity-${toString idx}";
value = {
type = "disk";
device = device;
content = {
type = "gpt";
partitions = {
luks = {
size = "100%";
content = {
type = "luks";
name = "crypted-parity-${toString idx}";
initrdUnlock = false;
settings = {
keyFile = "/etc/secrets/disks/parity-${toString idx}";
allowDiscards = true;
crypttabExtraOpts = [ "nofail" ];
};
content = {
type = "filesystem";
format = cfg.parityDisksFilesystem;
mountpoint = "/mnt/parity-${toString idx}";
mountOptions = [ "nofail" "noauto" ];
};
};
};
};
};
};
};
isMirror = length cfg.bootDisksList > 1;
bootDisksConfig = if isMirror then {
mdadm = {
boot = {
type = "mdadm";
level = 1;
metadata = "1.2";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
mountOptions = [ "umask=0077" ];
};
};
};
disk = listToAttrs (imap1 (i: device: {
name = "boot-${toString i}";
value = {
type = "disk";
device = device;
content = {
type = "gpt";
partitions = {
ESP = {
size = "1G";
type = "EF00";
content = {
type = "mdraid";
name = "boot";
};
};
luks = {
size = "100%";
content = {
type = "luks";
name = "crypted-boot-${toString i}";
settings = {
allowDiscards = true;
keyFile = "/etc/secrets/disks/boot-${toString i}";
};
content = {
type = "lvm_pv";
vg = "pool";
};
};
};
};
};
};
}) cfg.bootDisksList);
} else {
disk = {
"boot-1" = {
type = "disk";
device = head cfg.bootDisksList;
content = {
type = "gpt";
partitions = {
ESP = {
size = "1G";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
mountOptions = [ "umask=0077" ];
};
};
luks = {
size = "100%";
content = {
type = "luks";
name = "crypted-boot-1";
settings = {
keyFile = "/etc/secrets/disks/boot-1";
allowDiscards = true;
};
content = {
type = "lvm_pv";
vg = "pool";
};
};
};
};
};
};
};
};
lvmConfig = {
lvm_vg = {
pool = {
type = "lvm_vg";
lvs = {
swap = {
size = cfg.swapSize;
content = {
type = "swap";
};
} // optionalAttrs isMirror { lvm_type = "mirror"; };
snapraid = {
size = "1G";
content = {
type = "filesystem";
format = "btrfs";
mountpoint = "/mnt/content-0";
};
} // optionalAttrs isMirror { lvm_type = "mirror"; };
root = {
size = "100%";
content = {
type = "btrfs";
extraArgs = [ "-f" ];
subvolumes = {
"/rootfs" = {
mountpoint = "/";
mountOptions = [ "compress=zstd" "noatime" ];
};
"/home" = {
mountpoint = "/home";
mountOptions = [ "compress=zstd" ];
};
"/nix" = {
mountpoint = "/nix";
mountOptions = [ "compress=zstd" "noatime" ];
};
};
};
} // optionalAttrs isMirror { lvm_type = "mirror"; };
};
};
};
};
in
{
options.numbus.hardware = {
dataDisksList = mkOption {
type = types.listOf types.str;
default = [];
example = [ "/dev/disk/by-id/WD_Blue_ATO431_159Ejz224G0000382b" "/dev/disk/by-id/Seagate_Barracuda_159Ejz224G" ];
description = "List by-id path of devices for data disks";
};
parityDisksList = mkOption {
type = types.listOf types.str;
default = [];
example = [ "/dev/disk/by-id/WD_Blue_ATO431_159Ejz224G0000382b" "/dev/disk/by-id/Seagate_Barracuda_159Ejz224G" ];
description = "List of by-id path of devices for parity disks";
};
bootDisksList = mkOption {
type = types.listOf types.str;
default = [];
example = [ "/dev/disk/by-id/nvme_SAMSUNG_MZVPYEHCO_159Ejz224G0000" "/dev/disk/by-id/ata-San_Disk_159Ejz224G" ];
description = "List of by-id path of devices for boot disks";
};
spindownDisksList = mkOption {
type = types.listOf types.str;
default = [];
example = [ "/dev/disk/by-id/WD_Blue_ATO431_159Ejz224G0000382b" "/dev/disk/by-id/Seagate_Barracuda_159Ejz224G" ];
description = "List of by-id path of devices to spindown when inactive to save power (HDD only)";
};
swapSize = mkOption {
type = types.str;
default = "16G";
example = "16G";
description = "Size of the swap partition";
};
dataDisksFilesystem = mkOption {
type = types.enum [ "xfs" "ext4" "btrfs" ];
default = "xfs";
example = "xfs";
description = "Filesystem for data disks. Available filesystem options : xfs, ext4, btrfs";
};
parityDisksFilesystem = mkOption {
type = types.enum [ "xfs" "ext4" "btrfs" ];
default = "xfs";
example = "xfs";
description = "Filesystem for parity disks. Available filesystem options : xfs, ext4, btrfs";
};
};
config = mkIf (cfg.bootDisksList != []) {
disko.devices = mkMerge [
bootDisksConfig
lvmConfig
{
disk = listToAttrs (imap1 mkDataDisk cfg.dataDisksList);
}
{
disk = listToAttrs (imap1 mkParityDisk cfg.parityDisksList);
}
];
services.snapraid = {
enable = true;
contentFiles = [ "/mnt/content-0/snapraid.content" ] ++
(map (i: "/mnt/content-${toString i}/snapraid.content") (range 1 (length cfg.dataDisksList)));
parityFiles = map (i: "/mnt/parity-${toString i}/snapraid.parity") (range 1 (length cfg.parityDisksList));
dataDisks = listToAttrs (imap1 (i: _: nameValuePair "d${toString i}" "/mnt/content-${toString i}") cfg.dataDisksList);
};
fileSystems."/mnt/data" = {
device = "/mnt/content-*";
fsType = "fuse.mergerfs";
options = [
"category.create=ff"
"cache.files=partial"
"dropcacheonclose=true"
"defaults"
"noauto"
"nofail"
"allow_other"
"moveonenospc=1"
"minfreespace=50G"
"func.getattr=newest"
"fsname=mergerfs_data"
"x-mount.mkdir"
"x-systemd.automount"
];
};
systemd.services.mount-disks = {
description = "Mount data and parity disks";
before = [ "mnt-data.mount" ];
requiredBy = [ "mnt-data.mount" ];
path = [ pkgs.cryptsetup pkgs.util-linux ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
script = let
mountDataDisk = i: ''
if [ ! -e /dev/mapper/crypted-content-${toString i} ]; then
cryptsetup luksOpen --key-file /etc/secrets/disks/content-${toString i} /dev/disk/by-partlabel/disk-content-${toString i}-luks crypted-content-${toString i}
fi
mkdir -p /mnt/content-${toString i}
if ! mountpoint -q /mnt/content-${toString i}; then
mount -t ${cfg.dataDisksFilesystem} /dev/mapper/crypted-content-${toString i} /mnt/content-${toString i}
fi
'';
mountParityDisk = i: ''
if [ ! -e /dev/mapper/crypted-parity-${toString i} ]; then
cryptsetup luksOpen --key-file /etc/secrets/disks/parity-${toString i} /dev/disk/by-partlabel/disk-parity-${toString i}-luks crypted-parity-${toString i}
fi
mkdir -p /mnt/parity-${toString i}
if ! mountpoint -q /mnt/parity-${toString i}; then
mount -t ${cfg.parityDisksFilesystem} /dev/mapper/crypted-parity-${toString i} /mnt/parity-${toString i}
fi
'';
in ''
${concatMapStrings mountDataDisk (range 1 (length cfg.dataDisksList))}
${concatMapStrings mountParityDisk (range 1 (length cfg.parityDisksList))}
'';
};
};
}
+111
View File
@@ -0,0 +1,111 @@
{ config, lib, pkgs, ... }:
let
cfg = config.numbus.hardware.pcie-coral;
gasket-driver = { stdenv, lib, fetchFromGitHub, kernel }: stdenv.mkDerivation rec {
pname = "gasket";
version = "1.0-18";
src = fetchFromGitHub {
owner = "google";
repo = "gasket-driver";
rev = "97aeba584efd18983850c36dcf7384b0185284b3";
sha256 = "pJwrrI7jVKFts4+bl2xmPIAD01VKFta2SRuElerQnTo=";
};
makeFlags = [
"-C"
"${kernel.dev}/lib/modules/${kernel.modDirVersion}/build"
"M=$(PWD)"
];
buildFlags = [ "modules" ];
installFlags = [ "INSTALL_MOD_PATH=${placeholder "out"}" ];
installTargets = [ "modules_install" ];
sourceRoot = "source/src";
hardeningDisable = [ "pic" "format" ];
nativeBuildInputs = kernel.moduleBuildDependencies;
meta = with lib; {
description = "The Coral Gasket Driver allows usage of the Coral EdgeTPU on Linux systems.";
homepage = "https://github.com/google/gasket-driver";
license = licenses.gpl2;
maintainers = [ maintainers.kylehendricks ];
platforms = platforms.linux;
};
};
libedgetpu-pkg = { stdenv, lib, fetchFromGitHub, libusb1, abseil-cpp, flatbuffers, xxd }:
let
flatbuffers_1_12 = flatbuffers.overrideAttrs (oldAttrs: rec {
version = "1.12.0";
NIX_CFLAGS_COMPILE = "-Wno-error=class-memaccess -Wno-error=maybe-uninitialized";
cmakeFlags = (oldAttrs.cmakeFlags or []) ++ ["-DFLATBUFFERS_BUILD_SHAREDLIB=ON"];
NIX_CXXSTDLIB_COMPILE = "-std=c++17";
configureFlags = (oldAttrs.configureFlags or []) ++ ["--enable-shared"];
src = fetchFromGitHub {
owner = "google";
repo = "flatbuffers";
rev = "v${version}";
sha256 = "sha256-L1B5Y/c897Jg9fGwT2J3+vaXsZ+lfXnskp8Gto1p/Tg=";
};
});
in stdenv.mkDerivation rec {
pname = "libedgetpu";
version = "grouper";
src = fetchFromGitHub {
owner = "google-coral";
repo = pname;
rev = "release-${version}";
sha256 = "sha256-73hwItimf88Iqnb40lk4ul/PzmCNIfdt6Afi+xjNiBE=";
};
makeFlags = ["-f" "makefile_build/Makefile" "libedgetpu" ];
buildInputs = [
libusb1
abseil-cpp
flatbuffers_1_12
];
nativeBuildInputs = [
xxd
];
NIX_CXXSTDLIB_COMPILE = "-std=c++17";
TFROOT = "${fetchFromGitHub {
owner = "tensorflow";
repo = "tensorflow";
rev = "v2.7.4";
sha256 = "sha256-liDbUAdaVllB0b74aBeqNxkYNu/zPy7k3CevzRF5dk0=";
}}";
enableParallelBuilding = false;
installPhase = ''
mkdir -p $out/lib
cp out/direct/k8/libedgetpu.so.1.0 $out/lib
ln -s $out/lib/libedgetpu.so.1.0 $out/lib/libedgetpu.so.1
mkdir -p $out/lib/udev/rules.d
cp debian/edgetpu-accelerator.rules $out/lib/udev/rules.d/99-edgetpu-accelerator.rules
'';
};
gasket = config.boot.kernelPackages.callPackage gasket-driver {};
libedgetpu = pkgs.callPackage libedgetpu-pkg {};
in
{
options.numbus.hardware.pcie-coral = lib.mkEnableOption "PCIe Coral TPU support";
config = lib.mkIf cfg {
services.udev.packages = [ libedgetpu ];
users.groups.plugdev = {};
boot.extraModulePackages = [ gasket ];
};
}
+24
View File
@@ -0,0 +1,24 @@
{ config, lib, ... }:
with lib;
{
options.numbus-backup = {
hardware = {
HddSpindown = {
enable = mkOption {
description = "Spin down Hard drives when inactive in order to save power.";
type = types.bool;
example = true;
default = true;
};
optimize = mkOption {
description = "Optimize services to reduce HDD wakeups when HddSpindown is enabled. Can be set to \"compatible\" to optimize all compatible services, or a list of service names to optimize.";
type = types.nullOr (types.either (types.enum [ "compatible" ]) (types.listOf types.str));
default = "compatible";
example = "[ \"crafty\" \"gitea\" ]";
};
};
};
};
}
+89
View File
@@ -0,0 +1,89 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.numbus.services.clamav;
clamav_notifier = pkgs.writeScript "clamav-notify.sh" ''
#!${pkgs.bash}/bin/bash
# Check if triggered by Real-time event (file exists)
if [ -f /var/lib/clamav/virus_event.env ]; then
source /var/lib/clamav/virus_event.env
rm /var/lib/clamav/virus_event.env
fi
ADMIN_EMAIL="${config.numbus.mail.adminAddress}"
USER_EMAIL="${config.numbus.mail.userAddress}"
OWNER_NAME="${config.numbus.owner}"
if [ -n "$CLAM_VIRUSEVENT_VIRUSNAME" ]; then
# --- Real-time / VirusEvent Mode ---
SUBJECT="Numbus Server Alert: Virus Detected (Real-time)"
# Retrieve logs from clamav-daemon
LOGS=$(journalctl -u clamav-daemon.service -n 50 --no-pager | grep "FOUND")
TECH_BODY="
ClamAV Real-time Alert:
Server owner: $OWNER_NAME
Virus detected: $CLAM_VIRUSEVENT_VIRUSNAME
File: $CLAM_VIRUSEVENT_FILENAME
Logs:
$LOGS
Action taken: Access blocked (OnAccessPrevention).
Please investigate manually.
"
FRIENDLY_BODY="Cher/Chère $OWNER_NAME,
L'antivirus de votre serveur a détecté et bloqué une menace en temps réel.
Fichier : $CLAM_VIRUSEVENT_FILENAME
Votre administrateur a été notifié.
"
else
# --- Scheduled Scan Summary Mode ---
SUBJECT="Numbus Server Alert: Virus Detected during Scheduled Scan"
# Retrieve logs (clamdscan prints FOUND when a virus is detected)
LOGS=$(journalctl -u clamav-periodic-scan.service -n 100 --no-pager | grep "FOUND")
TECH_BODY="
ClamAV Scan Alert:
Server owner: $OWNER_NAME
Viruses detected:
$LOGS
Action taken: Detection only.
Please investigate manually.
"
FRIENDLY_BODY="Cher/Chère $OWNER_NAME,
L'antivirus de votre serveur a détecté une menace potentielle lors de l'analyse périodique.
Votre administrateur a été notifié avec les détails techniques.
Nous vous conseillons d'être prudent avec vos fichiers récents.
"
fi
printf "Subject: [ADMIN] %s\n\n%s" "$SUBJECT" "$TECH_BODY" | /run/wrappers/bin/sendmail -t "$ADMIN_EMAIL"
printf "Subject: [Alerte] Menace détectée sur votre serveur Numbus\n\n%s\n\nMerci de votre confiance,\nL'équipe de support,\nNumbus-Server." "$FRIENDLY_BODY" | /run/wrappers/bin/sendmail -t "$USER_EMAIL"
'';
in
{
config = mkIf cfg.enable {
systemd.services.clamav-virus-notify = {
description = "Email notification for ClamAV virus detection";
serviceConfig = {
Type = "oneshot";
ExecStart = "${clamav_notifier}";
};
};
};
}
+10
View File
@@ -0,0 +1,10 @@
{ ... }:
{
imports = [
./clamav.nix
./smart.nix
./systemd.nix
./smtp.nix
];
}
+61
View File
@@ -0,0 +1,61 @@
{ config, pkgs, ... }:
let
smartd_notifier = pkgs.writeScript "smartd-notify.sh" ''
#!${pkgs.bash}/bin/bash
# 1. Send Technical Email to Admin
ADMIN_EMAIL="${config.numbus.mail.adminAddress}"
SUBJECT="Numbus Server Alert: $SMARTD_FAILTYPE on $SMARTD_DEVICE"
TECH_BODY="
SMARTD Alert Details:
Server owner: $OWNER_NAME
Device: $SMARTD_DEVICE
Type: $SMARTD_DEVICETYPE
Failure Type: $SMARTD_FAILTYPE
Message: $SMARTD_MESSAGE
Full Message:
$SMARTD_FULLMESSAGE
"
printf "Subject: [ADMIN] $SUBJECT\n\n$TECH_BODY" | /run/wrappers/bin/sendmail -t "$ADMIN_EMAIL"
# 2. Send Friendly Email to Owner
USER_EMAIL="${config.numbus.mail.userAddress}"
OWNER_NAME="${config.numbus.owner}"
FRIENDLY_BODY="Cher/Chère $OWNER_NAME,
Votre serveur a automatiquement détecté une panne matérielle de disque dur.
Ce genre de panne est tout à fait normal selon l'âge de votre matériel et n'entraîne
dans la grande majorité des cas aucune perte de données grâce au système de
stockage redondant préventif.
Votre administrateur a été notifié de cette panne. Il vous recontactera dans de très
brefs délais afin de procéder au remplacement, si nécessaire, du disque dur défaillant.
Merci de votre confiance,
L'équipe de support,
Numbus-Server."
printf "Subject: [Alerte] Défaillance matérielle sur votre serveur Numbus\n\n$FRIENDLY_BODY" | /run/wrappers/bin/sendmail -t "$USER_EMAIL"
'';
in
{
services.smartd = {
enable = true;
defaults.autodetected = "-a -o on -S on -s (S/../.././00|L/../../6/01) -n standby,q -M exec ${smartd_notifier}";
notifications = {
wall = {
enable = true;
};
mail = {
enable = true;
sender = config.numbus.mail.fromAddress;
recipient = "${config.numbus.mail.userAddress},${config.numbus.mail.adminAddress}";
};
};
};
}
+84
View File
@@ -0,0 +1,84 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.numbus.mail;
in
{
options.numbus.mail = {
enable = mkEnableOption "Email sending functionality";
userAddress = mkOption {
description = "The address of the user this server will send emails to";
type = types.str;
example = "user@your-domain.com";
};
adminAddress = mkOption {
description = "The address of the admin this server will send emails to";
type = types.str;
example = "admin@your-domain.com";
};
smtpUsername = mkOption {
description = "The username/email that will be use to authenticate to the SMTP server";
type = types.str;
example = "your-smtp-enabled-address@your-domain.com";
};
smtpPasswordPath = mkOption {
description = "The path to a file containing the password that will be use to authenticate to the SMTP server";
type = types.path;
example = /run/secrets/smtp-password;
};
fromAddress = mkOption {
description = "This server will send emails from this address";
type = types.str;
default = "numbus-server-noreply@${config.numbus.services.domain}";
example = "numbus-server-noreply@your-domain.com";
};
smtpServer = mkOption {
description = "The SMTP server address your server will use to send emails";
type = types.str;
default = "smtp.gmail.com";
example = "smtp.your-provider.com";
};
smtpPort = mkOption {
description = "The SMTP port your server will connect to to send emails";
type = types.port;
default = 587;
example = 587;
};
};
config = mkIf cfg.enable {
environment.etc."aliases".text = ''
root: ${cfg.userAddress}, ${cfg.adminAddress}
default: ${cfg.userAddress}, ${cfg.adminAddress}
'';
programs.msmtp = {
enable = true;
defaults = {
aliases = "/etc/aliases";
timeout = 60;
syslog = "on";
};
accounts.default = {
auth = true;
host = cfg.smtpServer;
port = cfg.smtpPort;
from = cfg.fromAddress;
user = cfg.smtpUsername;
tls = true;
tls_starttls = true;
passwordeval = "${pkgs.coreutils}/bin/cat ${cfg.smtpPasswordPath}";
};
};
};
}
+55
View File
@@ -0,0 +1,55 @@
{ config, pkgs, ... }:
let
systemd_notifier = pkgs.writeScript "systemd-email-notify.sh" ''
#!${pkgs.bash}/bin/bash
# The failing service name is passed as the first argument
UNIT=$1
# 1. Send Technical Email to Admin
ADMIN_EMAIL="${config.numbus.mail.adminAddress}"
SUBJECT="Numbus Server Alert: Service $UNIT Failed"
# Retrieve recent logs for context
LOGS=$(journalctl -u "$UNIT" -n 20 --no-pager)
TECH_BODY="
Systemd Service Failure Alert:
Server owner: ${config.numbus.owner}
Service: $UNIT
Recent Logs:
$LOGS
"
printf "Subject: [ADMIN] $SUBJECT\n\n$TECH_BODY" | /run/wrappers/bin/sendmail -t "$ADMIN_EMAIL"
# 2. Send Friendly Email to Owner
USER_EMAIL="${config.numbus.mail.userAddress}"
OWNER_NAME="${config.numbus.owner}"
FRIENDLY_BODY="Cher/Chère $OWNER_NAME,
Votre serveur a détecté une défaillance du service $UNIT.
Le système a tenté de gérer l'erreur, mais une intervention peut être nécessaire.
Votre administrateur a été notifié de cet incident avec les détails techniques nécessaires.
Il interviendra si une action manuelle est requise.
Merci de votre confiance,
L'équipe de support,
Numbus-Server."
printf "Subject: [Alerte] Erreur sur votre serveur Numbus\n\n$FRIENDLY_BODY" | /run/wrappers/bin/sendmail -t "$USER_EMAIL"
'';
in
{
systemd.services."service-failure-notify@" = {
description = "Email notification for failed service %i";
onFailure = [ ];
serviceConfig = {
Type = "oneshot";
ExecStart = "${systemd_notifier} %i";
};
};
}
+10
View File
@@ -0,0 +1,10 @@
{ ... }:
{
imports = [
./internationalisation.nix
./power.nix
./update.nix
./users.nix
];
}
@@ -0,0 +1,24 @@
{ config, lib, ... }:
{
config = {
i18n.defaultLocale = "${config.numbus.locale}.UTF-8";
i18n.extraLocaleSettings = {
LC_ADDRESS = "${config.numbus.locale}.UTF-8";
LC_IDENTIFICATION = "${config.numbus.locale}.UTF-8";
LC_MEASUREMENT = "${config.numbus.locale}.UTF-8";
LC_MONETARY = "${config.numbus.locale}.UTF-8";
LC_NAME = "${config.numbus.locale}.UTF-8";
LC_NUMERIC = "${config.numbus.locale}.UTF-8";
LC_PAPER = "${config.numbus.locale}.UTF-8";
LC_TELEPHONE = "${config.numbus.locale}.UTF-8";
LC_TIME = "${config.numbus.locale}.UTF-8";
};
console.keyMap = lib.toLower config.numbus.language;
services.xserver.xkb = {
layout = lib.toLower config.numbus.language;
variant = "";
};
};
}
+30
View File
@@ -0,0 +1,30 @@
{ config, lib, pkgs, ... }:
let
hardDrives = config.numbus.hardware.spindownDisksList;
in
{
config = {
systemd.services.hd-idle = {
description = "External HD spin down daemon";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "simple";
ExecStart =
let
idleTime = toString 1800;
hardDriveParameter = lib.strings.concatMapStringsSep " " (x: "-a ${x} -i ${idleTime}") hardDrives;
in
"${pkgs.hd-idle}/bin/hd-idle -i 0 ${hardDriveParameter}";
};
};
services.autoaspm.enable = true;
powerManagement.powertop.enable = true;
boot.kernelParams = [
"pcie_aspm=force"
"consoleblank=60"
];
};
}
+23
View File
@@ -0,0 +1,23 @@
{ config, inputs, ... }:
{
config = {
system.autoUpgrade = {
enable = true;
allowReboot = false;
flake = inputs.self.outPath;
flags = [ "--print-build-logs" ];
dates = "02:00";
randomizedDelaySec = "45min";
};
nix.gc = {
automatic = true;
dates = "weekly";
options = "--delete-older-than 7d";
};
nix.settings.experimental-features = [ "nix-command" "flakes" ];
nix.settings.auto-optimise-store = true;
};
}
+16
View File
@@ -0,0 +1,16 @@
{ config, pkgs, ... }:
{
users.users.numbus-admin = {
shell = pkgs.fish;
isNormalUser = true;
description = "Numbus Admin";
extraGroups = [ "wheel" ];
uid = 1000;
initialPassword = "changeMe!";
# required for auto start before user login
linger = true;
# required for rootless container with multiple users
autoSubUidGidRange = true;
};
}
+8
View File
@@ -0,0 +1,8 @@
{ ... }:
{
imports = [
./firewall.nix
./networking.nix
];
}
+13
View File
@@ -0,0 +1,13 @@
{ config, pkgs, lib, ... }:
{
config = {
networking.nftables.enable = true;
networking.firewall = {
enable = true;
allowPing = true;
allowedTCPPorts = [ 53 80 443 ];
allowedUDPPorts = [ 53 443 ];
};
};
}
+60
View File
@@ -0,0 +1,60 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.numbus.networking;
in
{
options.numbus.networking = {
ipAddress = mkOption {
description = "The IP address that this server will use";
type = types.str;
example = "192.168.1.100";
};
interface = mkOption {
description = "The interface that this server will use to connect to the network";
type = types.str;
example = "enp1s0";
};
routerIpAddress = mkOption {
description = "The IP address of the router of your network";
type = types.str;
example = "192.168.1.1";
};
networkSubnet = mkOption {
description = "The subnet of your network";
type = types.str;
default = "";
example = "192.168.1.0/24";
};
dnsServers = mkOption {
description = "The list of DNS servers that this server will use";
type = types.listOf types.str;
default = [ "${cfg.ipAddress}" "9.9.9.9" ];
example = [ "${cfg.ipAddress}" "9.9.9.9" ];
};
};
config = {
networking.hostName = "numbus-server";
networking.networkmanager.enable = false;
# Allow rootless containers to bind to port 53 and up
boot.kernel.sysctl."net.ipv4.ip_unprivileged_port_start" = 53;
networking.bridges.br0.interfaces = [ "${cfg.interface}" ];
networking.interfaces."${cfg.interface}".useDHCP = false;
networking.interfaces.br0.useDHCP = false;
networking.nameservers = cfg.dnsServers;
networking.interfaces.br0.ipv4.addresses = [{
address = "${cfg.ipAddress}";
prefixLength = 24;
}];
networking.defaultGateway = {
address = "${cfg.routerIpAddress}";
interface = "br0";
};
};
}
+10
View File
@@ -0,0 +1,10 @@
{ ... }:
{
imports = [
./packages.nix
./podman.nix
./ssh.nix
./terminal.nix
];
}
+25
View File
@@ -0,0 +1,25 @@
{ config, pkgs, ... }:
{
nixpkgs.config.allowUnfree = true;
environment.systemPackages = with pkgs; [
git
ncdu
fastfetch
tpm2-tss
sops
age
powertop
pciutils
hdparm
hd-idle
hddtemp
smartmontools
cpufrequtils
intel-gpu-tools
snapraid
mergerfs
mergerfs-tools
];
}
+16
View File
@@ -0,0 +1,16 @@
{ pkgs, ... }:
{
virtualisation.podman.enable = true;
virtualisation.podman.defaultNetwork.settings.dns_enabled = true;
virtualisation.containers.containersConf.settings = {
network.default_rootless_network_cmd = "slirp4netns";
};
environment.systemPackages = with pkgs; [
podman-compose
podman-tui
slirp4netns
];
}
+5
View File
@@ -0,0 +1,5 @@
{ config, ... }:
{
services.openssh.enable = true;
}
+24
View File
@@ -0,0 +1,24 @@
{ config, pkgs, ... }:
{
environment.systemPackages = with pkgs; [
fish
fishPlugins.fzf-fish
fishPlugins.grc
grc
fzf
];
programs.fish = {
enable = true;
interactiveShellInit = ''
set fish_greeting # Disable greeting
fastfetch
echo -e "\n\nWelcome to your Numbus-Server !\n\n- This system is managed by NixOS\n- All changes are futile\n- Please consider buying support if you can't get your server running\n- Have a nice day and enjoy !"
'';
shellAliases = {
nixup = "cd /etc/nixos/ && sudo nix flake update && sudo nixos-rebuild --flake . switch --upgrade && cd -";
nixwitch = "cd /etc/nixos/ && sudo nix flake update && sudo nixos-rebuild --flake . switch && cd -";
};
};
}
@@ -0,0 +1,171 @@
{ config, pkgs, lib, ... }:
with lib;
let
# Container config
name = "authelia";
# Version tagging
autheliaVersion = "v4.39.16";
databaseVersion = "18.3";
# Helper
helper = import ../service-helper.nix { inherit config pkgs lib; };
cfg = config.numbus-server.services.authelia;
# Derive Base DN from domain (e.g., example.com -> dc=example,dc=com)
domainParts = splitString "." config.numbus-server.services.domain;
baseDN = concatStringsSep "," (map (p: "dc=${p}") domainParts);
# Generate dynamic access control rules based on groups and allowedApps
mkGroupRule = groupName: appName:
let
app = config.numbus-server.service.${appName} or {};
in
if app ? subdomain && app ? domain then ''
- domain: "${app.subdomain}.${app.domain}"
policy: two_factor
subject: "group:${groupName}"''
else "";
allGroupRules = concatStringsSep "\n" (filter (s: s != "") (flatten (mapAttrsToList (groupName: groupCfg:
map (appName: mkGroupRule groupName appName) (groupCfg.allowedApps or [])
) (config.numbus-server.groups or {}))));
defaultRedirectionUrl =
if config.numbus-server.services.homepage.enable then
"https://${config.numbus-server.services.homepage.subdomain}.${config.numbus-server.services.domain}"
else if config.numbus-server.services.dashy.enable then
"https://${config.numbus-server.services.dashy.subdomain}.${config.numbus-server.services.domain}"
else null;
in
helper.mkPodmanService {
inherit name;
pod = name;
description = "Authelia, your own unified login provider";
defaultPort = "9091";
dependencies = [
"sops-install-secrets.service"
"traefik.service"
"${config.numbus-server.services.dns}.service"
];
middlewares = [
"secureHeaders"
];
dirPermissions = [
"100999:100 ${cfg.configDir}"
];
secrets = [
"authelia/db_name"
"authelia/db_username"
"authelia/db_password"
"authelia/jwt_secret"
"authelia/session_secret"
"authelia/storage_secret"
];
composeText = ''
services:
authelia-server:
image: ghcr.io/authelia/authelia:${autheliaVersion}
container_name: authelia-server
hostname: authelia-server
user: '1000:1000'
networks:
authelia:
ipv4_address: 10.89.251.253
ports:
- "${cfg.port}:9091/tcp"
volumes:
- ${cfg.configDir}/server:/config
security_opt:
- no-new-privileges:true
cap_drop:
- NET_RAW
restart: unless-stopped
authelia-database:
container_name: authelia-database
hostname: authelia-database
image: docker.io/library/postgres:${databaseVersion}
user: '1000:1000'
networks:
authelia:
ipv4_address: 10.89.251.252
environment:
POSTGRES_DB: ${config.sops.placeholder."authelia/db_name"}
POSTGRES_USER: ${config.sops.placeholder."authelia/db_username"}
POSTGRES_PASSWORD: ${config.sops.placeholder."authelia/db_password"}
POSTGRES_INITDB_ARGS: '--data-checksums'
volumes:
- ${cfg.configDir}/database:/var/lib/postgresql/data
shm_size: 128mb
healthcheck:
disable: false
security_opt:
- no-new-privileges:true
cap_drop:
- NET_RAW
restart: unless-stopped
networks:
authelia:
driver: bridge
name: authelia
ipam:
config:
- subnet: "10.89.251.0/24"
gateway: "10.89.251.254"
'';
extraConfig = {
sops.templates."authelia-config" = {
gid = "100";
uid = "100999";
mode = "0400";
content = ''
authelia:
identity_validation:
reset_password:
jwt_secret: "${config.sops.placeholder."authelia/jwt_secret"}"
jwt_lifespan: "5 minutes"
jwt_algorithm: "HS256"
storage:
encryption_key: "${config.sops.placeholder."authelia/storage_secret"}"
postgres:
address: "tcp://authelia-database:5432"
database: "${config.sops.placeholder."authelia/db_name"}"
username: "${config.sops.placeholder."authelia/db_username"}"
password: "${config.sops.placeholder."authelia/db_password"}"
session:
secret: "${config.sops.placeholder."authelia/session_secret"}"
cookies:
- domain: "${config.numbus-server.services.domain}"
authelia_url: "https://${cfg.subdomain}.${config.numbus-server.services.domain}"
${optionalString (defaultRedirectionUrl != null) "default_redirection_url: \"${defaultRedirectionUrl}\""}
authentication_backend:
ldap:
implementation: "lldap"
address: "ldap://host.containers.internal:3890"
base_dn: "${baseDN}"
user: "UID=authelia,OU=people,${baseDN}"
password: "${config.sops.placeholder."lldap/"}"
notifier:
smtp:
address: submission://${config.numbus-server.mail.smtpHost}:${config.numbus-server.mail.smtpPort}
username: ${config.numbus-server.mail.smtpUsername}
password: ${config.sops.placeholder.smtpPassword}
sender: ${config.numbus-server.mail.fromAddress}
tls:
server_name: ${config.numbus-server.mail.smtpHost}
minimum_version: TLS1.2
skip_verify: false
access_control:
default_policy: 'deny'
rules:
- domain: "*.${config.numbus-server.service.domain}"
policy: two_factor
subject: "group:admin"
${allGroupRules}
'';
path = "/etc/authelia/authelia.yaml";
};
};
}
@@ -0,0 +1,74 @@
{ config, pkgs, lib, ... }:
with lib;
let
# Container config
name = "crafty";
# Version tagging
craftyVersion = "v4.10.1";
# Storage optimization
spindown = config.numbus-server.hardware.HddSpindown;
optimizedDir = if spindown.enable && (spindown.optimize == "compatible" || (isList spindown.optimize && elem name spindown.optimize))
then cfg.configDir
else cfg.dataDir;
# Helper
helper = import ../service-helper.nix { inherit config pkgs lib; };
cfg = config.numbus-server.services.crafty;
in
helper.mkPodmanService {
inherit name;
description = "Crafty controller, one place to manage your minecraft servers";
defaultPort = "8443";
scheme = "https";
dataDirEnabled = optimizedDir == cfg.dataDir;
middlewares = [
"secureHeaders"
];
dirPermissions = [
"100999:100 ${cfg.configDir}"
"100999:100 ${cfg.configDir}/log"
"100999:100 ${cfg.configDir}/config"
"100999:100 ${optimizedDir}/import"
"100999:100 ${optimizedDir}/backups"
"100999:100 ${optimizedDir}/servers"
];
composeText = ''
services:
crafty:
image: registry.gitlab.com/crafty-controller/crafty-4:${craftyVersion}
container_name: crafty
user: '1000:1000'
networks:
crafty:
ipv4_address: 10.89.250.253
ports:
- "${cfg.port}:8443/tcp"
- "19132:19132/udp"
- "25500-25600:25500-25600"
volumes:
- ${optimizedDir}/backups:/crafty/backups
- ${optimizedDir}/servers:/crafty/servers
- ${optimizedDir}/import:/crafty/import
- ${cfg.configDir}/logs:/crafty/logs
- ${cfg.configDir}/config:/crafty/app/config
environment:
- TZ=${time.timeZone}
security_opt:
- no-new-privileges:true
cap_drop:
- NET_RAW
restart: unless-stopped
networks:
crafty:
driver: bridge
name: crafty
ipam:
config:
- subnet: "10.89.250.0/24"
gateway: "10.89.250.254"
'';
}
@@ -0,0 +1,97 @@
{ config, pkgs, lib, ... }:
with lib;
let
# Container config
name = "dashy";
# Version tagging
dashyVersion = "v3.2.3";
# Helper
helper = import ../service-helper.nix { inherit config pkgs lib; };
cfg = config.numbus-server.services.dashy;
in
helper.mkPodmanService {
inherit name;
description = "Dashy, the ultimate dashboard for your homelab";
defaultPort = "8999";
configDirEnabled = false;
dataDirEnabled = false;
middlewares = [
"secureHeaders"
];
composeText = ''
services:
dashy:
image: lissy93/dashy:${dashyVersion}
container_name: dashy
hostname: dashy
user: '1000:1000'
networks:
dashy:
ipv4_address: 10.89.235.253
ports:
- ${cfg.port}:8080
volumes:
- ${config.sops."dashy/config".path}:/app/user-data/conf.yml
environment:
- UID=1000
- GID=1000
- NODE_ENV=production
healthcheck:
test: ['CMD', 'node', '/app/services/healthcheck']
interval: 1m30s
timeout: 10s
retries: 3
start_period: 40s
security_opt:
- no-new-privileges:true
cap_drop:
- NET_RAW
restart: unless-stopped
networks:
dashy:
driver: bridge
name: dashy
ipam:
config:
- subnet: "10.89.235.0/24"
gateway: "10.89.235.254"
'';
extraConfig = {
sops.templates."dashy/config" = {
gid = "100";
uid = "100999";
mode = "0440" ;
content = ''
pageInfo:
title: My Homelab
sections:
- name: Example Section
icon: far fa-rocket
items:
- title: GitHub
description: Dashy source code and docs
icon: fab fa-github
url: https://github.com/Lissy93/dashy
- title: Issues
description: View open issues, or raise a new one
icon: fas fa-bug
url: https://github.com/Lissy93/dashy/issues
- name: Local Services
items:
- title: Firewall
icon: favicon
url: http://192.168.1.1/
- title: Game Server
icon: https://i.ibb.co/710B3Yc/space-invader-x256.png
url: http://192.168.130.1/
'';
path = "/etc/dashy/dashy.yaml";
};
};
}
@@ -0,0 +1,29 @@
{ ... }:
{
imports = [
# Good
./gitea.nix
./immich.nix
./nextcloud.nix
./passbolt.nix
./traefik.nix
# Testing needed
./authelia.nix
./crafty.nix
./dashy.nix
./frigate.nix
./home-assistant.nix
./homepage.nix
./it-tools.nix
./jellyfin.nix
./lldap.nix
./n8n.nix
./netbird.nix
./netbootxyz.nix
./ntfy.nix
./odoo.nix
./uptime-kuma.nix
./vscodium.nix
];
}
@@ -0,0 +1,83 @@
{ config, pkgs, lib, ... }:
with lib;
let
# Container config
name = "frigate";
# Version tagging
frigateVersion = "0.16.4";
# Helper
helper = import ../service-helper.nix { inherit config pkgs lib; };
cfg = config.numbus-server.services.frigate;
in
helper.mkPodmanService {
inherit name;
pod = "home-assistant";
description = "Frigate, your fully-local NVR (Network Video Recorder)";
defaultPort = "8971";
scheme = "https";
dependencies = [
"sops-install-secrets.service"
"traefik.service"
"authelia.service"
"home-assistant.service"
"${config.numbus-server.services.dns}.service"
];
middlewares = [
"secureHeaders"
];
dirPermissions = [
"1000:100 ${cfg.configDir}"
"1000:100 ${cfg.dataDir}"
];
composeText = ''
services:
frigate:
image: ghcr.io/blakeblackshear/frigate:${frigateVersion}
container_name: frigate
hostname: frigate
shm_size: "256mb"
networks:
home-assistant:
ipv4_address: 10.89.230.253
ports:
- "${cfg.port}:8971/tcp"
volumes:
- ${cfg.configDir}:/config
- ${cfg.dataDir}:/media/frigate
- /etc/localtime:/etc/localtime:ro
- type: tmpfs
target: /tmp/cache
tmpfs:
size: 1000000000
environment:
- FRIGATE_MQTT_USER=${config.sops.placeholder."home-assistant/mqtt_username"}
- FRIGATE_MQTT_PASSWORD=${config.sops.placeholder."home-assistant/mqtt_password"}
${lib.optionalString (cfg.devices != []) ''
devices:
${lib.concatStringsSep "\n" (map (d: " - \"${d}\"") cfg.devices)}
''}
security_opt:
- no-new-privileges:true
cap_drop:
- NET_RAW
stop_grace_period: 30s
restart: unless-stopped
networks:
home-assistant:
external: true
'';
extraOptions = {
devices = mkOption {
type = types.listOf types.str;
default = [];
example = [ "/dev/dri:/dev/dri" "/dev/bus/usb:/dev/bus/usb" "/dev/apex_0:/dev/apex_0" ];
description = "List of devices to map into the container. /dev/dri is used for graphics acceleration, /dev/bus/usb for USB Coral TPUs, and /dev/apex_0 for PCI coral TPUs";
};
};
}
@@ -0,0 +1,103 @@
{ config, pkgs, lib, ... }:
with lib;
let
# Container config
name = "gitea";
# Version tagging
giteaVersion = "1.25.4-rootless";
databaseVersion = "18-alpine";
# Storage optimization
spindown = config.numbus-server.hardware.HddSpindown;
optimizedDir = if spindown.enable && (spindown.optimize == "compatible" || (isList spindown.optimize && elem name spindown.optimize))
then cfg.configDir
else cfg.dataDir;
# Helper
helper = import ../service-helper.nix { inherit config pkgs lib; };
cfg = config.numbus-server.services.gitea;
in
helper.mkPodmanService {
inherit name;
pod = "false";
description = "Gitea, your own self-hosted git platform";
defaultPort = "3000";
dataDirEnabled = optimizedDir == cfg.dataDir;
middlewares = [
"secureHeaders"
];
dirPermissions = [
"100999:100 ${cfg.configDir}"
"100999:100 ${optimizedDir}/data"
"100999:100 ${cfg.configDir}/config"
"100999:100 ${cfg.configDir}/database"
];
secrets = [
"gitea/db_name"
"gitea/db_username"
"gitea/db_password"
];
composeText = ''
services:
gitea-database:
image: docker.io/library/postgres:${databaseVersion}
container_name: gitea-database
hostname: gitea-database
user: '1000:1000'
networks:
gitea:
ipv4_address: 10.89.240.253
volumes:
- ${cfg.configDir}/database:/var/lib/postgresql
environment:
- POSTGRES_DB=${config.sops.placeholder."gitea/db_name"}
- POSTGRES_USER=${config.sops.placeholder."gitea/db_username"}
- POSTGRES_PASSWORD=${config.sops.placeholder."gitea/db_password"}
security_opt:
- no-new-privileges:true
cap_drop:
- NET_RAW
restart: unless-stopped
gitea-server:
image: docker.gitea.com/gitea:${giteaVersion}
container_name: gitea-server
hostname: gitea-server
user: '1000:1000'
networks:
gitea:
ipv4_address: 10.89.240.252
ports:
- "${cfg.port}:3000/tcp"
volumes:
- ${optimizedDir}/data:/var/lib/gitea
- ${cfg.configDir}/config:/etc/gitea
- /etc/localtime:/etc/localtime:ro
environment:
- GITEA__database__DB_TYPE=postgres
- GITEA__database__HOST=gitea-database:5432
- GITEA__database__NAME=${config.sops.placeholder."gitea/db_name"}
- GITEA__database__USER=${config.sops.placeholder."gitea/db_username"}
- GITEA__database__PASSWD=${config.sops.placeholder."gitea/db_password"}
- GITEA__server__SSH_PORT=2424
- GITEA__server__ROOT_URL=https://${cfg.subdomain}.${config.numbus-server.services.domain}
depends_on:
- gitea-database
security_opt:
- no-new-privileges:true
cap_drop:
- NET_RAW
restart: unless-stopped
networks:
gitea:
driver: bridge
name: gitea
ipam:
config:
- subnet: "10.89.240.0/24"
gateway: "10.89.240.254"
'';
}
@@ -0,0 +1,177 @@
{ config, pkgs, lib, ... }:
with lib;
let
# Container config
name = "home-assistant";
# Version tagging
homeAssistantVersion = "2026.2.3";
mqttVersion = "2.1-alpine";
# Helper
helper = import ../service-helper.nix { inherit config pkgs lib; };
cfg = config.numbus-server.services.home-assistant;
in
helper.mkPodmanService {
inherit name;
description = "Home Assistant, libre house control and much more";
defaultPort = "8123";
dataDirEnabled = false;
middlewares = [
"secureHeaders"
];
dirPermissions = [
"1000:100 ${cfg.configDir}"
"1000:100 ${cfg.configDir}/config"
"100999:100 ${cfg.configDir}/mqtt"
];
secrets = [
"home-assistant/mqtt_user"
"home-assistant/mqtt_password"
];
# Compose file good
composeText = ''
services:
home-assistant:
image: ghcr.io/home-assistant/home-assistant:${homeAssistantVersion}
container_name: home-assistant
hostname: home-assistant
networks:
home-assistant:
ipv4_address: 10.89.230.252
ports:
- "${cfg.port}:8123/tcp"
volumes:
- ${cfg.configDir}/config:/config
- /etc/localtime:/etc/localtime:ro
- /run/dbus:/run/dbus:ro
${lib.optionalString (cfg.devices != []) ''
devices:
${lib.concatStringsSep "\n" (map (d: " - \"${d}\"") cfg.devices)}
''}
security_opt:
- no-new-privileges:true
cap_drop:
- NET_RAW
restart: unless-stopped
home-assistant-mqtt:
image: docker.io/library/eclipse-mosquitto:${mqttVersion}
container_name: home-assistant-mqtt
hostname: home-assistant-mqtt
user: '1000:1000'
networks:
home-assistant:
ipv4_address: 10.89.230.252
volumes:
- ${cfg.configDir}/mqtt:/mosquitto
security_opt:
- no-new-privileges:true
cap_drop:
- NET_RAW
restart: unless-stopped
networks:
home-assistant:
driver: bridge
name: home-assistant
ipam:
config:
- subnet: "10.89.230.0/24"
gateway: "10.89.230.254"
'';
extraOptions = {
devices = mkOption {
type = types.listOf types.str;
default = [];
example = [ "/dev/serial/by-id/Sonoff_Zigbee_3.0-id-port0:/dev/ttyUSB0" ];
description = "List of devices to map into the container. /dev/ttyUSB0 is used for Zigbee dongles";
};
};
extraConfig = {
systemd.services."${name}-quirk" = {
description = "Podman container quirk : ${name}";
after = [ "${name}.service" ];
onFailure = [ "service-failure-notify@%n.service" ];
startLimitBurst = 5;
startLimitIntervalSec = 600;
path = [ pkgs.coreutils pkgs.systemd ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
script = ''
mkdir -p /var/lib/numbus-server/${name}
if [[ -e ${cfg.configDir}/config/configuration.yaml ]]; then
if grep -qF "10.89.230.1/32" ${cfg.configDir}/config/configuration.yaml; then
exit 0
elif grep -qF "use_x_forwarded_for" ${cfg.configDir}/config/configuration.yaml && ! grep -qF "10.89.230.1/32" ${cfg.configDir}/config/configuration.yaml; then
tmp=$(mktemp)
head -n -6 ${cfg.configDir}/config/configuration.yaml > "$tmp"
mv "$tmp" ${cfg.configDir}/config/configuration.yaml
fi
fi
until [[ -e ${cfg.configDir}/config/configuration.yaml ]]; do
sleep 15
done
cat << 'EOF' >> ${cfg.configDir}/config/configuration.yaml
http:
use_x_forwarded_for: true
trusted_proxies: 10.89.230.1
zha:
EOF
systemctl restart ${name}.service
'';
};
};
systemd.services."mqtt-quirk" = {
description = "Podman container quirk : Home-assistant MQTT";
after = [ "sops-install-secrets.service" ];
before = [ "${name}.service" ];
onFailure = [ "service-failure-notify@%n.service" ];
startLimitBurst = 5;
startLimitIntervalSec = 600;
path = [ pkgs.coreutils pkgs.mosquitto ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
script = ''
if [[ -e ${cfg.configDir}/mqtt/mosquitto.conf && ${cfg.configDir}/mqtt/password.txt ]]; then
if grep -qF "listener 1883" ${cfg.configDir}/mqtt/mosquitto.conf; then
exit 0
else
rm ${cfg.configDir}/mqtt/mosquitto.conf
rm ${cfg.configDir}/mqtt/password.txt
touch ${cfg.configDir}/mqtt/mosquitto.conf
touch ${cfg.configDir}/mqtt/password.txt
fi
fi
cat << EOF >> ${cfg.configDir}/mqtt/mosquitto.conf
persistence true
persistence_location /mosquitto/data/
log_dest file /mosquitto/log/mosquitto.log
listener 1883
## Authentication ##
allow_anonymous false
password_file /mosquitto/password.txt
EOF
HOME_ASSISTANT_MQTT_USER=$(cat /run/secrets/home-assistant/mqtt_user)
HOME_ASSISTANT_MQTT_PASSWORD=$(cat /run/secrets/home-assistant/mqtt_password)
mosquitto_passwd -b ${cfg.configDir}/mqtt/password.txt "$HOME_ASSISTANT_MQTT_USER" "$HOME_ASSISTANT_MQTT_PASSWORD"
chmod 0400 ${cfg.configDir}/mqtt/password.txt
'';
};
}
@@ -0,0 +1,63 @@
{ config, pkgs, lib, ... }:
with lib;
let
# Container config
name = "homepage";
# Version tagging
homepageVersion = "v1.10.1";
# Helper
helper = import ../service-helper.nix { inherit config pkgs lib; };
cfg = config.numbus-server.services.homepage;
in
helper.mkPodmanService {
inherit name;
description = "Homepage, a modern and highly customizable application dashboard";
defaultPort = "3003";
middlewares = [
"secureHeaders"
];
dirPermissions = [
"100999:100 ${cfg.configDir}"
"100999:100 ${cfg.configDir}/config"
"100999:100 ${cfg.configDir}/images"
"100999:100 ${cfg.configDir}/icons"
];
composeText = ''
services:
homepage:
image: ghcr.io/gethomepage/homepage:${homepageVersion}
container_name: homepage
hostname: homepage
user: '1000:1000'
networks:
homepage:
ports:
- "${cfg.port}:3000/tcp"
volumes:
- ${cfg.configDir}/config:/app/config
- ${cfg.configDir}/images:/app/public/images
- ${cfg.configDir}/icons:/app/public/icons
environment:
PUID: 1000
PGID: 1000
HOMEPAGE_ALLOWED_HOSTS: ${cfg.subdomain}.${config.numbus-server.services.domain}
security_opt:
- no-new-privileges:true
cap_drop:
- NET_RAW
restart: unless-stopped
networks:
homepage:
driver: bridge
name: homepage
ipam:
config:
- subnet: "10.89.220.0/24"
gateway: "10.89.220.254"
'';
}
@@ -0,0 +1,190 @@
{ config, pkgs, lib, ... }:
with lib;
let
# Container configuration
name = "immich";
# Version tagging
immichVersion = "v2.5.6";
redisVersion = "9@sha256:546304417feac0874c3dd576e0952c6bb8f06bb4093ea0c9ca303c73cf458f63";
databaseVersion = "14-vectorchord0.4.3-pgvectors0.2.0@sha256:bcf63357191b76a916ae5eb93464d65c07511da41e3bf7a8416db519b40b1c23";
# Helper
helper = import ../service-helper.nix { inherit config pkgs lib; };
cfg = config.numbus-server.services.immich;
in
helper.mkPodmanService {
inherit name;
description = "Immich, Google Photos but better";
defaultPort = "2283";
middlewares = [
"immichSecureHeaders"
];
dirPermissions = [
"100999:100 ${cfg.configDir}"
"100999:100 ${cfg.configDir}/redis"
"100999:100 ${cfg.configDir}/model-cache"
"100999:100 ${cfg.configDir}/machine-learning-cache"
"100999:100 ${cfg.configDir}/machine-learning-config"
"100999:100 ${cfg.configDir}/database"
"100999:100 ${cfg.dataDir}"
];
secrets = [
"immich/redis_hostname"
"immich/db_hostname"
"immich/db_name"
"immich/db_username"
"immich/db_password"
];
# Compose file good
composeText = ''
services:
immich-server:
container_name: immich-server
hostname: immich-server
image: ghcr.io/immich-app/immich-server:${immichVersion}
user: '1000:1000'
networks:
immich:
ipv4_address: 10.89.210.253
ports:
- "${cfg.port}:2283/tcp"
volumes:
- $UPLOAD_LOCATION:/data
- /etc/localtime:/etc/localtime:ro
environment:
TZ: $TZ
REDIS_HOSTNAME: ${config.sops.placeholder."immich/redis_hostname"}
DB_HOSTNAME: ${config.sops.placeholder."immich/db_hostname"}
DB_DATABASE_NAME: ${config.sops.placeholder."immich/db_name"}
DB_USERNAME: ${config.sops.placeholder."immich/db_username"}
DB_PASSWORD: ${config.sops.placeholder."immich/db_password"}
IMMICH_TRUSTED_PROXIES: 10.89.210.1
depends_on:
- immich-redis
- immich-database
healthcheck:
disable: false
security_opt:
- no-new-privileges:true
cap_drop:
- NET_RAW
restart: unless-stopped
immich-machine-learning:
container_name: immich-machine-learning
hostname: immich-machine-learning
image: ghcr.io/immich-app/immich-machine-learning:${immichVersion}
user: '1000:1000'
networks:
immich:
ipv4_address: 10.89.210.252
volumes:
- ${cfg.configDir}/model-cache:/cache
- ${cfg.configDir}/machine-learning-config:/usr/src/.config
- ${cfg.configDir}/machine-learning-cache:/usr/src/.cache/
healthcheck:
disable: false
security_opt:
- no-new-privileges:true
cap_drop:
- NET_RAW
restart: unless-stopped
immich-redis:
container_name: immich-redis
hostname: immich-redis
image: docker.io/valkey/valkey:${redisVersion}
user: '1000:1000'
networks:
immich:
ipv4_address: 10.89.210.251
volumes:
- ${cfg.configDir}/redis:/data
healthcheck:
test: redis-cli ping || exit 1
security_opt:
- no-new-privileges:true
cap_drop:
- NET_RAW
restart: unless-stopped
immich-database:
container_name: immich-database
hostname: immich-database
image: ghcr.io/immich-app/postgres:${databaseVersion}
user: '1000:1000'
networks:
immich:
ipv4_address: 10.89.210.250
environment:
POSTGRES_DB: ${config.sops.placeholder."immich/db_name"}
POSTGRES_USER: ${config.sops.placeholder."immich/db_username"}
POSTGRES_PASSWORD: ${config.sops.placeholder."immich/db_password"}
POSTGRES_INITDB_ARGS: '--data-checksums'
volumes:
- $DB_DATA_LOCATION:/var/lib/postgresql/data
shm_size: 128mb
healthcheck:
disable: false
security_opt:
- no-new-privileges:true
cap_drop:
- NET_RAW
restart: unless-stopped
networks:
immich:
driver: bridge
name: immich
ipam:
config:
- subnet: "10.89.210.0/24"
gateway: "10.89.210.254"
'';
extraConfig = {
sops.templates."immich/env" = {
gid = "100";
uid = "1000";
mode = "0400";
content = ''
DB_DATA_LOCATION=${cfg.configDir}/database
UPLOAD_LOCATION=${cfg.dataDir}
'';
path = "/etc/podman/immich/.env";
};
sops.templates."traefik/rules/immich-secureHeaders" = {
gid = "100";
uid = "100999";
mode = "0400";
content = ''
http:
middlewares:
immichSecureHeaders:
headers:
FrameDeny: true
AccessControlAllowMethods: 'GET,POST,PUT,DELETE,OPTIONS'
AccessControlAllowOriginList:
- https://${cfg.subdomain}.${config.numbus-server.services.domain}
- origin-list-or-null
AccessControlMaxAge: 100
AddVaryHeader: true
BrowserXssFilter: true
ContentTypeNosniff: true
ForceSTSHeader: true
STSIncludeSubdomains: true
STSPreload: true
ContentSecurityPolicy: "default-src 'self'; base-uri 'self'; img-src 'self' https://static.immich.cloud https://tiles.immich.cloud data: blob:; connect-src 'self' https://${cfg.subdomain}.${config.numbus-server.services.domain} wss://${cfg.subdomain}.${config.numbus-server.services.domain} https://static.immich.cloud https://tiles.immich.cloud; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; script-src 'self' 'unsafe-inline' 'unsafe-eval'; worker-src 'self' blob: https://${cfg.subdomain}.${config.numbus-server.services.domain}; frame-ancestors 'self';"
CustomFrameOptionsValue: SAMEORIGIN
ReferrerPolicy: same-origin
PermissionsPolicy: vibrate 'self'
STSSeconds: 315360000
'';
path = "/etc/traefik/rules/immich-secureHeaders.yaml";
};
};
}
@@ -0,0 +1,54 @@
{ config, pkgs, lib, ... }:
with lib;
let
# Container config
name = "it-tools";
# Version tagging
it-toolsVersion = "2024.10.22-7ca5933";
# Helper
helper = import ../service-helper.nix { inherit config pkgs lib; };
cfg = config.numbus-server.services.it-tools;
in
helper.mkPodmanService {
inherit name;
description = "IT-tools, useful tools when doing IT";
pod = "false";
defaultPort = "8880";
configDirEnabled = false;
dataDirEnabled = false;
middlewares = [
"secureHeaders"
];
# Compose file good
composeText = ''
services:
it-tools:
image: docker.io/corentinth/it-tools:${it-toolsVersion}
container_name: it-tools
hostname: it-tools
user: '1000:1000'
networks:
it-tools:
ipv4_address: 10.89.200.253
ports:
- "${cfg.port}:80/tcp"
security_opt:
- no-new-privileges:true
cap_drop:
- NET_RAW
restart: unless-stopped
networks:
it-tools:
driver: bridge
name: it-tools
ipam:
config:
- subnet: "10.89.200.0/24"
gateway: "10.89.200.254"
'';
}
@@ -0,0 +1,69 @@
{ config, pkgs, lib, ... }:
with lib;
let
# Container config
name = "jellyfin";
# Version tagging
jellyfinVersion = "10.11.6";
# Helper
helper = import ../service-helper.nix { inherit config pkgs lib; };
cfg = config.numbus-server.services.jellyfin;
in
helper.mkPodmanService {
inherit name;
description = "Jellyfin : A self-hosted media server to stream your movies and music";
defaultPort = "8096";
scheme = "https"; #TODO CHECK
middlewares = [
"secureHeaders"
];
dirPermissions = [
"100999:100 ${cfg.dataDir}"
"100999:100 ${cfg.configDir}"
"100999:100 ${cfg.dataDir}/media"
"100999:100 ${cfg.dataDir}/fonts"
"100999:100 ${cfg.configDir}/cache"
"100999:100 ${cfg.configDir}/config"
];
composeText = ''
services:
jellyfin:
image: docker.io/jellyfin/jellyfin:${jellyfinVersion}
container_name: jellyfin
hostname: jellyfin
user: '1000:1000'
networks:
jellyfin:
ipv4_address: 10.89.190.253
ports:
- "${cfg.port}:8096/tcp"
volumes:
- ${cfg.configDir}/config:/config
- ${cfg.configDir}/cache:/cache
- type: bind
source: ${cfg.dataDir}/media
target: /media
- type: bind
source: ${cfg.dataDir}/fonts
target: /usr/local/share/fonts/custom
read_only: true
cap_drop:
- NET_RAW
security_opt:
- no-new-privileges:true
restart: unless-stopped
networks:
jellyfin:
driver: bridge
name: jellyfin
ipam:
config:
- subnet: "10.89.190.0/24"
gateway: "10.89.190.254"
'';
}
@@ -0,0 +1,84 @@
{ config, pkgs, lib, ... }:
with lib;
let
# Container config
name = "lldap";
# Version tagging
lldapVersion = "v0.6.2";
# Helper
helper = import ../service-helper.nix { inherit config pkgs lib; };
cfg = config.numbus-server.services.lldap;
# Derive Base DN from domain (e.g., example.com -> dc=example,dc=com)
domainParts = splitString "." config.numbus-server.services.domain;
baseDN = concatStringsSep "," (map (p: "dc=${p}") domainParts);
in
helper.mkPodmanService {
inherit name;
pod = "false";
description = "LLDAP, unified user management";
defaultPort = "17170";
dependencies = [
"sops-install-secrets.service"
"network-online.target"
];
middlewares = [
"secureHeaders"
];
dirPermissions = [
"100999:100 ${cfg.configDir}"
];
secrets = [
"lldap/jwt_secret"
"lldap/key_seed"
"lldap/admin_password"
];
composeText = ''
services:
lldap:
image: lldap/lldap:${lldapVersion}
container_name: lldap
hostname: lldap
user: '1000:1000'
networks:
lldap:
ipv4_address: 10.89.185.253
ports:
- "3890:3890"
- "${cfg.port}:17170"
volumes:
- ${cfg.configDir}:/data
environment:
- UID=1000
- GID=1000
- TZ=${config.time.timeZone}
- LLDAP_LDAP_BASE_DN=${baseDN}
- LLDAP_JWT_SECRET="${config.sops.placeholder."lldap/jwt_secret"}"
- LLDAP_KEY_SEED="${config.sops.placeholder."lldap/key_seed"}"
- LLDAP_LDAP_USER_PASS="${config.sops.placeholder."lldap/admin_password"}"
- LLDAP_SMTP_OPTIONS__ENABLE_PASSWORD_RESET=true
- LLDAP_SMTP_OPTIONS__SERVER=${config.numbus-server.mail.smtpServer}
- LLDAP_SMTP_OPTIONS__PORT=${config.numbus-server.mail.smtpPort}
- LLDAP_SMTP_OPTIONS__SMTP_ENCRYPTION=${config.numbus-server.mail.smtpEncryption}
- LLDAP_SMTP_OPTIONS__USER=${config.numbus-server.mail.smtpUsername}
- LLDAP_SMTP_OPTIONS__PASSWORD=${config.sops.placeholder."mail/smtpPassword"}
- LLDAP_SMTP_OPTIONS__FROM=no-reply <${config.numbus-server.mail.fromAddress}>
security_opt:
- no-new-privileges:true
cap_drop:
- NET_RAW
restart: unless-stopped
networks:
lldap:
driver: bridge
name: lldap
ipam:
config:
- subnet: "10.89.185.0/24"
gateway: "10.89.185.254"
'';
}
@@ -0,0 +1,72 @@
{ config, pkgs, lib, ... }:
with lib;
let
# Container config
name = "n8n";
# Version tagging
n8nVersion = "2.11.4";
# Storage optimization
spindown = config.numbus-server.hardware.HddSpindown;
optimizedDir = if spindown.enable && (spindown.optimize == "compatible" || (isList spindown.optimize && elem name spindown.optimize))
then cfg.configDir
else cfg.dataDir;
# Helper
helper = import ../service-helper.nix { inherit config pkgs lib; };
cfg = config.numbus-server.services.n8n;
in
helper.mkPodmanService {
inherit name;
pod = "false";
description = "n8n, the ultimate automation platform";
defaultPort = "5678";
scheme = "https";
middlewares = [
"secureHeaders"
];
dirPermissions = [
"100999:100 ${optimizedDir}"
];
composeText = ''
services:
n8n:
image: docker.n8n.io/n8nio/n8n:${n8nVersion}
container_name: n8n
hostname: n8n
user: '1000:1000'
networks:
n8n:
ipv4_address: 10.89.180.253
ports:
- "${cfg.port}:5678"
volumes:
- ${optimizedDir}:/home/node/.n8n
environment:
- N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true
- N8N_HOST=${cfg.subdomain}.${config.numbus-server.services.domain}
- N8N_PORT=5678
- N8N_PROTOCOL=https
- N8N_RUNNERS_ENABLED=true
- NODE_ENV=production
- WEBHOOK_URL=https://${cfg.subdomain}.${config.numbus-server.services.domain}/
- GENERIC_TIMEZONE=${time.timeZone}
- TZ=${time.timeZone}
security_opt:
- no-new-privileges:true
cap_drop:
- NET_RAW
restart: unless-stopped
networks:
n8n:
driver: bridge
name: n8n
ipam:
config:
- subnet: "10.89.180.0/24"
gateway: "10.89.180.254"
'';
}
@@ -0,0 +1,203 @@
{ config, pkgs, lib, ... }:
with lib;
let
# Container config
name = "netbird";
# Version tagging
netbirdDashboardVersion = "";
netbirdServerVersion = "";
# Helper
helper = import ../service-helper.nix { inherit config pkgs lib; };
cfg = config.numbus-server.services.netbird;
in
helper.mkPodmanService {
inherit name;
pod = "false";
description = "NetBird, an all-in-one ZTNA remote access platform";
defaultPort = "8888";
reverseProxied = false;
dependencies = [
"sops-install-secrets.service"
"traefik.service"
"${config.numbus-server.services.dns}.service"
];
dirPermissions = [
"100999:100 ${cfg.configDir}"
];
secrets = [
"netbird/auth_key"
"netbird/encryption_key"
];
composeText = ''
services:
netbird-dashboard:
image: netbirdio/dashboard:${netbirdDashboardVersion}
container_name: netbird-dashboard
hostname: netbird-dashboard
user: '1000:1000'
networks:
netbird:
ipv4_address: 10.89.175.253
ports:
- "${defaultPort}:8080/tcp"
environment:
# Endpoints
- NETBIRD_MGMT_API_ENDPOINT=https://${cfg.subdomain}.${config.numbus-server.services.domain}
- NETBIRD_MGMT_GRPC_API_ENDPOINT=https://${cfg.subdomain}.${config.numbus-server.services.domain}
# OIDC - using embedded IdP
- AUTH_AUDIENCE=netbird-dashboard
- AUTH_CLIENT_ID=netbird-dashboard
- AUTH_CLIENT_SECRET=
- AUTH_AUTHORITY=https://${cfg.subdomain}.${config.numbus-server.services.domain}/oauth2
- USE_AUTH0=false
- AUTH_SUPPORTED_SCOPES=openid profile email groups
- AUTH_REDIRECT_URI=/nb-auth
- AUTH_SILENT_REDIRECT_URI=/nb-silent-auth
# SSL
- NGINX_SSL_PORT=443
- LETSENCRYPT_DOMAIN=none
logging:
driver: "json-file"
options:
max-size: "500m"
max-file: "2"
security_opt:
- no-new-privileges:true
cap_drop:
- NET_RAW
restart: unless-stopped
netbird-server:
image: netbirdio/netbird-server:${netbirdServerVersion}
container_name: netbird-server
hostname: netbird-server
user: '1000:1000'
networks:
netbird:
ipv4_address: 10.89.175.252
ports:
- "8889:8081/tcp"
- "3478:3478/udp"
volumes:
- ${config.sops.templates."netbird-config".path}:/etc/netbird/config.yaml
- ${cfg.configDir}:/var/lib/netbird
command: ["--config", "/etc/netbird/config.yaml"]
logging:
driver: "json-file"
options:
max-size: "500m"
max-file: "2"
security_opt:
- no-new-privileges:true
cap_drop:
- NET_RAW
restart: unless-stopped
networks:
netbird:
driver: bridge
name: netbird
ipam:
config:
- subnet: "10.89.175.0/24"
gateway: "10.89.175.254"
'';
extraConfig = {
sops.templates."netbird-config" = {
gid = "100";
uid = "100999";
mode = "0400";
content = ''
server:
listenAddress: ":80"
exposedAddress: "https://${cfg.subdomain}.${config.numbus-server.services.domain}:443"
stunPorts:
- 3478
metricsPort: 9090
healthcheckAddress: ":9000"
logLevel: "info"
logFile: "console"
authSecret: "${config.sops.placeholder."netbird/auth_key"}"
dataDir: "/var/lib/netbird"
auth:
issuer: "https://${cfg.subdomain}.${config.numbus-server.services.domain}/oauth2"
signKeyRefreshEnabled: true
dashboardRedirectURIs:
- "https://${cfg.subdomain}.${config.numbus-server.services.domain}/nb-auth"
- "https://${cfg.subdomain}.${config.numbus-server.services.domain}/nb-silent-auth"
cliRedirectURIs:
- "http://localhost:53000/"
reverseProxy:
trustedHTTPProxies:
- "10.89.175.1/32"
store:
engine: "sqlite"
encryptionKey: "${config.sops.placeholder."netbird/encryption_key"}"
'';
path = "/etc/netbird/netbird.yaml";
};
sops.templates."traefik/rules/${name}" = {
gid = "100";
uid = "1000";
mode = "0400";
content = ''
http:
routers:
${name}-dashboard:
rule: "Host(`${cfg.subdomain}.${config.numbus-server.services.domain}`)"
entrypoints:
- "websecure"
middlewares:
- secureHeaders
tls:
certresolver: "cloudflare"
options: "secureTLS"
priority: 1
${name}-grpc:
rule: "Host(`${cfg.subdomain}.${config.numbus-server.services.domain}`) && (PathPrefix(`/signalexchange.SignalExchange/`) || PathPrefix(`/management.ManagementService/`))"
entrypoints:
- "websecure"
service: ${name}-server-h2c
middlewares:
- secureHeaders
tls:
certresolver: "cloudflare"
options: "secureTLS"
${name}-backend:
rule: "Host(`${cfg.subdomain}.${config.numbus-server.services.domain}`) && (PathPrefix(`/relay`) || PathPrefix(`/ws-proxy/`) || PathPrefix(`/api`) || PathPrefix(`/oauth2`))"
entrypoints:
- "websecure"
service: ${name}-server
middlewares:
- secureHeaders
tls:
certresolver: "cloudflare"
options: "secureTLS"
services:${cfg.port}
${name}-dashboard:
loadBalancer:
servers:
- url: "http://host.containers.internal:${cfg.port}"
${name}-server:
loadBalancer:
servers:
- url: "http://host.containers.internal:8889"
${name}-server-h2c:
loadBalancer:
servers:
- url: "h2c://host.containers.internal:3478"
'';
path = "/etc/traefik/rules/${name}";
};
};
}
@@ -0,0 +1,75 @@
{ config, pkgs, lib, ... }:
with lib;
let
# Container config
name = "netbootxyz";
# Version tagging
netbootxyzVersion = "3.0.0";
# Storage optimization
spindown = config.numbus-server.hardware.HddSpindown;
optimizedDir = if spindown.enable && (spindown.optimize == "compatible" || (isList spindown.optimize && elem name spindown.optimize))
then cfg.configDir
else cfg.dataDir;
# Helper
helper = import ../service-helper.nix { inherit config pkgs lib; };
cfg = config.numbus-server.services.netbootxyz;
in
helper.mkPodmanService {
inherit name;
description = "Netboot.xyz, forget about flashing isos on USB sticks with PXE boot";
pod = "false";
defaultPort = "3004";
configDirEnabled = optimizedDir == cfg.configDir;
dataDirEnabled = optimizedDir == cfg.dataDir;
middlewares = [
"secureHeaders"
];
dirPermissions = [
"100999:100 ${cfg.configDir}"
"100999:100 ${optimizedDir}"
"100999:100 ${cfg.configDir}/config"
"100999:100 ${optimizedDir}/assets"
];
composeText = ''
services:
netbootxyz:
image: ghcr.io/netbootxyz/netbootxyz:${netbootxyzVersion}
container_name: netbootxyz
hostname: netbootxyz
user: '1000:1000'
networks:
netbootxyz:
ipv4_address: 10.89.170.253
ports:
- "${cfg.port}:3000/tcp"
- "69:69/udp"
- "8008:80/tcp"
volumes:
- ${cfg.configDir}/config:/config
- ${optimizedDir}/assets:/assets
environment:
- PUID=1000
- PGID=1000
- TZ=${time.timeZone}
- PORT_RANGE=30000:30010
- SUBFOLDER=/
security_opt:
- no-new-privileges:true
cap_drop:
- NET_RAW
restart: unless-stopped
networks:
netbootxyz:
driver: bridge
name: netbootxyz
ipam:
config:
- subnet: "10.89.170.0/24"
gateway: "10.89.170.254"
'';
}
@@ -0,0 +1,384 @@
{ config, pkgs, lib, ... }:
with lib;
let
# Version tagging
nextcloudVersion = "33.0.0";
redisVersion = "8.6-alpine";
databaseVersion = "11.8";
onlyofficeVersion = "9.2";
whiteboardVersion = "v1.5.6";
# Helper
helper = import ../service-helper.nix { inherit config pkgs lib; };
cfg = config.numbus-server.services.nextcloud;
# Container config
name = "nextcloud";
in
helper.mkPodmanService {
inherit name;
description = "Nextcloud, your own online office suite";
defaultPort = "1100";
middlewares = [
"nextcloudSecureHeaders"
];
secrets = [
"nextcloud/db_name"
"nextcloud/db_username"
"nextcloud/db_password"
"nextcloud/redis_password"
"nextcloud/onlyoffice_secret"
"nextcloud/whiteboard_secret"
];
dirPermissions = [
"100032:100 ${cfg.dataDir}"
"100032:100 ${cfg.configDir}"
"100032:100 ${cfg.configDir}/web"
"100999:100 ${cfg.configDir}/redis"
"100999:100 ${cfg.configDir}/database"
"1000:100 ${cfg.configDir}/onlyoffice"
"1000:100 ${cfg.configDir}/onlyoffice/log"
"1000:100 ${cfg.configDir}/onlyoffice/cache"
"1000:100 ${cfg.configDir}/onlyoffice/data"
"1000:100 ${cfg.configDir}/onlyoffice/database"
];
# Compose file good
composeText = ''
services:
nextcloud-server:
image: docker.io/library/nextcloud:${nextcloudVersion}
container_name: nextcloud-server
hostname: nextcloud-server
networks:
nextcloud:
ipv4_address: 10.89.160.253
ports:
- "${cfg.port}:80/tcp"
volumes:
- ${cfg.configDir}/web:/var/www/html
- ${cfg.dataDir}:/mnt/ncdata
environment:
MYSQL_HOST: nextcloud-database:3306
MYSQL_DATABASE: ${config.sops.placeholder."nextcloud/db_name"}
MYSQL_USER: ${config.sops.placeholder."nextcloud/db_username"}
MYSQL_PASSWORD: ${config.sops.placeholder."nextcloud/db_password"}
REDIS_HOST_PASSWORD: ${config.sops.placeholder."nextcloud/redis_password"}
REDIS_HOST: nextcloud-redis
NEXTCLOUD_TRUSTED_DOMAINS: ${cfg.subdomain}.${config.numbus-server.services.domain}
NEXTCLOUD_DATA_DIR: /mnt/ncdata
SMTP_SECURE: tls
SMTP_HOST: ${config.numbus-server.mail.smtpServer}
SMTP_PORT: ${toString config.numbus-server.mail.smtpPort}
SMTP_NAME: ${config.numbus-server.mail.smtpUsername}
SMTP_PASSWORD: ${config.sops.placeholder.smtpPassword}
MAIL_FROM_ADDRESS: no-reply
MAIL_DOMAIN: ${config.numbus-server.services.domain}
APACHE_DISABLE_REWRITE_IP: 1
OVERWRITEPROTOCOL: https
TRUSTED_PROXIES: 10.89.160.1
NC_default_phone_region: "${config.numbus-server.language}"
NC_default_language: "${config.numbus-server.language}"
NC_default_locale: "${config.numbus-server.locale}"
NC_default_timezone: "${config.time.timeZone}"
NC_maintenance_window_start: "1"
depends_on:
- nextcloud-database
security_opt:
- no-new-privileges:true
cap_drop:
- NET_RAW
restart: unless-stopped
nextcloud-redis:
image: docker.io/library/redis:${redisVersion}
container_name: nextcloud-redis
hostname: nextcloud-redis
user: '1000:1000'
networks:
nextcloud:
ipv4_address: 10.89.160.252
volumes:
- ${cfg.configDir}/redis:/data
command: redis-server --requirepass ${config.sops.placeholder."nextcloud/redis_password"} --save 60 1 --loglevel warning
security_opt:
- no-new-privileges:true
cap_drop:
- NET_RAW
restart: unless-stopped
nextcloud-database:
image: docker.io/library/mariadb:${databaseVersion}
container_name: nextcloud-database
hostname: nextcloud-database
user: '1000:1000'
networks:
nextcloud:
ipv4_address: 10.89.160.251
volumes:
- ${cfg.configDir}/database:/var/lib/mysql
environment:
MARIADB_DATABASE: ${config.sops.placeholder."nextcloud/db_name"}
MARIADB_USER: ${config.sops.placeholder."nextcloud/db_username"}
MARIADB_PASSWORD: ${config.sops.placeholder."nextcloud/db_password"}
MARIADB_RANDOM_ROOT_PASSWORD: true
security_opt:
- no-new-privileges:true
cap_drop:
- NET_RAW
command:
- "--transaction-isolation=READ-COMMITTED"
- "--binlog-format=ROW"
restart: unless-stopped
nextcloud-onlyoffice:
image: docker.io/onlyoffice/documentserver:${onlyofficeVersion}
container_name: nextcloud-onlyoffice
hostname: nextcloud-onlyoffice
networks:
nextcloud:
ipv4_address: 10.89.160.250
ports:
- "9980:80/tcp"
volumes:
- ${cfg.configDir}/onlyoffice/log:/var/log/onlyoffice
- ${cfg.configDir}/onlyoffice/cache:/var/lib/onlyoffice
- ${cfg.configDir}/onlyoffice/data:/var/www/onlyoffice/Data
- ${cfg.configDir}/onlyoffice/database:/var/lib/postgresql
environment:
- JWT_SECRET=${config.sops.placeholder."nextcloud/onlyoffice_secret"}
- REDIS_SERVER_PASS=${config.sops.placeholder."nextcloud/redis_password"}
- REDIS_SERVER_HOST=nextcloud-redis
- REDIS_SERVER_PORT=6379
- ADMINPANEL_ENABLED=false
- EXAMPLE_ENABLED=false
- METRICS_ENABLED=false
cap_drop:
- NET_RAW
restart: unless-stopped
nextcloud-whiteboard:
image: ghcr.io/nextcloud-releases/whiteboard:${whiteboardVersion}
container_name: nextcloud-whiteboard
hostname: nextcloud-whiteboard
user: '1000:1000'
ports:
- "3002:3002/tcp"
environment:
NEXTCLOUD_URL: https://${cfg.subdomain}.${config.numbus-server.services.domain}
JWT_SECRET_KEY: ${config.sops.placeholder."nextcloud/whiteboard_secret"}
security_opt:
- no-new-privileges:true
cap_drop:
- NET_RAW
restart: unless-stopped
networks:
nextcloud:
driver: bridge
name: nextcloud
ipam:
config:
- subnet: "10.89.160.0/24"
gateway: "10.89.160.254"
'';
extraOptions = {
onlyoffice = {
subdomain = mkOption {
type = types.str;
default = "onlyoffice";
example = "onlyoffice";
description = "The subdomain that onlyoffice for nextcloud will use";
};
};
whiteboard = {
subdomain = mkOption {
type = types.str;
default = "whiteboard";
example = "whiteboard";
description = "The subdomain that whiteboard for nextcloud will use";
};
};
};
extraConfig = {
sops.templates."traefik/rules/nextcloud-onlyoffice" = {
gid = "100";
uid = "100999";
mode = "0400";
content = ''
http:
routers:
nextcloud-onlyoffice:
rule: "Host(`${cfg.onlyoffice.subdomain}.${config.numbus-server.services.domain}`)"
entrypoints:
- "websecure"
service: nextcloud-onlyoffice
tls:
certresolver: "cloudflare"
options: "secureTLS"
services:
nextcloud-onlyoffice:
loadBalancer:
servers:
- url: "http://host.containers.internal:9980"
'';
path = "/etc/traefik/rules/nextcloud-onlyoffice.yaml";
};
sops.templates."traefik/rules/nextcloud-whiteboard" = {
gid = "100";
uid = "100999";
mode = "0400";
content = ''
http:
routers:
nextcloud-whiteboard:
rule: "Host(`${cfg.whiteboard.subdomain}.${config.numbus-server.services.domain}`)"
entrypoints:
- "websecure"
service: nextcloud-whiteboard
middlewares:
- "secureHeaders"
tls:
certresolver: "cloudflare"
options: "secureTLS"
services:
nextcloud-whiteboard:
loadBalancer:
servers:
- url: "http://host.containers.internal:3002"
'';
path = "/etc/traefik/rules/nextcloud-whiteboard.yaml";
};
sops.templates."traefik/rules/nextcloud-secureHeaders" = {
gid = "100";
uid = "100999";
mode = "0400";
content = ''
http:
middlewares:
nextcloudSecureHeaders:
headers:
FrameDeny: false
CustomFrameOptionsValue: "SAMEORIGIN"
AddVaryHeader: true
BrowserXssFilter: true
ContentTypeNosniff: true
ForceSTSHeader: true
STSSeconds: 315360000
STSIncludeSubdomains: true
STSPreload: true
AccessControlAllowMethods: "GET,OPTIONS,PUT"
AccessControlAllowOriginList:
- origin-list-or-null
AccessControlMaxAge: 100
ReferrerPolicy: same-origin
PermissionsPolicy: "vibrate=()"
ContentSecurityPolicy: >-
default-src https://${cfg.onlyoffice.subdomain}.${config.numbus-server.services.domain} 'self';
script-src https://${cfg.onlyoffice.subdomain}.${config.numbus-server.services.domain} 'self' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
connect-src 'self';
img-src 'self' data:;
font-src 'self' data:;
frame-src https://${cfg.onlyoffice.subdomain}.${config.numbus-server.services.domain} 'self';
frame-ancestors https://${cfg.onlyoffice.subdomain}.${config.numbus-server.services.domain} 'self';
object-src 'none';
base-uri 'self';
'';
path = "/etc/traefik/rules/nextcloud-secureHeaders";
};
systemd.services."${name}-quirk" = {
description = "Podman container quirk : ${name}";
wantedBy = [ "multi-user.target" ];
after = [ "${name}.service" "${name}-secrets.service" ];
onFailure = [ "service-failure-notify@%n.service" ];
startLimitBurst = 5;
startLimitIntervalSec = 600;
path = [ pkgs.coreutils pkgs.sudo pkgs.podman pkgs.systemd pkgs.gnugrep ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
script = ''
OCC="sudo -u numbus-admin podman exec --user www-data nextcloud-server php occ"
[[ ! -e /var/lib/numbus-server/${name}/.env ]] && systemctl start ${name}-secrets.service
until [[ -e /var/lib/numbus-server/${name}/.env ]]; do
echo "Waiting for secrets generation..."
sleep 5
done
source /var/lib/numbus-server/${name}/.env
until $OCC status | grep -iq "installed: true" >/dev/null 2>&1; do
echo "Waiting for Nextcloud to be up and running..."
sleep 60
done
$OCC db:add-missing-indices
$OCC maintenance:repair --include-expensive
INSTALL_APPS_LIST=( "calendar" "contacts" "mail" "notes" "onlyoffice" "cookbook" "whiteboard" )
DISABLE_APPS_LIST=( "activity" "federation" "webhook_listeners" "photos" "recommendations" "sharebymail" "teams" "support" "richdocumentscode" )
for app in ''${INSTALL_APPS_LIST[@]}; do
if ! $OCC --no-warnings app:list | grep -iq "$app:"; then
$OCC --no-warnings app:install "$app"
fi
if $OCC --no-warnings app:list --disabled | grep -iq "$app:"; then
$OCC --no-warnings app:enable "$app"
fi
done
for app in ''${DISABLE_APPS_LIST[@]}; do
if $OCC --no-warnings app:list --enabled | grep -iq "$app:"; then
$OCC --no-warnings app:disable "$app"
fi
done
$OCC --no-warnings config:system:set onlyoffice DocumentServerInternalUrl --value="https://${cfg.onlyoffice.subdomain}.${config.numbus-server.services.domain}/"
$OCC --no-warnings config:system:set onlyoffice DocumentServerUrl --value="https://${cfg.onlyoffice.subdomain}.${config.numbus-server.services.domain}/"
$OCC --no-warnings config:system:set onlyoffice jwt_secret --value="$ONLYOFFICE_PASSWORD"
$OCC --no-warnings config:app:set whiteboard collabBackendUrl --value="https://${cfg.whiteboard.subdomain}.${config.numbus-server.services.domain}"
$OCC --no-warnings config:app:set whiteboard jwt_secret_key --value="$WHITEBOARD_PASSWORD"
if [[ ! -f /var/lib/numbus-server/${name}/croned.true ]]; then
$OCC background:cron
sudo -u numbus-admin podman exec --user www-data nextcloud-server php -f /var/www/html/cron.php
touch /var/lib/numbus-server/${name}/croned.true
fi
if [[ ! -f /var/lib/numbus-server/${name}/scanned.true ]]; then
$OCC files:scan --all
$OCC files:repair-tree
touch /var/lib/numbus-server/${name}/scanned.true
fi
'';
};
systemd.services."${name}-cron" = {
description = "Podman container crontab : ${name}";
after = [ "${name}.service" "${name}-quirk.service" ];
onFailure = [ "service-failure-notify@%n.service" ];
path = [ pkgs.sudo pkgs.podman ];
serviceConfig = {
Type = "oneshot";
ExecCondition = ''${pkgs.sudo}/bin/sudo -u numbus-admin podman exec --user www-data nextcloud-server php occ status'';
ExecStart = "${pkgs.sudo}/bin/sudo -u numbus-admin podman exec --user www-data nextcloud-server php -f /var/www/html/cron.php";
};
};
systemd.timers."${name}-cron" = {
description = "Timer for Nextcloud cron";
wantedBy = [ "timers.target" ];
timerConfig = {
OnBootSec = "5m";
OnUnitActiveSec = "5m";
Unit = "${name}-cron.service";
};
};
};
}
@@ -0,0 +1,62 @@
{ config, pkgs, lib, ... }:
with lib;
let
# Container config
name = "ntfy";
# Version tagging
ntfyVersion = "v2.18.0";
# Helper
helper = import ../service-helper.nix { inherit config pkgs lib; };
cfg = config.numbus-server.services.ntfy;
in
helper.mkPodmanService {
inherit name;
description = "Ntfy, get notified easily";
defaultPort = "8099";
middlewares = [
"secureHeaders"
];
dirPermissions = [
"100999:100 ${cfg.configDir}"
"100999:100 ${cfg.configDir}/cache"
"100999:100 ${cfg.configDir}/config"
];
composeText = ''
services:
ntfy:
image: docker.io/binwiederhier/ntfy
container_name: ntfy
hostname: ntfy
user: "1000:1000"
networks:
ntfy:
ipv4_address: 10.89.150.253
ports:
- "${cfg.port}:80/tcp"
command:
- serve
volumes:
- ${cfg.config}/cache:/var/cache/ntfy
- ${cfg.config}/config:/etc/ntfy
environment:
- TZ=${time.timeZone}
security_opt:
- no-new-privileges:true
cap_drop:
- NET_RAW
restart: unless-stopped
networks:
ntfy:
driver: bridge
name: ntfy
ipam:
config:
- subnet: "10.89.150.0/24"
gateway: "10.89.150.254"
'';
}
@@ -0,0 +1,118 @@
{ config, pkgs, lib, ... }:
with lib;
let
# Container config
name = "odoo";
# Version tagging
odooVersion = "10.11.6";
databaseVersion = "15.17";
# Storage optimization
spindown = config.numbus-server.hardware.HddSpindown;
optimizedDir = if spindown.enable && (spindown.optimize == "compatible" || (isList spindown.optimize && elem name spindown.optimize))
then cfg.configDir
else cfg.dataDir;
# Helper
helper = import ../service-helper.nix { inherit config pkgs lib; };
cfg = config.numbus-server.services.odoo;
in
helper.mkPodmanService {
inherit name;
description = "Odoo : An open ERP (Enterprise resource planning) solution";
defaultPort = "8069";
configDirEnabled = optimizedDir == cfg.configDir;
dataDirEnabled = optimizedDir == cfg.dataDir;
middlewares = [
"secureHeaders"
];
dirPermissions = [
"100999:100 ${optimizedDir}"
"100999:100 ${optimizedDir}/odoo"
"100999:100 ${cfg.configDir}/addons"
"100999:100 ${cfg.configDir}/config"
"100999:100 ${cfg.configDir}/database"
];
composeText = ''
services:
odoo-database:
image: docker.io/library/postgres:${databaseVersion}
container_name: odoo-database
hostname: odoo-database
user: '1000:1000'
shm_size: 128mb
networks:
odoo:
ipv4_address: 10.89.190.253
volumes:
- ${cfg.configDir}/database:/var/lib/postgresql/data
environment:
- POSTGRES_DB=${config.sops.placeholder."odoo/db_name"}
- POSTGRES_PASSWORD=${config.sops.placeholder."odoo/db_password"}
- POSTGRES_USER=${config.sops.placeholder."odoo/db_username"}
- PGDATA=/var/lib/postgresql/data
cap_drop:
- NET_RAW
security_opt:
- no-new-privileges:true
restart: unless-stopped
odoo-server:
image: docker.io/library/odoo:${odooVersion}
container_name: odoo-server
hostname: odoo-server
user: '1000:1000'
networks:
odoo:
ipv4_address: 10.89.190.252
ports:
- "${cfg.port}:8069/tcp"
volumes:
- ${optimizedDir}/odoo:/var/lib/odoo
- ${cfg.configDir}/config:/etc/odoo
- ${cfg.configDir}/addons:/mnt/extra-addons
environment:
- HOST=odoo-database
- USER=${config.sops.placeholder."odoo/db_username"}
- PASSWORD=${config.sops.placeholder."odoo/db_password"}
depends_on:
- odoo-database
cap_drop:
- NET_RAW
security_opt:
- no-new-privileges:true
restart: unless-stopped
networks:
odoo:
driver: bridge
name: odoo
ipam:
config:
- subnet: "10.89.190.0/24"
gateway: "10.89.190.254"
'';
extraConfig = {
sops.secrets."odoo/db_name" = {
sopsFile = /etc/nixos/secrets/podman/odoo.yaml;
gid = "100";
uid = "1000";
mode = "0400";
};
sops.secrets."odoo/db_username" = {
sopsFile = /etc/nixos/secrets/podman/odoo.yaml;
gid = "100";
uid = "1000";
mode = "0400";
};
sops.secrets."odoo/db_password" = {
sopsFile = /etc/nixos/secrets/podman/odoo.yaml;
gid = "100";
uid = "1000";
mode = "0400";
};
};
}
@@ -0,0 +1,112 @@
{ config, pkgs, lib, ... }:
with lib;
let
# Container config
name = "passbolt";
# Version tagging
passboltVersion = "5.9.0-1-ce-non-root";
databaseVersion = "12.2";
# Storage optimization
spindown = config.numbus-server.hardware.HddSpindown;
optimizedDir = if spindown.enable && (spindown.optimize == "compatible" || (isList spindown.optimize && elem name spindown.optimize))
then cfg.configDir
else cfg.dataDir;
# Helper
helper = import ../service-helper.nix { inherit config pkgs lib; };
cfg = config.numbus-server.services.passbolt;
in
helper.mkPodmanService {
inherit name;
description = "Passbolt, your password manager";
defaultPort = "4433";
scheme = "https";
dataDirEnabled = false;
middlewares = [ "secureHeaders" ];
dirPermissions = [
"100032:100 ${cfg.configDir}"
"100032:100 ${cfg.configDir}/gpg"
"100032:100 ${cfg.configDir}/jwt"
"100999:100 ${cfg.configDir}/database"
];
secrets = [
"passbolt/db_name"
"passbolt/db_username"
"passbolt/db_password"
];
# Compose file good
composeText = ''
services:
passbolt-server:
image: docker.io/passbolt/passbolt:${passboltVersion}
container_name: passbolt-server
hostname: passbolt-server
user: '33:33'
networks:
passbolt:
ports:
- "${cfg.port}:4433/tcp"
volumes:
- ${cfg.configDir}/gpg:/etc/passbolt/gpg
- ${cfg.configDir}/jwt:/etc/passbolt/jwt
environment:
APP_DEFAULT_TIMEZONE: ${config.time.timeZone}
APP_FULL_BASE_URL: https://${cfg.subdomain}.${config.numbus-server.services.domain}
DATASOURCES_DEFAULT_HOST: "passbolt-database"
DATASOURCES_DEFAULT_USERNAME: ${config.sops.placeholder."passbolt/db_username"}
DATASOURCES_DEFAULT_PASSWORD: ${config.sops.placeholder."passbolt/db_password"}
DATASOURCES_DEFAULT_DATABASE: ${config.sops.placeholder."passbolt/db_name"}
EMAIL_DEFAULT_FROM_NAME: "Passbolt"
EMAIL_TRANSPORT_DEFAULT_HOST: ${config.numbus-server.mail.smtpServer}
EMAIL_TRANSPORT_DEFAULT_PORT: ${toString config.numbus-server.mail.smtpPort}
EMAIL_TRANSPORT_DEFAULT_USERNAME: ${config.numbus-server.mail.smtpUsername}
EMAIL_TRANSPORT_DEFAULT_PASSWORD: ${config.sops.placeholder."mail/smtpPassword"}
EMAIL_TRANSPORT_DEFAULT_TLS: true
EMAIL_DEFAULT_FROM: passbolt-noreply@${config.numbus-server.services.domain}
PASSBOLT_SSL_FORCE: true
command:
[
"/usr/bin/wait-for.sh",
"-t",
"0",
"passbolt-database:3306",
"--",
"/docker-entrypoint.sh"
]
depends_on:
- passbolt-database
security_opt:
- no-new-privileges:true
cap_drop:
- NET_RAW
restart: unless-stopped
passbolt-database:
image: docker.io/library/mariadb:${databaseVersion}
container_name: passbolt-database
hostname: passbolt-database
user: '1000:1000'
networks:
passbolt:
volumes:
- ${cfg.configDir}/database:/var/lib/mysql
environment:
MYSQL_RANDOM_ROOT_PASSWORD: "true"
MYSQL_DATABASE: ${config.sops."passbolt/db_name"}
MYSQL_USER: ${config.sops."passbolt/db_username"}
MYSQL_PASSWORD: ${config.sops."passbolt/db_password"}
security_opt:
- no-new-privileges:true
cap_drop:
- NET_RAW
restart: unless-stopped
networks:
passbolt:
name: passbolt
driver: bridge
'';
}
@@ -0,0 +1,178 @@
{ config, pkgs, lib, ... }:
with lib;
let
# Container config
name = "traefik";
# Version tagging
traefikVersion = "v3.6.8";
# Helper
helper = import ../service-helper.nix { inherit config pkgs lib; };
cfg = config.numbus-server.services.traefik;
in
helper.mkPodmanService {
inherit name;
description = "Traefik reverse proxy, one to rule them all";
defaultPort = "7780";
pod = "false";
startDelay = 10;
dataDirEnabled = false;
middlewares = [
"secureHeaders"
];
dependencies = [
"sops-install-secrets.service"
"network-online.target"
];
dirPermissions = [
"100999:100 ${cfg.configDir}"
"100999:100 ${cfg.configDir}/certs"
];
# Compose file good
composeText = ''
services:
traefik:
image: docker.io/library/traefik:${traefikVersion}
container_name: traefik
hostname: traefik
user: '1000:1000'
network_mode: pasta
ports:
- "${cfg.port}:8080/tcp"
- "443:443/tcp"
volumes:
- ${config.sops.templates."traefik/config".path}:/etc/traefik/traefik.yaml:ro
- ${cfg.configDir}/certs:/var/traefik/certs
- /etc/traefik/rules:/etc/traefik/rules:ro
environment:
- CF_DNS_API_TOKEN=${config.sops.placeholder."traefik/cloudflare_api_token"}
cap_add:
- NET_BIND_SERVICE
security_opt:
- no-new-privileges:true
restart: unless-stopped
'';
extraConfig = {
sops.secrets."traefik/cloudflare_api_token" = {
sopsFile = /etc/nixos/secrets/podman/traefik.yaml;
gid = "100";
uid = "1000";
mode = "0400";
};
sops.templates."traefik/config"= {
gid = "100";
uid = "100999";
mode = "0400";
content = ''
global:
checkNewVersion: false
sendAnonymousUsage: false
log:
level: ${cfg.logLevel}
accesslog: {}
api:
dashboard: true
insecure: false
entryPoints:
web:
address: :80
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: :443
forwardedHeaders:
trustedIPs:
- "127.0.0.1/32"
- "10.0.0.0/8"
- "192.168.0.0/16"
- "172.16.0.0/12"
certificatesResolvers:
cloudflare:
acme:
email: ${config.numbus-server.mail.adminAddress}
storage: /var/traefik/certs/cloudflare-acme.json
caServer: "https://acme-v02.api.letsencrypt.org/directory"
dnsChallenge:
provider: cloudflare
resolvers:
- "1.1.1.1:53"
- "9.9.9.9:53"
serversTransport:
insecureSkipVerify: true
providers:
file:
directory: "/etc/traefik/rules"
watch: true
'';
path = "/etc/traefik/traefik.yaml";
};
sops.templates."traefik/rules/secureHeaders" = {
gid = "100";
uid = "100999";
mode = "0400";
content = ''
http:
middlewares:
secureHeaders:
headers:
FrameDeny: true
AccessControlAllowMethods: 'GET,OPTIONS,PUT'
AccessControlAllowOriginList:
- origin-list-or-null
AccessControlMaxAge: 100
AddVaryHeader: true
BrowserXssFilter: true
ContentTypeNosniff: true
ForceSTSHeader: true
STSIncludeSubdomains: true
STSPreload: true
ContentSecurityPolicy: default-src 'self' 'unsafe-inline'
CustomFrameOptionsValue: SAMEORIGIN
ReferrerPolicy: same-origin
PermissionsPolicy: vibrate 'self'
STSSeconds: 315360000
'';
path = "/etc/traefik/rules/secureHeaders.yaml";
};
sops.templates."traefik/rules/secureTLS" = {
gid = "100";
uid = "100999";
mode = "0400";
content = ''
tls:
options:
secureTLS:
minVersion: VersionTLS12
sniStrict: true
curvePreferences:
- CurveP521
- CurveP384
cipherSuites:
- TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
- TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
- TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256
'';
path = "/etc/traefik/rules/secureTLS.yaml";
};
};
extraOptions = {
enable.default = true;
logLevel = mkOption {
type = types.enum [ "TRACE" "DEBUG" "INFO" "WARN" "ERROR" "FATAL" ];
default = "ERROR";
example = "ERROR";
description = "The level of detail Traefik should print in the logs.";
};
};
}
@@ -0,0 +1,54 @@
{ config, pkgs, lib, ... }:
with lib;
let
# Container config
name = "uptimeKuma";
# Version tagging
uptimeKumaVersion = "2.2.0-rootless";
# Storage optimization
spindown = config.numbus-server.hardware.HddSpindown;
optimizedDir = if spindown.enable && (spindown.optimize == "compatible" || (isList spindown.optimize && elem name spindown.optimize))
then cfg.configDir
else cfg.dataDir;
# Helper
helper = import ../service-helper.nix { inherit config pkgs lib; };
cfg = config.numbus-server.services.uptimeKuma;
in
helper.mkPodmanService {
inherit name;
description = "Uptime-Kuma, \"don't let your server down !\" monitoring tools";
defaultPort = "3001";
scheme = "http";
middlewares = [ "secureHeaders" ];
dirPermissions = [ "100999:100 ${optimizedDir}" ];
composeText = ''
services:
uptimekuma:
image: docker.io/louislam/uptime-kuma:${uptimeKumaVersion}
container_name: uptime-kuma
hostname: uptime-kuma
user: '1000:1000'
networks:
uptime-kuma:
ipv4_address: 10.89.100.253
ports:
- "${cfg.port}:3001/tcp"
volumes:
- ${optimizedDir}:/app/data
security_opt:
- no-new-privileges:true
restart: unless-stopped
networks:
uptime-kuma:
driver: bridge
ipam:
config:
- subnet: "10.89.100.0/24"
gateway: "10.89.100.254"
'';
}
@@ -0,0 +1,81 @@
{ config, pkgs, lib, ... }:
with lib;
let
# Container config
name = "vscodium";
# Version tagging
vscodiumVersion = "1.110.11607-ls15";
# Storage optimization
spindown = config.numbus-server.hardware.HddSpindown;
optimizedDir = if spindown.enable && (spindown.optimize == "compatible" || (isList spindown.optimize && elem name spindown.optimize))
then cfg.configDir
else cfg.dataDir;
# Helper
helper = import ../service-helper.nix { inherit config pkgs lib; };
cfg = config.numbus-server.services.vscodium;
in
helper.mkPodmanService {
inherit name;
description = "VScodium, an open-source version of VScode in your web browser";
defaultPort = "8000";
configDirEnabled = optimizedDir == cfg.configDir;
dataDirEnabled = optimizedDir == cfg.dataDir;
middlewares = [ "secureHeaders" ];
dirPermissions = [
"100999:100 ${optimizedDir}"
"100999:100 ${cfg.configDir}"
"100999:100 ${optimizedDir}/workspace"
"100999:100 ${cfg.configDir}/config"
];
composeText = ''
services:
vscodium:
image: lscr.io/linuxserver/vscodium-web:${vscodiumVersion}
container_name: vscodium
hostname: vscodium
user: '1000:1000'
networks:
vscodium:
ipv4_address: 10.89.50.253
ports:
- "${defaultPort}:8000"
volumes:
- ${cfg.configDir}/config:/config
- ${optimizedDir}/workspace:/workspace
environment:
- PUID=1000
- PGID=1000
- TZ=${time.timeZone}
- CONNECTION_TOKEN=${config.sops.placeholder."vscodium/connection_token"}
shm_size: "1gb"
cap_add:
- IPC_LOCK
cap_drop:
- NET_RAW
security_opt:
- no-new-privileges:true
restart: unless-stopped
networks:
vscodium:
name: vscodium
driver: bridge
ipam:
config:
- subnet: "10.89.50.0/24"
gateway: "10.89.50.254"
'';
extraConfig = {
sops.secrets."vscodium/connection_token" = {
sopsFile = /etc/nixos/secrets/podman/vscodium.yaml;
gid = "100";
uid = "1000";
mode = "0400";
};
};
}
+53
View File
@@ -0,0 +1,53 @@
{ config, pkgs, lib, ... }:
with lib;
let
# Version tagging
adguardVersion = "latest";
# Helper
helper = import ../service-helper.nix { inherit config pkgs lib; };
cfg = config.numbus-server.services.adguard;
# Container config
name = "adguard";
in
helper.mkPodmanService {
inherit name;
description = "AdGuard, feature-rich DNS service";
defaultPort = "3000";
scheme = "http";
dataDirEnabled = false;
startDelay = 10;
dependencies = [
"network.target"
];
middlewares = [
"secureHeaders"
];
dirPermissions = [
"100999:100 ${cfg.configDir}"
];
composeText = ''
services:
adguardhome:
image: adguard/adguardhome:${adguardVersion}
container_name: adguard
hostname: adguard
network_mode: pasta
user: '1000:1000'
ports:
- "${cfg.port}:3000/tcp"
- "53:53/tcp"
- "53:53/udp"
volumes:
- ${cfg.configDir}/work:/opt/adguardhome/work
- ${cfg.configDir}/config:/opt/adguardhome/conf
cap_add:
- SYS_NICE
security_opt:
- no-new-privileges:true
restart: unless-stopped
'';
}
+10
View File
@@ -0,0 +1,10 @@
{ ... }:
{
imports = [
# To test
./adguard.nix
# Tested
./pi-hole.nix
];
}
+71
View File
@@ -0,0 +1,71 @@
{ config, pkgs, lib, ... }:
with lib;
let
# Version tagging
piholeVersion = "2026.02.0";
# Helper
helper = import ../service-helper.nix { inherit config pkgs lib; };
cfg = config.numbus-server.services.pi-hole;
# Container config
name = "pi-hole";
# DNS config
dnsConfig = ''
'';
in
helper.mkPodmanService {
inherit name;
description = "Pi-Hole, the ads black hole";
defaultPort = "4443";
scheme = "https";
dataDirEnabled = false;
startDelay = 10;
dependencies = [
"network.target"
];
middlewares = [
"secureHeaders"
];
dirPermissions = [
"100999:100 ${cfg.configDir}"
];
secrets = [
"pi-hole/web_password"
];
# Compose file good
composeText = ''
services:
pi-hole:
image: docker.io/pihole/pihole:${piholeVersion}
container_name: pi-hole
hostname: pi-hole
network_mode: pasta
ports:
- "${cfg.port}:443/tcp"
- "53:53/tcp"
- "53:53/udp"
volumes:
- ${cfg.configDir}:/etc/pihole
environment:
PIHOLE_UID: '1000'
PIHOLE_GID: '1000'
TZ: ${config.time.timeZone}
FTLCONF_webserver_domain: ${cfg.subdomain}.${config.numbus-server.services.domain}
FTLCONF_dns_domain_name: "${config.numbus-server.services.domain}"
FTLCONF_webserver_api_password: ${config.sops.placeholder."pi-hole/web_password"}
FTLCONF_dns_upstreams: 9.9.9.9;149.112.112.112
FTLCONF_dns_listeningMode: "BIND"
FTLCONF_dns_domain_local: "true"
FTLCONF_dhcp_active: "false"
FTLCONF_ntp_ipv4_active: "false"
FTLCONF_ntp_ipv6_active: "false"
FTLCONF_ntp_sync_active: "false"
cap_add:
- SYS_NICE
restart: unless-stopped
'';
}
+91
View File
@@ -0,0 +1,91 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.numbus-server.services.clamav;
onAccessPaths = lib.mapAttrsToList (n: v: v.dataDir) (lib.filterAttrs (n: v:
v ? enable && v.enable && v ? dataDir && v.dataDir != null && v.dataDir != false
) config.numbus-server.services);
clamonacc_virus_notifier = pkgs.writeScript "clamonacc_virus_notifier.sh" ''
#!${pkgs.bash}/bin/bash
echo "CLAM_VIRUSEVENT_VIRUSNAME=\"$CLAM_VIRUSEVENT_VIRUSNAME\"" > /var/lib/clamav/virus_event.env
echo "CLAM_VIRUSEVENT_FILENAME=\"$CLAM_VIRUSEVENT_FILENAME\"" >> /var/lib/clamav/virus_event.env
/run/wrappers/bin/sudo /run/current-system/sw/bin/systemctl start clamav-virus-notify.service
'';
in
{
options.numbus-server.services.clamav = {
enable = mkEnableOption "ClamAV open-source anti-virus software";
};
config = mkIf cfg.enable {
environment.systemPackages = [ pkgs.clamav pkgs.curl ];
system.activationScripts.clamav-quarantine = ''
mkdir -p /quarantine
chown clamav:clamav /quarantine
chmod 440 /quarantine
'';
security.sudo.extraRules = [{
users = [ "clamav" ];
commands = [{
command = "/run/current-system/sw/bin/systemctl start clamav-virus-notify.service";
options = [ "NOPASSWD" ];
}];
}];
services.clamav = {
updater.enable = true;
clamonacc.enable = true;
scanner = {
enable = true;
interval = "*-*-* 04:00:00"; # Everyday at 4am
scanDirectories = [
"/etc"
"/home"
"/var/lib"
"/var/tmp"
"/tmp"
];
};
daemon = {
enable = true;
settings = {
OnAccessPrevention = true;
OnAccessIncludePath = onAccessPaths;
VirusEvent = "${clamonacc_virus_notifier}";
};
};
};
systemd.services.clamav-periodic-scan = mkIf (onAccessPaths != []) {
description = "Periodic ClamAV virus scan";
after = [ "clamav-daemon.service" "clamav-freshclam.service" ];
requires = [ "clamav-daemon.service" ];
wants = [ "clamav-freshclam.service" ];
onFailure = [ "clamav-virus-notify.service" ];
serviceConfig = {
Type = "oneshot";
ExecStart = "${pkgs.clamav}/bin/clamdscan --multiscan --fdpass --infected --allmatch --move=/quarantine ${lib.escapeShellArgs onAccessPaths}";
Slice = "system-clamav.slice";
};
};
systemd.timers.clamav-periodic-scan = mkIf (onAccessPaths != []) {
description = "Timer for ClamAV periodic scan";
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = "*-1/3-01 04:00:00";
Persistent = true;
Unit = "clamav-periodic-scan.service";
};
};
};
}
@@ -0,0 +1,11 @@
{ ... }:
{
imports = [
# To add
# ./backup-client.nix
# To test
./clamav.nix
./virtualization.nix
];
}
@@ -0,0 +1,17 @@
{ config, lib, ... }:
with lib;
let
cfg = config.numbus-server.services.virtualization;
in
{
options.numbus-server.services.virtualization = {
enable = mkEnableOption "QEMU/KVM virtualization software";
};
config = mkIf cfg.enable {
virtualisation.libvirtd.enable = true;
};
}