Compare commits

..

No commits in common. "cef3a949feae5ef6e87dee00cf44def0b2dd2ed7" and "2c195bf8077c90b2330ba6d43e3de1b5048bfa92" have entirely different histories.

35 changed files with 1129 additions and 301 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
.idea

10
.sops.yaml Normal file
View file

@ -0,0 +1,10 @@
keys:
- &tdpeuter_Tibo-NixFatDesk age1fva6s64s884z0q2w7de024sp69ucvqu0pg9shrhhqsn3ewlpjfpsh6md7y
- &tdpeuter_Tibo-NixTop age1qzutny0mqpcccqw6myyfntu6wcskruu9ghzvt6r4te7afkqwnguq05ex37
creation_rules:
- path_regex: secrets/[^/]+\.(yaml|json|env|ini)$
key_groups:
- age:
- *tdpeuter_Tibo-NixFatDesk
- *tdpeuter_Tibo-NixTop

39
flake.lock generated
View file

@ -5,11 +5,11 @@
"systems": "systems" "systems": "systems"
}, },
"locked": { "locked": {
"lastModified": 1726560853, "lastModified": 1731533236,
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -20,11 +20,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1730785428, "lastModified": 1759381078,
"narHash": "sha256-Zwl8YgTVJTEum+L+0zVAWvXAGbWAuXHax3KzuejaDyo=", "narHash": "sha256-gTrEEp5gEspIcCOx9PD8kMaF1iEmfBcTbO0Jag2QhQs=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "4aa36568d413aca0ea84a1684d2d46f55dbabad7", "rev": "7df7ff7d8e00218376575f0acdcc5d66741351ee",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -37,9 +37,30 @@
"inputs": { "inputs": {
"flake-utils": "flake-utils", "flake-utils": "flake-utils",
"nixpkgs": "nixpkgs", "nixpkgs": "nixpkgs",
"sops-nix": "sops-nix",
"utils": "utils" "utils": "utils"
} }
}, },
"sops-nix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1759188042,
"narHash": "sha256-f9QC2KKiNReZDG2yyKAtDZh0rSK2Xp1wkPzKbHeQVRU=",
"owner": "Mic92",
"repo": "sops-nix",
"rev": "9fcfabe085281dd793589bdc770a2e577a3caa5d",
"type": "github"
},
"original": {
"owner": "Mic92",
"repo": "sops-nix",
"type": "github"
}
},
"systems": { "systems": {
"locked": { "locked": {
"lastModified": 1681028828, "lastModified": 1681028828,
@ -62,11 +83,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1722363685, "lastModified": 1738591040,
"narHash": "sha256-XCf2PIAT6lH7BwytgioPmVf/wkzXjSKScC4KzcZgb64=", "narHash": "sha256-4WNeriUToshQ/L5J+dTSWC5OJIwT39SEP7V7oylndi8=",
"owner": "gytis-ivaskevicius", "owner": "gytis-ivaskevicius",
"repo": "flake-utils-plus", "repo": "flake-utils-plus",
"rev": "6b10f51ff73a66bb29f3bc8151a59d217713f496", "rev": "afcb15b845e74ac5e998358709b2b5fe42a948d1",
"type": "github" "type": "github"
}, },
"original": { "original": {

View file

@ -5,6 +5,10 @@
nixpkgs.url = "nixpkgs/nixos-unstable"; nixpkgs.url = "nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils"; flake-utils.url = "github:numtide/flake-utils";
sops-nix = {
url = "github:Mic92/sops-nix";
inputs.nixpkgs.follows = "nixpkgs";
};
utils = { utils = {
url = "github:gytis-ivaskevicius/flake-utils-plus"; url = "github:gytis-ivaskevicius/flake-utils-plus";
inputs.flake-utils.follows = "flake-utils"; inputs.flake-utils.follows = "flake-utils";
@ -13,11 +17,11 @@
outputs = inputs@{ outputs = inputs@{
self, nixpkgs, self, nixpkgs,
flake-utils, utils, flake-utils, sops-nix, utils,
... ...
}: }:
let let
system = "x86_64-linux"; system = utils.lib.system.x86_64-linux;
in in
utils.lib.mkFlake { utils.lib.mkFlake {
inherit self inputs; inherit self inputs;
@ -28,20 +32,30 @@
modules = [ modules = [
./modules ./modules
./users ./users
sops-nix.nixosModules.sops
]; ];
}; };
hosts = { hosts = {
# Physical hosts
Niko.modules = [ ./hosts/Niko ]; Niko.modules = [ ./hosts/Niko ];
# Virtual machines
# Single-service
Ingress.modules = [ ./hosts/Ingress ]; Ingress.modules = [ ./hosts/Ingress ];
Gitea.modules = [ ./hosts/Gitea ]; Gitea.modules = [ ./hosts/Gitea ];
Vaultwarden.modules = [ ./hosts/Vaultwarden ]; Vaultwarden.modules = [ ./hosts/Vaultwarden ];
# Production multi-service
Binnenpost.modules = [ ./hosts/Binnenpost ]; Binnenpost.modules = [ ./hosts/Binnenpost ];
Production.modules = [ ./hosts/Production ];
ProductionGPU.modules = [ ./hosts/ProductionGPU ]; ProductionGPU.modules = [ ./hosts/ProductionGPU ];
ProductionArr.modules = [ ./hosts/ProductionArr ];
ACE.modules = [ ./hosts/ACE ]; ACE.modules = [ ./hosts/ACE ];
# Others
Template.modules = [ ./hosts/Template ]; Template.modules = [ ./hosts/Template ];
Development.modules = [ ./hosts/Development ]; Development.modules = [ ./hosts/Development ];
Testing.modules = [ ./hosts/Testing ]; Testing.modules = [ ./hosts/Testing ];

View file

@ -16,6 +16,7 @@
apps = { apps = {
speedtest.enable = true; speedtest.enable = true;
technitiumDNS.enable = true; technitiumDNS.enable = true;
traefik.enable = true;
}; };
virtualisation.guest.enable = true; virtualisation.guest.enable = true;
}; };
@ -76,6 +77,14 @@
}; };
}; };
virtualisation.oci-containers.containers.traefik.labels = {
"traefik.http.routers.roxanne.rule" = "Host(`roxanne.depeuter.dev`)";
"traefik.http.services.roxanne.loadbalancer.server.url" = "https://192.168.0.13:8006";
"traefik.http.routers.hugo.rule" = "Host(`hugo.depeuter.dev`)";
"traefik.http.services.hugo.loadbalancer.server.url" = "https://192.168.0.11:444";
};
system.stateVersion = "24.05"; system.stateVersion = "24.05";
}; };
} }

View file

@ -3,8 +3,10 @@
{ {
config = { config = {
homelab = { homelab = {
apps.arr = { apps = {
qbittorrent.enable = true; bind9.enable = true;
traefik.enable = true;
plex.enable = true;
}; };
virtualisation.guest.enable = true; virtualisation.guest.enable = true;
}; };

View file

@ -59,6 +59,7 @@ prefixLength = 24;
}; };
"cloud.depeuter.dev" = { }; "cloud.depeuter.dev" = { };
"git.depeuter.dev" = { }; "git.depeuter.dev" = { };
"home.depeuter.dev" = { };
"jelly.depeuter.dev" = { }; "jelly.depeuter.dev" = { };
"vault.depeuter.dev" = { }; "vault.depeuter.dev" = { };
}; };
@ -136,10 +137,27 @@ prefixLength = 24;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
''; '';
}; };
"calendar.depeuter.dev".locations."/".return = "301 https://cloud.depeuter.dev/apps/calendar"; "calendar.depeuter.dev" = {
useACMEHost = "depeuter.dev";
locations."/".return = "301 https://cloud.depeuter.dev/apps/calendar";
};
"tasks.depeuter.dev".locations."/".return = "301 https://cloud.depeuter.dev/apps/tasks"; "tasks.depeuter.dev".locations."/".return = "301 https://cloud.depeuter.dev/apps/tasks";
"notes.depeuter.dev".locations."/".return = "301 https://cloud.depeuter.dev/apps/notes"; "notes.depeuter.dev".locations."/".return = "301 https://cloud.depeuter.dev/apps/notes";
"home.depeuter.dev" = {
enableACME = true;
forceSSL = true;
locations."/" = {
proxyPass = "http://192.168.0.21:8123";
extraConfig = ''
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
'';
};
};
"jelly.depeuter.dev" = { "jelly.depeuter.dev" = {
enableACME = true; enableACME = true;
forceSSL = true; forceSSL = true;
@ -176,7 +194,7 @@ prefixLength = 24;
}; };
}; };
extraConfig = '' extraConfig = ''
client_max_body_size 20M; client_max_body_size 512M;
# Security / XSS Mitigation Headers # Security / XSS Mitigation Headers
# NOTE: X-Frame-Options may cause issues with the webOS app # NOTE: X-Frame-Options may cause issues with the webOS app
@ -206,7 +224,7 @@ prefixLength = 24;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 512M; client_max_body_size 10G;
keepalive_timeout 600s; keepalive_timeout 600s;
proxy_buffers 4 256k; # Number and size of buffers for reading response proxy_buffers 4 256k; # Number and size of buffers for reading response
proxy_buffer_size 256k; # Buffer for the first part of the response proxy_buffer_size 256k; # Buffer for the first part of the response
@ -220,10 +238,18 @@ prefixLength = 24;
enableACME = true; enableACME = true;
forceSSL = true; forceSSL = true;
locations = { locations = {
"/".proxyPass = "http://192.168.0.22:10102"; "/" = {
proxyPass = "http://192.168.0.22:10102";
proxyWebSockets = true;
};
"~ ^/admin".return = 403; "~ ^/admin".return = 403;
}; };
}; };
"rss.depeuter.dev" = {
enableACME = true;
forceSSL = true;
locations."/".proxyPass = "http://192.168.92:${toString config.homelab.apps.freshrss.port}";
};
}; };
}; };
}; };

View file

@ -7,7 +7,10 @@
]; ];
homelab = { homelab = {
apps.technitiumDNS.enable = true; apps = {
technitiumDNS.enable = true;
traefik.enable = true;
};
users.deploy.enable = true; users.deploy.enable = true;
}; };
@ -34,12 +37,11 @@
hardware = { hardware = {
enableRedistributableFirmware = true; enableRedistributableFirmware = true;
enableAllFirmware = true; enableAllFirmware = true;
pulseaudio.enable = true; graphics.enable = true;
opengl.enable = true;
}; };
# Select internationalisation properties. # Select internationalisation properties.
i18n.defaultLocale = "en_GB.utf8"; i18n.defaultLocale = "en_GB.UTF-8";
networking = { networking = {
hostName = "Niko"; hostName = "Niko";
@ -79,6 +81,8 @@
user = config.users.users.jellyfin-mpv-shim.name; user = config.users.users.jellyfin-mpv-shim.name;
}; };
pulseaudio.enable = true;
tailscale = { tailscale = {
enable = true; enable = true;
useRoutingFeatures = "server"; useRoutingFeatures = "server";
@ -94,8 +98,6 @@
# resolved.enable = true; # resolved.enable = true;
}; };
sound.enable = true;
# Define a user account. Don't forget to set a password with 'passwd'. # Define a user account. Don't forget to set a password with 'passwd'.
users.users.jellyfin-mpv-shim = { users.users.jellyfin-mpv-shim = {
description = "Jellyfin MPV Shim User"; description = "Jellyfin MPV Shim User";
@ -114,67 +116,4 @@
systemd.services."cage-tty1".serviceConfig.Restart = "always"; systemd.services."cage-tty1".serviceConfig.Restart = "always";
system.stateVersion = "24.05"; system.stateVersion = "24.05";
virtualisation = {
# Enable Android emulator
# waydroid.enable = true;
docker = {
enable = true;
autoPrune.enable = true;
};
oci-containers = {
backend = "docker";
containers = {
reverse-proxy = {
hostname = "traefik";
image = "traefik:v3.0";
cmd = [
"--api.insecure=true"
# Add Docker provider
"--providers.docker=true"
"--providers.docker.exposedByDefault=false"
# Add web entrypoint
"--entrypoints.web.address=:80/tcp"
"--entrypoints.web.http.redirections.entrypoint.to=websecure"
"--entrypoints.web.http.redirections.entrypoint.scheme=https"
# Add websecure entrypoint
"--entrypoints.websecure.address=:443/tcp"
"--entrypoints.websecure.http.tls=true"
"--entrypoints.websecure.http.tls.certResolver=letsencrypt"
"--entrypoints.websecure.http.tls.domains[0].main=depeuter.dev"
"--entrypoints.websecure.http.tls.domains[0].sans=*.depeuter.dev"
"--entrypoints.websecure.http.tls.domains[1].sans=*.niko.depeuter.dev"
# Certificates
"--certificatesresolvers.letsencrypt.acme.dnschallenge=true"
"--certificatesresolvers.letsencrypt.acme.dnschallenge.provider=cloudflare"
"--certificatesresolvers.letsencrypt.acme.email=tibo.depeuter@telenet.be"
"--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
];
ports = [
"80:80/tcp"
"443:443/tcp"
# "8080:8080/tcp" # The Web UI (enabled by --api.insecure=true)
];
environment = {
# TODO Hide this!
"CLOUDFLARE_DNS_API_TOKEN" = "6Vz64Op_a6Ls1ljGeBxFoOVfQ-yB-svRbf6OyPv2";
};
environmentFiles = [
];
volumes = [
"/var/run/docker.sock:/var/run/docker.sock:ro" # So that Traefik can listen to the Docker events
"letsencrypt:/letsencrypt"
];
labels = {
"traefik.enable" = "true";
"traefik.http.routers.traefik.rule" = "Host(`traefik.niko.depeuter.dev`)";
"traefik.http.services.traefik.loadbalancer.server.port" = "8080";
};
autoStart = true;
};
};
};
};
} }

View file

@ -0,0 +1,48 @@
{ config, pkgs, lib, system, ... }:
{
config = {
homelab = {
apps = {
calibre.enable = true;
traefik.enable = true;
};
virtualisation.guest.enable = true;
};
networking = {
hostId = "aaaa2100";
domain = "roxanne.depeuter.dev";
useDHCP = false;
enableIPv6 = true;
defaultGateway = {
address = "192.168.0.1";
interface = "ens18";
};
# Open ports in the firewall.
firewall = {
enable = true;
};
interfaces.ens18 = {
ipv4.addresses = [
{
address = "192.168.0.31";
prefixLength = 24;
}
];
};
nameservers = [
"1.1.1.1" # Cloudflare
"1.0.0.1" # Cloudflare
];
};
system.stateVersion = "24.05";
};
}

View file

@ -0,0 +1,48 @@
{ config, pkgs, lib, system, ... }:
{
config = {
homelab = {
apps = {
arr.enable = true;
traefik.enable = true;
};
virtualisation.guest.enable = true;
};
networking = {
hostId = "aaaa2300";
domain = "roxanne.depeuter.dev";
useDHCP = false;
enableIPv6 = true;
defaultGateway = {
address = "192.168.0.1";
interface = "ens18";
};
# Open ports in the firewall.
firewall = {
enable = true;
};
interfaces.ens18 = {
ipv4.addresses = [
{
address = "192.168.0.33";
prefixLength = 24;
}
];
};
nameservers = [
"1.1.1.1" # Cloudflare
"1.0.0.1" # Cloudflare
];
};
system.stateVersion = "24.05";
};
}

View file

@ -17,7 +17,7 @@
defaultGateway = { defaultGateway = {
address = "192.168.0.1"; address = "192.168.0.1";
interface = "enp6s18"; interface = "ens18";
}; };
# Open ports in the firewall. # Open ports in the firewall.
@ -25,7 +25,7 @@
enable = true; enable = true;
}; };
interfaces.enp6s18 = { interfaces.ens18 = {
ipv4.addresses = [ ipv4.addresses = [
{ {
address = "192.168.0.94"; address = "192.168.0.94";
@ -40,7 +40,7 @@
]; ];
}; };
system.stateVersion = "unstable"; system.stateVersion = "24.11";
### Nvidia GPU support ### ### Nvidia GPU support ###
@ -64,7 +64,7 @@
}; };
hardware = { hardware = {
opengl = { graphics = {
enable = true; enable = true;
# driSupport = true; # driSupport = true;
# driSupport32Bit = true; # driSupport32Bit = true;

View file

@ -3,10 +3,11 @@
{ {
config = { config = {
homelab = { homelab = {
virtualisation = { apps = {
containers.enable = true; freshrss.enable = true;
guest.enable = true; traefik.enable = true;
}; };
virtualisation.guest.enable = true;
}; };
networking = { networking = {

View file

@ -3,7 +3,11 @@
{ {
config = { config = {
homelab = { homelab = {
apps.vaultwarden.enable = true; apps.vaultwarden = {
enable = true;
domain = "https://vault.depeuter.dev";
name = "Hugo's Vault";
};
virtualisation.guest.enable = true; virtualisation.guest.enable = true;
}; };

View file

@ -4,61 +4,86 @@ let
cfg = config.homelab.apps.arr; cfg = config.homelab.apps.arr;
networkName = "arrStack"; networkName = "arrStack";
appNames = [ "bazarr" "lidarr" "prowlarr" "qbittorrent" "radarr" "sonarr" ]; proxyNet = config.homelab.apps.traefik.sharedNetworkName;
appNames = [ "bazarr" "prowlarr" "qbittorrent" "radarr" "sonarr" ];
inUse = builtins.any (app: cfg.${app}.enable) appNames; inUse = builtins.any (app: cfg.${app}.enable) appNames;
PGID = toString config.users.groups.media.gid; PGID = toString config.users.groups.media.gid;
UMASK = "002"; UMASK = "002";
in { in {
options.homelab.apps.arr = { options.homelab.apps.arr = let
mkAppOption = appName: {
enable = lib.mkEnableOption "${appName} using Docker";
exposePorts = lib.mkOption {
type = lib.types.bool;
description = "Expose ${appName} port";
default = cfg.exposePorts;
};
};
in {
enable = lib.mkEnableOption "Arr Stack using Docker"; enable = lib.mkEnableOption "Arr Stack using Docker";
exposePorts = lib.mkOption {
type = lib.types.bool;
description = "Expose all app ports";
# Only expose ports by default if Traefik is not in use.
default = ! config.homelab.apps.traefik.enable;
};
bazarr.enable = lib.mkEnableOption "Bazarr using Docker"; bazarr = mkAppOption "Bazarr";
lidarr.enable = lib.mkEnableOption "Lidarr using Docker"; prowlarr = mkAppOption "Prowlarr";
prowlarr.enable = lib.mkEnableOption "Prowlarr using Docker"; qbittorrent = mkAppOption "qBittorrent";
qbittorrent.enable = lib.mkEnableOption "qBittorrent using Docker"; radarr = mkAppOption "Radarr";
radarr.enable = lib.mkEnableOption "Radarr using Docker"; sonarr = mkAppOption "Sonarr";
sonarr.enable = lib.mkEnableOption "Sonarr using Docker";
}; };
config = { config = {
homelab = { homelab = {
users.media.enable = lib.mkIf inUse true; users = lib.mkIf inUse {
apps.enable = true;
media.enable = true;
};
# "Master switch": Enable all apps. # "Master switch": Enable all apps.
apps.arr = { apps.arr = lib.mkIf cfg.enable {
bazarr.enable = lib.mkIf cfg.enable true; bazarr.enable = true;
lidarr.enable = lib.mkIf cfg.enable true; prowlarr.enable = true;
prowlarr.enable = lib.mkIf cfg.enable true; qbittorrent.enable = true;
qbittorrent.enable = lib.mkIf cfg.enable true; radarr.enable = true;
radarr.enable = lib.mkIf cfg.enable true; sonarr.enable = true;
sonarr.enable = lib.mkIf cfg.enable true;
};
}; };
fileSystems = lib.mkIf inUse { fileSystems.media.video = {
"/srv/video" = { enable = true;
device = "192.168.0.11:/mnt/SMALL/MEDIA/VIDEO"; permissions = [ "read" "write" ];
};
virtualisation.containers.enable = lib.mkIf inUse true;
};
fileSystems = let
mkFileSystem = device: {
inherit device;
fsType = "nfs"; fsType = "nfs";
options = [ options = [
"rw" "rw"
"auto"
"nfsvers=4.2" "nfsvers=4.2"
"async" "soft" "rsize=1048576" "wsize=1048576"
"timeo=100" "retry=50" "actimeo=1800" "lookupcache=all" "hard"
"nosuid" "tcp" "timeo=600" "retrans=2"
"_netdev" "nosuid" "tcp"
]; ];
}; };
"/srv/qbittorrent" = { hugoBackup = "192.168.0.11:/mnt/BIG/BACKUP";
device = "192.168.0.11:/mnt/SMALL/CONFIG/QBITTORRENT/qBittorrent"; in lib.mkIf inUse {
fsType = "nfs"; "/srv/bazarr-backup" = lib.mkIf cfg.bazarr.enable (mkFileSystem "${hugoBackup}/BAZARR");
options = [ "/srv/prowlarr-backup" = lib.mkIf cfg.bazarr.enable (mkFileSystem "${hugoBackup}/PROWLARR");
"rw" "/srv/qbittorrent" = lib.mkIf cfg.qbittorrent.enable (mkFileSystem "192.168.0.11:/mnt/SMALL/CONFIG/QBITTORRENT");
"nfsvers=4.2" "/srv/radarr-backup" = lib.mkIf cfg.radarr.enable (mkFileSystem "${hugoBackup}/RADARR");
"async" "soft" "/srv/sonarr-backup" = lib.mkIf cfg.sonarr.enable (mkFileSystem "${hugoBackup}/SONARR");
"nosuid" "tcp" "/srv/torrent" = mkFileSystem "192.168.0.11:/mnt/SMALL/MEDIA/TORRENT";
];
};
}; };
# Make sure the Docker network exists. # Make sure the Docker network exists.
@ -66,7 +91,6 @@ in {
description = "Create Docker network for ${networkName}"; description = "Create Docker network for ${networkName}";
requiredBy = [ requiredBy = [
"docker-bazarr.service" "docker-bazarr.service"
"docker-lidarr.service"
"docker-prowlarr.service" "docker-prowlarr.service"
"docker-qbittorrent.service" "docker-qbittorrent.service"
"docker-radarr.service" "docker-radarr.service"
@ -84,109 +108,77 @@ in {
}; };
# Create a user for each app. # Create a user for each app.
users.users = { users.users = let
bazarr = lib.mkIf cfg.bazarr.enable { mkUser = uid: {
uid = lib.mkForce 3003; uid = lib.mkForce uid;
isSystemUser = true; isSystemUser = true;
group = config.users.groups.media.name; group = config.users.groups.media.name;
home = "/var/empty"; home = "/var/empty";
shell = null; shell = null;
}; };
lidarr = lib.mkIf cfg.lidarr.enable { in {
uid = lib.mkForce 3002; bazarr = lib.mkIf cfg.bazarr.enable (mkUser 3003);
isSystemUser = true; prowlarr = lib.mkIf cfg.prowlarr.enable (mkUser 3004);
group = config.users.groups.media.name; qbittorrent = lib.mkIf cfg.qbittorrent.enable (mkUser 3005) // {
home = "/var/empty"; extraGroups = [
shell = null; config.users.groups.apps.name
}; ];
prowlarr = lib.mkIf cfg.prowlarr.enable {
uid = lib.mkForce 3004;
isSystemUser = true;
group = config.users.groups.media.name;
home = "/var/empty";
shell = null;
};
qbittorrent = lib.mkIf cfg.qbittorrent.enable {
uid = lib.mkForce 3005;
isSystemUser = true;
group = config.users.groups.media.name;
home = "/var/empty";
shell = null;
};
radarr = lib.mkIf cfg.radarr.enable {
uid = lib.mkForce 3006;
isSystemUser = true;
group = config.users.groups.media.name;
home = "/var/empty";
shell = null;
};
sonarr = lib.mkIf cfg.sonarr.enable {
uid = lib.mkForce 3007;
isSystemUser = true;
group = config.users.groups.media.name;
home = "/var/empty";
shell = null;
}; };
radarr = lib.mkIf cfg.radarr.enable (mkUser 3006);
sonarr = lib.mkIf cfg.sonarr.enable (mkUser 3007);
}; };
virtualisation.oci-containers.containers = { virtualisation.oci-containers.containers = let
bazarr = lib.mkIf cfg.bazarr.enable { videoHostPath = config.homelab.fileSystems.media.video.hostPath;
in {
bazarr = let
port = 6767;
in lib.mkIf cfg.bazarr.enable {
hostname = "bazarr"; hostname = "bazarr";
image = "ghcr.io/hotio/bazarr:release-1.4.4"; image = "ghcr.io/hotio/bazarr:release-1.5.2";
autoStart = true; autoStart = true;
ports = [ ports = lib.mkIf cfg.bazarr.exposePorts [
"6767:6767/tcp" "${toString port}:${toString port}/tcp"
"6767:6767/udp" "${toString port}:${toString port}/udp"
]; ];
extraOptions = [ extraOptions = [
"--network=${networkName}" "--network=${networkName}"
"--network=${proxyNet}"
"--mount" ''type=volume,source=bazarr-backup,target=/backup,volume-driver=local,volume-opt=type=nfs,volume-opt=device=:/mnt/BIG/BACKUP/BAZARR,"volume-opt=o=addr=192.168.0.11,rw,nfsvers=4.2,async,nosuid"''
]; ];
environment = { environment = {
PUID = toString config.users.users.bazarr.uid; PUID = toString config.users.users.bazarr.uid;
inherit PGID UMASK; inherit PGID UMASK;
TZ = config.time.timeZone; TZ = config.time.timeZone;
WEBUI_PORTS = "6767/tcp,6767/udp"; WEBUI_PORTS = "${toString port}/tcp,${toString port}/udp";
}; };
volumes = [ volumes = [
"bazarr-config:/config" "bazarr-config:/config"
"/srv/video:/data"
"/srv/bazarr-backup:/config/backup"
"${videoHostPath}/Films:/media/movies"
"${videoHostPath}/Series:/media/series"
]; ];
labels = {
"traefik.enable" = "true";
"traefik.docker.network" = proxyNet;
"traefik.http.routers.bazarr.rule" = "Host(`bazarr.depeuter.dev`)";
"traefik.http.services.bazarr.loadbalancer.server.port" = toString port;
};
}; };
lidarr = lib.mkIf cfg.lidarr.enable { prowlarr = let
hostname = "lidarr"; port = 9696;
image = "ghcr.io/hotio/lidarr:release-2.5.3.4341"; in lib.mkIf cfg.prowlarr.enable {
autoStart = true;
ports = [
"8686:8686/tcp"
];
extraOptions = [
"--network=${networkName}"
"--mount" ''type=volume,source=lidarr-backup,target=/backup,volume-driver=local,volume-opt=type=nfs,volume-opt=device=:/mnt/BIG/BACKUP/LIDARR,"volume-opt=o=addr=192.168.0.11,rw,nfsvers=4.2,async,nosuid"''
];
environment = {
PUID = toString config.users.users.lidarr.uid;
inherit PGID UMASK;
TZ = config.time.timeZone;
};
volumes = [
"lidarr-config:/config"
# TODO "data:/data"
];
};
prowlarr = lib.mkIf cfg.prowlarr.enable {
hostname = "prowlarr"; hostname = "prowlarr";
image = "ghcr.io/hotio/prowlarr:release-1.23.1.4708"; image = "ghcr.io/hotio/prowlarr:release-2.0.5.5160";
autoStart = true; autoStart = true;
ports = [ ports = lib.mkIf cfg.prowlarr.exposePorts [
"9696:9696/tcp" "${toString port}:${toString port}/tcp"
]; ];
extraOptions = [ extraOptions = [
"--network=${networkName}" "--network=${networkName}"
"--network=${proxyNet}"
]; ];
environment = { environment = {
PUID = toString config.users.users.prowlarr.uid; PUID = toString config.users.users.prowlarr.uid;
@ -194,44 +186,63 @@ in {
TZ = config.time.timeZone; TZ = config.time.timeZone;
}; };
volumes = [ volumes = [
# TODO "config:/config" "prowlarr-config:/config"
"/srv/prowlarr-backup:/config/Backups"
]; ];
labels = {
"traefik.enable" = "true";
"traefik.docker.network" = proxyNet;
"traefik.http.routers.prowlarr.rule" = "Host(`prowlarr.depeuter.dev`)";
"traefik.http.services.prowlarr.loadbalancer.server.port" = toString port;
};
}; };
qbittorrent = lib.mkIf cfg.qbittorrent.enable { qbittorrent = let
port = 10095;
in lib.mkIf cfg.qbittorrent.enable {
hostname = "qbittorrent"; hostname = "qbittorrent";
image = "ghcr.io/hotio/qbittorrent:release-4.6.7"; image = "ghcr.io/hotio/qbittorrent:release-5.1.2";
autoStart = true; autoStart = true;
ports = [ ports = lib.mkIf cfg.qbittorrent.exposePorts [
"10095:10095/udp" "${toString port}:${toString port}/tcp"
"10095:10095/tcp" "${toString port}:${toString port}/udp"
]; ];
extraOptions = [ extraOptions = [
"--network=${networkName}" "--network=${networkName}"
"--network=${proxyNet}"
"--mount" ''type=volume,source=torrents,target=/data,volume-driver=local,volume-opt=type=nfs,volume-opt=device=:/mnt/SMALL/MEDIA/TORRENT,"volume-opt=o=addr=192.168.0.11,rw,nfsvers=4.2,async,nosuid"''
]; ];
environment = { environment = {
PUID = toString config.users.users.qbittorrent.uid; PUID = toString config.users.users.qbittorrent.uid;
inherit PGID UMASK; inherit PGID UMASK;
TZ = config.time.timeZone; TZ = config.time.timeZone;
WEBUI_PORTS = "10095/tcp,10095/udp"; WEBUI_PORTS = "${toString port}/tcp,${toString port}/udp";
}; };
volumes = [ volumes = [
"/srv/qbittorrent:/config/config" "/srv/qbittorrent:/config"
"/srv/video:/media/video"
"/srv/torrent:/media/cache"
]; ];
labels = {
"traefik.enable" = "true";
"traefik.docker.network" = proxyNet;
"traefik.http.routers.qbittorrent.rule" = "Host(`qb.depeuter.dev`)";
"traefik.http.services.qbittorrent.loadbalancer.server.port" = toString port;
};
}; };
radarr = lib.mkIf cfg.radarr.enable { radarr = let
port = 7878;
in lib.mkIf cfg.radarr.enable {
hostname = "radarr"; hostname = "radarr";
image = "ghcr.io/hotio/radarr:release-5.9.1.9070"; image = "ghcr.io/hotio/radarr:testing-5.28.0.10205";
autoStart = true; autoStart = true;
ports = [ ports = lib.mkIf cfg.radarr.exposePorts [
"7878:7878/tcp" "${toString port}:${toString port}/tcp"
]; ];
extraOptions = [ extraOptions = [
"--network=${networkName}" "--network=${networkName}"
"--network=${proxyNet}"
]; ];
environment = { environment = {
PUID = toString config.users.users.radarr.uid; PUID = toString config.users.users.radarr.uid;
@ -239,20 +250,33 @@ in {
TZ = config.time.timeZone; TZ = config.time.timeZone;
}; };
volumes = [ volumes = [
# TODO "config:/config" "radarr-config:/config"
# TODO "data:/data"
"/srv/radarr-backup:/config/Backups"
"/srv/torrent:/media/cache"
"${videoHostPath}/Films:/media/movies"
]; ];
labels = {
"traefik.enable" = "true";
"traefik.docker.network" = proxyNet;
"traefik.http.routers.radarr.rule" = "Host(`radarr.depeuter.dev`)";
"traefik.http.services.radarr.loadbalancer.server.port" = toString port;
};
}; };
sonarr = lib.mkIf cfg.sonarr.enable { sonarr = let
port = 8989;
in lib.mkIf cfg.sonarr.enable {
hostname = "sonarr"; hostname = "sonarr";
image = "ghcr.io/hotio/sonarr:release-4.0.9.2244"; image = "ghcr.io/hotio/sonarr:release-4.0.15.2941";
autoStart = true; autoStart = true;
ports = [ ports = lib.mkIf cfg.sonarr.exposePorts [
"8989:8989/tcp" "${toString port}:${toString port}/tcp"
]; ];
extraOptions = [ extraOptions = [
"--network=${networkName}" "--network=${networkName}"
"--network=${proxyNet}"
]; ];
environment = { environment = {
PUID = toString config.users.users.sonarr.uid; PUID = toString config.users.users.sonarr.uid;
@ -260,9 +284,19 @@ in {
TZ = config.time.timeZone; TZ = config.time.timeZone;
}; };
volumes = [ volumes = [
# TODO "config:/config" "sonarr-config:/config"
# TODO "data:/data"
"/srv/sonarr-backup:/config/Backups"
"/srv/torrent:/media/cache"
"${videoHostPath}/Series:/media/series"
]; ];
labels = {
"traefik.enable" = "true";
"traefik.docker.network" = proxyNet;
"traefik.http.routers.sonarr.rule" = "Host(`sonarr.depeuter.dev`)";
"traefik.http.services.sonarr.loadbalancer.server.port" = toString port;
};
}; };
}; };
}; };

View file

@ -0,0 +1,45 @@
$TTL 604800
@ IN SOA ns1 admin (
15 ; Serial
604800 ; Refresh
86400 ; Retry
2419200 ; Expire
604800 ) ; Negative Cache TTL
; Name servers - NS records
IN NS ns1
; IN NS ns2
ns1 IN A 192.168.0.91
;ns2 IN A 192.158.0.X
; Hostnames
hugo.kmtl IN A 192.168.0.11
ingress.kmtl IN A 192.168.0.10
ingress.kmtl IN AAAA fe80::be24:11ff:fed6:842a
; Core services
cloud IN A 192.168.0.10
git IN A 78.23.37.117
home IN A 192.168.0.10
jelly IN CNAME ingress.kmtl
vault IN A 192.168.0.10
; Production VM
books IN A 192.168.0.31
calibre IN A 192.168.0.31
; Production VM - Arr
bazarr IN A 192.168.0.33
prowlarr IN A 192.168.0.33
qb IN A 192.168.0.33
radarr IN A 192.168.0.33
sonarr IN A 192.168.0.33
; Development VM
plex IN A 192.168.0.91
; Catchalls
*.production IN A 192.168.0.31
*.development IN A 192.168.0.91

View file

@ -0,0 +1,54 @@
{ config, lib, ... }:
let
cfg = config.homelab.apps.bind9;
in {
options.homelab.apps.bind9.enable = lib.mkEnableOption "ISC BIND 9 (Docker)";
config = lib.mkIf cfg.enable {
homelab.virtualisation.containers.enable = true;
environment.etc = {
"bind/named.conf" = {
source = ./named.conf;
mode = "0555";
};
"bind/named.conf.options" = {
source = ./named.conf.options;
mode = "0555";
};
"bind/named.conf.local" = {
source = ./named.conf.local;
mode = "0555";
};
"bind/zones/db.depeuter.dev" = {
source = ./db.depeuter.dev;
mode = "0555";
};
};
virtualisation.oci-containers.containers.bind9 = {
hostname = "bind9";
#image = "internetsystemsconsortium/bind9:9.20"; # Current stable
image = "ubuntu/bind9"; # Current stable
autoStart = true;
ports = [
"53:53/udp"
"53:53/tcp"
"953:953/tcp"
];
extraOptions = [
];
environment = {
};
volumes = [
"/etc/bind:/etc/bind" # For configuration, your `named.conf` lives here
"bind9-cache:/var/cache/bind"
#"...:/var/lib/bind" # Secondary zones
"bind9-logs:/var/log" # Logfiles
];
labels = {
};
};
};
}

View file

@ -0,0 +1,2 @@
include "/etc/bind/named.conf.options";
include "/etc/bind/named.conf.local";

View file

@ -0,0 +1,4 @@
zone "depeuter.dev" {
type primary;
file "/etc/bind/zones/db.depeuter.dev";
};

View file

@ -0,0 +1,35 @@
http local {
endpoints { "/dns-query"; };
};
acl bogusnets {
};
acl trusted {
192.168.0.0/16;
};
options {
directory "/var/cache/bind";
version "not currently available";
listen-on { any; };
listen-on-v6 { any; };
listen-on tls ephemeral { any; };
listen-on-v6 tls ephemeral { any; };
listen-on tls ephemeral http local { any; };
listen-on-v6 tls ephemeral http local { any; };
recursion yes;
forwarders {
9.9.9.9;
149.112.112.112;
};
forward only;
allow-query { any; };
allow-recursion { any; };
allow-transfer { none; };
blackhole { bogusnets; };
};

View file

@ -1,11 +1,39 @@
{ config, lib, ... }: { config, lib, pkgs, ... }:
let let
cfg = config.homelab.apps.calibre; cfg = config.homelab.apps.calibre;
in {
options.homelab.apps.calibre.enable = lib.mkEnableOption "Calibre";
config = lib.mkIf cfg.enable { PUID = toString config.users.users.calibre.uid;
PGID = toString config.users.groups.media.gid;
books = "/srv/books";
calibre-config = "/srv/calibre-config";
calibre-web-config = "/srv/calibre-web-config";
networkName = "calibre";
proxyNet = config.homelab.apps.traefik.sharedNetworkName;
in {
options.homelab.apps.calibre = {
enable = lib.mkEnableOption "Calibre (Desktop + Web)";
desktop.enable = lib.mkEnableOption "Calibre Desktop (KasmVNC)";
web.enable = lib.mkEnableOption "Calibre Web";
};
config = lib.mkMerge [
{
homelab.apps.calibre = lib.mkIf cfg.enable {
desktop.enable = true;
web.enable = true;
};
}
# Common
(lib.mkIf (cfg.desktop.enable || cfg.web.enable) {
homelab = {
users.media.enable = true;
virtualisation.containers.enable = true;
};
users.users.calibre = { users.users.calibre = {
uid = lib.mkForce 3010; uid = lib.mkForce 3010;
isSystemUser = true; isSystemUser = true;
@ -13,5 +41,149 @@ in {
home = "/var/empty"; home = "/var/empty";
shell = null; shell = null;
}; };
fileSystems."${books}" = {
device = "192.168.0.11:/mnt/SMALL/MEDIA/BOOKS";
fsType = "nfs";
options = [
"rw"
"auto"
"nfsvers=4.2"
"rsize=1048576" "wsize=1048576"
"soft"
"timeo=600" "retrans=2"
"_netdev" "nosuid" "tcp"
];
}; };
# Make sure the Docker network exists.
systemd.services."docker-${networkName}-create-network" = {
requiredBy = [
"docker-calibre.service"
];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
script = ''
if ! ${pkgs.docker}/bin/docker network ls | grep -q ${networkName}; then
${pkgs.docker}/bin/docker network create ${networkName}
fi
'';
};
})
# Calibre desktop
(lib.mkIf cfg.desktop.enable {
fileSystems."${calibre-config}" = {
device = "192.168.0.11:/mnt/SMALL/CONFIG/CALIBRE";
fsType = "nfs";
options = [
"rw"
"auto"
"nfsvers=4.2"
"rsize=1048576" "wsize=1048576"
"soft"
"timeo=600" "retrans=2"
"_netdev" "nosuid" "tcp"
];
};
virtualisation.oci-containers.containers.calibre = let
innerPort = 8080;
in {
hostname = "calibre";
image = "lscr.io/linuxserver/calibre:v8.10.0-ls354";
autoStart = true;
ports = [
# Open ports if you don't use Traefik
"9480:${toString innerPort}" # Calibre desktop GUI
#"9481:8181" # Calibre desktop GUI HTTPS
#"9581:8081" # Calibre webserver gui
];
extraOptions = [
"--network=${networkName}"
"--network=${proxyNet}"
# syscalls are unkown to Docker
#"--security-opt" "seccomp=unconfined"
];
environment = {
inherit PUID PGID;
#UMASK = "022";
TZ = config.time.timeZone;
#PASSWORD = "";
#CLI_ARGS = "";
};
volumes = [
"${calibre-config}:/config"
"${books}:/media/books"
];
labels = {
"traefik.enable" = "true";
"traefik.docker.network" = proxyNet;
"traefik.http.routers.calibre.rule" = "Host(`calibre.depeuter.dev`)";
"traefik.http.services.calibre.loadbalancer.server.port" = toString innerPort;
};
};
})
# Calibre Web
(lib.mkIf cfg.web.enable {
fileSystems."${calibre-web-config}" = {
device = "192.168.0.11:/mnt/SMALL/CONFIG/CALIBRE-WEB";
fsType = "nfs";
options = [
"rw"
"auto"
"nfsvers=4.2"
"rsize=1048576" "wsize=1048576"
"soft"
"timeo=600" "retrans=2"
"_netdev" "nosuid" "tcp"
];
};
virtualisation.oci-containers.containers.calibre-web = let
innerPort = 8083;
in {
hostname = "calibre-web";
image = "lscr.io/linuxserver/calibre-web:0.6.25-ls346";
autoStart = true;
ports = [
# Open ports if you don't use Traefik
"8083:${toString innerPort}" # Web UI
];
extraOptions = [
"--network=${networkName}"
"--network=${proxyNet}"
];
environment = {
inherit PUID PGID;
#UMASK = "022";
TZ = config.time.timeZone;
# (x86-64 only) Adds the ability to perform ebook conversion
DOCKER_MODS = "linuxserver/mods:universal-calibre";
# Allow Google Oauth
#OAUTHLIB_RELAX_TOKEN_SCOPE = "1";
};
volumes = [
"${calibre-web-config}:/config"
"${books}:/media/books"
];
labels = {
"traefik.enable" = "true";
"traefik.docker.network" = proxyNet;
"traefik.http.routers.calibre-web.rule" = "Host(`books.depeuter.dev`)";
"traefik.http.services.calibre-web.loadbalancer.server.port" = toString innerPort;
};
};
})
];
} }

View file

@ -0,0 +1,28 @@
{ config, lib, pkgs, ... }:
let
cfg = config.homelab.apps.changedetection;
in {
options.homelab.apps.changedetection.enable = lib.mkEnableOption "Changedetection.io";
config = lib.mkIf cfg.enable {
homelab.virtualisation.containers.enable = true;
virtualisation.oci-containers.containers.changedetection = {
hostname = "changedetection";
image = "ghcr.io/dgtlmoon/changedetection.io";
autoStart = true;
ports = [
"5000:5000/tcp"
];
extraOptions = [
];
volumes = [
"changedetection:/datastore"
];
environment = {
LOGGER_LEVEL = "WARNING";
};
};
};
}

View file

@ -1,12 +1,16 @@
{ {
imports = [ imports = [
./arr ./arr
./bind9
./calibre ./calibre
./changedetection
./freshrss
./gitea ./gitea
./jellyfin ./jellyfin
./plex ./plex
./speedtest ./speedtest
./technitium-dns ./technitium-dns
./traefik
./vaultwarden ./vaultwarden
]; ];
} }

View file

@ -0,0 +1,93 @@
{ config, lib, pkgs, ... }:
let
cfg = config.homelab.apps.freshrss;
networkName = "freshrss";
in {
options.homelab.apps.freshrss = {
enable = lib.mkEnableOption "FreshRSS";
port = lib.mkOption {
type = lib.types.int;
default = 9080;
description = "FreshRSS WebUI port";
};
};
config = let
inherit (config.homelab.apps.freshrss) port;
in
lib.mkIf cfg.enable {
homelab.virtualisation.containers.enable = true;
fileSystems."/srv/freshrss" = {
device = "192.168.0.11:/mnt/SMALL/CONFIG/FRESHRSS";
fsType = "nfs";
options = [
"rw"
"auto"
"nfsvers=4.2"
"async" "soft" "timeo=600"
"retrans=2"
"_netdev"
"nosuid"
"tcp"
];
};
systemd.services."docker-${networkName}-create-network" = {
description = "Create Docker network for ${networkName}";
requiredBy = [
"docker-freshrss.service"
];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
script = ''
if ! ${pkgs.docker}/bin/docker network ls | grep -q ${networkName}; then
${pkgs.docker}/bin/docker network create ${networkName}
fi
'';
};
virtualisation.oci-containers.containers.freshrss = {
hostname = "freshrss";
image = "freshrss/freshrss:1.25.0";
autoStart = true;
user = "0:33";
ports = [
"${toString port}:80/tcp"
];
extraOptions = [
"--network=${networkName}"
];
environment = {
TZ = config.time.timeZone;
CRON_MIN = "3,18,33,48"; # Alternatively, configure cron inside container.
SERVER_DNS = "rss.depeuter.dev";
TRUSTED_PROXY = "172.16.0.1/12 192.168.0.1/16";
};
volumes = [
"/srv/freshrss/www/freshrss/data:/var/www/FreshRSS/data"
"/srv/freshrss/www/freshrss/extensions:/var/www/FreshRSS/extensions"
];
labels = {
"traefik.enable" = "true";
"traefik.http.middlewares.freshrssM1.compress" = "true";
"traefik.http.middlewares.freshrssM2.headers.browserXssFilter" = "true";
"traefik.http.middlewares.freshrssM2.headers.forceSTSHeader" = "true";
"traefik.http.middlewares.freshrssM2.headers.frameDeny" = "true";
"traefik.http.middlewares.freshrssM2.headers.referrerPolicy" = "no-referrer-when-downgrade";
"traefik.http.middlewares.freshrssM2.headers.stsSeconds" = "31536000";
"traefik.http.routers.freshrss.entryPoints" = "websecure";
"traefik.http.routers.freshrss.tls" = "true";
"traefik.http.services.freshrss.loadbalancer.server.port" = "80";
"traefik.http.routers.freshrss.middlewares" = "freshrssM1,freshrssM2";
"traefik.http.routers.freshrss.rule" = "Host(`rss.depeuter.dev`)";
};
};
};
}

View file

@ -124,7 +124,7 @@ in {
gitea = { gitea = {
hostname = "gitea"; hostname = "gitea";
image = "codeberg.org/forgejo/forgejo:8.0.3-rootless"; image = "codeberg.org/forgejo/forgejo:11.0.1-rootless";
autoStart = true; autoStart = true;
user = "${toString UID}:${toString GID}"; user = "${toString UID}:${toString GID}";
ports = [ ports = [
@ -379,6 +379,25 @@ in {
# A relative path is interpreted as _`AppWorkPath`_/%(ROOT)s # A relative path is interpreted as _`AppWorkPath`_/%(ROOT)s
FORGEJO__repository__ROOT = repoDir; FORGEJO__repository__ROOT = repoDir;
# ... # ...
# Force every new repository to be private.
FORGEJO__repository__FORCE_PRIVATE = "false";
# Default private when creating a new repository with push-to-create.
FORGEJO__repository__DEFAULT_PUSH_TO_CREATE = "true";
# ...
# Allow users to push local repositories to Forgejo and have them automatically created for a user.
FORGEJO__repository__ENABLE_PUSH_CREATE_USER = "true";
# Allow users to push local repositories to Forgejo and have them automatically created for an org.
FORGEJO__repository__ENABLE_PUSH_CREATE_ORG = "false";
# Comma separated list of globally disabled repo units.
FORGEJO__repository__DISABLED_REPO_UNITS = "";
# Comma separated list of default new repo units.
FORGEJO__repository__DEFAULT_REPO_UNITS = "repo.code,repo.issues,repo.pulls,repo.releases,repo.actions";
# Comma separated list of default forked repo units.
FORGEJO__repository__DEFAULT_FORK_REPO_UNITS = "repo.code,repo.pulls";
# Prefix archive files by placing them in a directory named after the repository.
FORGEJO__repository__PREFIX_ARCHIVE_FILES = "true";
# Disable migrating feature.
FORGEJO__repository__DISABLE_MIGRATIONS = "false";
# Disable stars feature. # Disable stars feature.
FORGEJO__repository__DISABLE_STARS = "true"; FORGEJO__repository__DISABLE_STARS = "true";
# Disable repository forking. # Disable repository forking.
@ -557,7 +576,7 @@ in {
#FORGEJO__picture__AVATAR_RENDERED_SIZE_FACTOR = "2"; #FORGEJO__picture__AVATAR_RENDERED_SIZE_FACTOR = "2";
# Maximum allowed file size for uploaded avatars. # Maximum allowed file size for uploaded avatars.
# This is to limit the amount of RAM used when resizing the image. # This is to limit the amount of RAM used when resizing the image.
#FORGEJO__picture__AVATAR_MAX_FILE_SIZE = "1048576"; FORGEJO__picture__AVATAR_MAX_FILE_SIZE = "1048576";
# If the uploaded file is not larger than this byte size, the image will be used as is, without resizing/converting. # If the uploaded file is not larger than this byte size, the image will be used as is, without resizing/converting.
#FORGEJO__picture__AVATAR_MAX_ORIGIN_SIZE = "262144"; #FORGEJO__picture__AVATAR_MAX_ORIGIN_SIZE = "262144";
# Chinese users can choose "duoshuo" # Chinese users can choose "duoshuo"

View file

@ -4,6 +4,7 @@ let
cfg = config.homelab.apps.jellyfin; cfg = config.homelab.apps.jellyfin;
networkName = "jellyfin"; networkName = "jellyfin";
inherit (config.homelab.fileSystems) media;
UID = 3008; UID = 3008;
GID = config.users.groups.media.gid; GID = config.users.groups.media.gid;
@ -12,6 +13,11 @@ in {
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
homelab = { homelab = {
fileSystems.media.video = {
enable = true;
permissions = [ "read" ];
};
users = { users = {
apps.enable = true; apps.enable = true;
media.enable = true; media.enable = true;
@ -32,18 +38,6 @@ in {
]; ];
}; };
"/srv/video" = {
device = "192.168.0.11:/mnt/SMALL/MEDIA/VIDEO";
fsType = "nfs";
options = [
"ro"
"nfsvers=4.2"
"async" "soft"
"timeo=100" "retry=50" "actimeo=1800" "lookupcache=all"
"nosuid" "tcp"
];
};
"/srv/homevideo" = { "/srv/homevideo" = {
device = "192.168.0.11:/mnt/BIG/MEDIA/HOMEVIDEO/ARCHIVE"; device = "192.168.0.11:/mnt/BIG/MEDIA/HOMEVIDEO/ARCHIVE";
fsType = "nfs"; fsType = "nfs";
@ -101,7 +95,7 @@ in {
virtualisation.oci-containers.containers = { virtualisation.oci-containers.containers = {
jellyfin = { jellyfin = {
hostname = "jellyfin"; hostname = "jellyfin";
image = "jellyfin/jellyfin:10.10.0"; image = "jellyfin/jellyfin:10.10.7";
user = "${toString UID}:${toString GID}"; user = "${toString UID}:${toString GID}";
autoStart = true; autoStart = true;
ports = [ ports = [
@ -117,7 +111,7 @@ in {
"cache:/cache" "cache:/cache"
"/srv/audio:/media/audio" "/srv/audio:/media/audio"
"/srv/video:/media/video" "${media.video.hostPath}:/media/video"
"/srv/homevideo:/media/homevideo" "/srv/homevideo:/media/homevideo"
"/srv/photo:/media/photo" "/srv/photo:/media/photo"
]; ];
@ -126,11 +120,28 @@ in {
}; };
}; };
feishin = { jellyfin-vue = {
hostname = "feishin"; hostname = "jellyfin-vue";
image = "ghcr.io/jeffvli/feishin:0.7.1"; image = "ghcr.io/jellyfin/jellyfin-vue:unstable";
autoStart = true;
ports = [ ports = [
"9180:9180/tcp" # Web player (HTTP) "8080:80/tcp"
];
extraOptions = [
"--network=${networkName}"
];
labels = {
};
};
feishin = let
feishinPort = "9180";
in {
hostname = "feishin";
image = "ghcr.io/jeffvli/feishin:0.19.0";
autoStart = true;
ports = [
"${feishinPort}:9180/tcp" # Web player (HTTP)
]; ];
extraOptions = [ extraOptions = [
"--network=${networkName}" "--network=${networkName}"
@ -147,8 +158,11 @@ in {
TZ = config.time.timeZone; TZ = config.time.timeZone;
}; };
labels = { labels = {
"traefik.enable" = "true";
"traefik.http.routers.feishin.rule" = "Host(`play.jelly.depeuter.dev`)";
"traefik.http.services.feishin.loadbalancer.server.port" = feishinPort;
"traefik.tls.options.default.minVersion" = "VersionTLS13";
}; };
autoStart = true;
}; };
}; };
}; };

View file

@ -6,18 +6,31 @@ in {
options.homelab.apps.plex.enable = lib.mkEnableOption "Plex"; options.homelab.apps.plex.enable = lib.mkEnableOption "Plex";
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
homelab = {
users = {
apps.enable = true;
media.enable = true;
};
fileSystems.media.video.enable = true;
virtualisation.containers.enable = true;
};
users.users.plex = { users.users.plex = {
uid = lib.mkForce 3009; uid = lib.mkForce 3009;
isSystemUser = true; isSystemUser = true;
group = config.users.groups.media; group = config.users.groups.apps.name;
extraGroups = [
config.users.groups.media.name
];
home = "/var/empty"; home = "/var/empty";
shell = null; shell = null;
}; };
virtualisation.oci-containers.containers = { virtualisation.oci-containers.containers.plex = let
plex = { videoHostPath = config.homelab.fileSystems.media.video.hostPath;
in {
hostname = "plex"; hostname = "plex";
image = "plexinc/pms-docker:1.41.0.8992-8463ad060"; image = "plexinc/pms-docker:1.41.6.9685-d301f511a";
autoStart = true; autoStart = true;
ports = [ ports = [
"32400:32400/tcp" # Plex Media Server "32400:32400/tcp" # Plex Media Server
@ -30,20 +43,26 @@ in {
# "8324:8324/tcp" # Controlling Plex for Roku via Plex Companion # "8324:8324/tcp" # Controlling Plex for Roku via Plex Companion
]; ];
environment = { environment = {
ADVERTISE_AP = "..."; # TODO Configure ip #ADVERTISE_AP = "..."; # TODO Configure ip
ALLOWED_NETWORKS = "192.168.0.0/24,172.16.0.0/16"; ALLOWED_NETWORKS = "192.168.0.0/24,172.16.0.0/16";
CHANGE_CONFIG_DIR_OWNERSHIP = "false"; CHANGE_CONFIG_DIR_OWNERSHIP = "false";
HOSTNAME = "PlexServer"; HOSTNAME = "Hugo-Plex";
PLEX_CLAIM = "..."; # TODO Add token PLEX_CLAIM = "claim-d5MqsjMeCZrUF6oUvssr";
PLEX_UID = config.users.users.plex.uid; PLEX_UID = toString config.users.users.plex.uid;
PLEX_GID = config.users.groups.media.gid; PLEX_GID = toString config.users.groups.media.gid;
TZ = config.time.timeZone; TZ = config.time.timeZone;
}; };
volumes = [ volumes = [
# TODO "config:/var/lib/plexmediaserver" # TODO Backup over NFS
# TODO "transcode-temp:/transcode" "plex-config:/config"
# TODO "media:/data" "plex-transcode:/transcode"
"${videoHostPath}:/data/video:ro"
]; ];
labels = {
"traefik.enable" = "true";
"traefik.http.routers.plex.rule" = "Host(`plex.depeuter.dev`)";
"traefik.http.services.plex.loadbalancer.server.port" = "32400";
}; };
}; };
}; };

View file

@ -0,0 +1,90 @@
{ config, lib, pkgs, ... }:
let
cfg = config.homelab.apps.traefik;
port = 8080;
in {
options.homelab.apps.traefik = {
enable = lib.mkEnableOption "Traefik Reverse Proxy";
sharedNetworkName = lib.mkOption {
type = lib.types.str;
default = "traefik";
description = "The name of the shared network to connect the container to.";
};
};
config = lib.mkIf cfg.enable {
homelab.virtualisation.containers.enable = true;
# Make sure the Docker network exists.
systemd.services."docker-${cfg.sharedNetworkName}-create-network" = {
description = "Create Docker network for ${cfg.sharedNetworkName}";
requiredBy = [
"docker-traefik.service"
];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
script = ''
if ! ${pkgs.docker}/bin/docker network ls | grep -q ${cfg.sharedNetworkName}; then
${pkgs.docker}/bin/docker network create ${cfg.sharedNetworkName}
fi
'';
};
virtualisation.oci-containers.containers.traefik = {
hostname = "traefik";
image = "traefik:v3.4.3";
autoStart = true;
ports = [
"80:80/tcp"
"443:443/tcp"
"${toString port}:${toString port}/tcp" # Web UI (enabled by --api.insecure=true)
];
extraOptions = [
"--network=${cfg.sharedNetworkName}"
];
environmentFiles = [
/home/admin/.cloudflare.secret
];
cmd = [
"--api.insecure=true"
# Add Docker provider
"--providers.docker=true"
"--providers.docker.exposedByDefault=false"
# Add web entrypoint
"--entrypoints.web.address=:80/tcp"
"--entrypoints.web.http.redirections.entrypoint.to=websecure"
"--entrypoints.web.http.redirections.entrypoint.scheme=https"
# Add websecure entrypoint
"--entrypoints.websecure.address=:443/tcp"
"--entrypoints.websecure.http.tls=true"
"--entrypoints.websecure.http.tls.certResolver=letsencrypt"
"--entrypoints.websecure.http.tls.domains[0].main=depeuter.dev"
"--entrypoints.websecure.http.tls.domains[0].sans=*.depeuter.dev"
"--entrypoints.websecure.http.tls.domains[1].sans=*.${config.networking.hostName}.depeuter.dev"
# Certificates
"--certificatesresolvers.letsencrypt.acme.dnschallenge=true"
"--certificatesresolvers.letsencrypt.acme.dnschallenge.provider=cloudflare"
"--certificatesresolvers.letsencrypt.acme.email=tibo.depeuter@telenet.be"
"--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
];
volumes = [
"letsencryp:/letsencrypt"
"/var/run/docker.sock:/var/run/docker.sock:ro"
];
labels = {
"traefik.enable" = "true";
"traefik.http.routers.traefik.rule" = "Host(`traefik.${config.networking.hostName}.depeuter.dev`)";
"traefik.http.services.traefik.loadbalancer.server.port" = toString port;
};
};
};
}

View file

@ -5,7 +5,24 @@ let
networkName = "vaultwarden"; networkName = "vaultwarden";
in { in {
options.homelab.apps.vaultwarden.enable = lib.mkEnableOption "Vaultwarden"; options.homelab.apps.vaultwarden = {
enable = lib.mkEnableOption "Vaultwarden";
port = lib.mkOption {
type = lib.types.int;
default = 10102;
description = "Vaultwarden WebUI port";
};
domain = lib.mkOption {
type = lib.types.string;
example = "https://vault.depeuter.dev";
description = "Domain to configure Vaultwarden on";
};
name = lib.mkOption {
type = lib.types.string;
example = "Hugo's Vault";
description = "Service name to use for invitations and mail";
};
};
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
homelab = { homelab = {
@ -33,13 +50,16 @@ in {
''; '';
}; };
virtualisation.oci-containers.containers = { virtualisation.oci-containers.containers = let
vaultwarden-db = { dbHostname = "vaultwarden-db";
hostname = "vaultwarden-db"; dbPort = 5432;
in {
vaultwardenDb = {
hostname = dbHostname;
image = "postgres:15.8-alpine"; image = "postgres:15.8-alpine";
autoStart = true; autoStart = true;
ports = [ ports = [
"5432:5432/tcp" "${toString dbPort}:5432/tcp"
]; ];
extraOptions = [ extraOptions = [
"--network=${networkName}" "--network=${networkName}"
@ -57,16 +77,16 @@ in {
dataDir = "/data"; dataDir = "/data";
in { in {
hostname = "vaultwarden"; hostname = "vaultwarden";
image = "vaultwarden/server:1.30.5-alpine"; image = "vaultwarden/server:1.34.3-alpine";
autoStart = true; autoStart = true;
ports = [ ports = [
"10102:80/tcp" "${toString cfg.port}:80/tcp"
]; ];
extraOptions = [ extraOptions = [
"--network=${networkName}" "--network=${networkName}"
]; ];
dependsOn = [ dependsOn = [
"vaultwarden-db" "vaultwardenDb"
]; ];
volumes = [ volumes = [
"vaultwarden:${dataDir}" "vaultwarden:${dataDir}"
@ -115,7 +135,7 @@ in {
## Details: ## Details:
## - https://docs.diesel.rs/2.1.x/diesel/pg/struct.PgConnection.html ## - https://docs.diesel.rs/2.1.x/diesel/pg/struct.PgConnection.html
## - https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING ## - https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING
DATABASE_URL = "postgresql://vaultwarden:ChangeMe@vaultwarden-db:5432/vaultwarden"; DATABASE_URL = "postgresql://vaultwarden:ChangeMe@${dbHostname}:${toString dbPort}/vaultwarden";
## Enable WAL for the DB ## Enable WAL for the DB
## Set to false to avoid enabling WAL during startup. ## Set to false to avoid enabling WAL during startup.
@ -244,7 +264,7 @@ in {
## For development ## For development
# DOMAIN=http://localhost # DOMAIN=http://localhost
## For public server ## For public server
DOMAIN = "https://vault.depeuter.dev"; DOMAIN = cfg.domain;
## For public server (URL with port number) ## For public server (URL with port number)
# DOMAIN=https://vw.domain.tld:8443 # DOMAIN=https://vw.domain.tld:8443
## For public server (URL with path) ## For public server (URL with path)
@ -298,7 +318,7 @@ in {
## Note that setting this option to true prevents logins until the email address has been verified! ## Note that setting this option to true prevents logins until the email address has been verified!
## The welcome email will include a verification link, and login attempts will periodically ## The welcome email will include a verification link, and login attempts will periodically
## trigger another verification email to be sent. ## trigger another verification email to be sent.
SIGNUPS_VERIFY = "true"; SIGNUPS_VERIFY = "false";
## If SIGNUPS_VERIFY is set to true, this limits how many seconds after the last time ## If SIGNUPS_VERIFY is set to true, this limits how many seconds after the last time
## an email verification link has been sent another verification email will be sent ## an email verification link has been sent another verification email will be sent
@ -328,7 +348,7 @@ in {
## Invitations org admins to invite users, even when signups are disabled ## Invitations org admins to invite users, even when signups are disabled
# INVITATIONS_ALLOWED=true # INVITATIONS_ALLOWED=true
## Name shown in the invitation emails that don't come from a specific organization ## Name shown in the invitation emails that don't come from a specific organization
INVITATION_ORG_NAME = "Hugo's Vault"; INVITATION_ORG_NAME = cfg.name;
## The number of hours after which an organization invite token, emergency access invite token, ## The number of hours after which an organization invite token, emergency access invite token,
## email verification token and deletion request token will expire (must be at least 1) ## email verification token and deletion request token will expire (must be at least 1)
@ -571,7 +591,7 @@ in {
## Note: if SMTP_USERNAME is specified, SMTP_PASSWORD is mandatory ## Note: if SMTP_USERNAME is specified, SMTP_PASSWORD is mandatory
SMTP_HOST = "smtp.gmail.com"; SMTP_HOST = "smtp.gmail.com";
SMTP_FROM = "vault@depeuter.dev"; SMTP_FROM = "vault@depeuter.dev";
SMTP_FROM_NAME = "Hugo's Vault"; SMTP_FROM_NAME = cfg.name;
# SMTP_USERNAME=username # SMTP_USERNAME=username
# SMTP_PASSWORD=password # SMTP_PASSWORD=password
# SMTP_TIMEOUT=15 # SMTP_TIMEOUT=15

View file

@ -1,6 +1,7 @@
{ {
imports = [ imports = [
./apps ./apps
./fileSystems
./services ./services
./virtualisation ./virtualisation

View file

@ -0,0 +1,5 @@
{
imports = [
./media
];
}

View file

@ -0,0 +1,5 @@
{
imports = [
./video
];
}

View file

@ -0,0 +1,42 @@
{ config, lib, ... }:
let
cfg = config.homelab.fileSystems.media.video;
remotePath = "/mnt/SMALL/MEDIA/VIDEO";
maxPermissions = permissions:
if builtins.elem "write" permissions then "rw"
else "ro";
permissionsOption = maxPermissions cfg.permissions;
in {
options.homelab.fileSystems.media.video = {
enable = lib.mkEnableOption "MEDIA/VIDEO dataset";
hostPath = lib.mkOption {
type = lib.types.path;
default = "/srv/video";
description = "Mountpath on host";
};
permissions = lib.mkOption {
type = lib.types.listOf (lib.types.enum [ "read" "write" ]);
default = [ "read" ];
description = "Mount options permissions";
};
};
config = lib.mkIf cfg.enable {
fileSystems."${cfg.hostPath}" = {
device = "192.168.0.11:${remotePath}";
fsType = "nfs";
options = [
permissionsOption
"auto"
"nfsvers=4.2"
"async" "soft"
"rsize=1048576" "wsize=1048576"
"timeo=600" "retry=50" "retrans=2" "actimeo=1800" "lookupcache=all"
"_netdev" "nosuid" "tcp"
];
};
};
}

View file

@ -44,6 +44,6 @@ in {
]; ];
}; };
}; };
}; };
} }

19
secrets/secrets.yaml Normal file
View file

@ -0,0 +1,19 @@
users:
admin:
authorized_keys:
NixOS: ENC[AES256_GCM,data:sj2hkUkWp628KuXp+AnncLdawHpxb9fH1ZHnIisP0x9Tght9+/X2sWHpuMSeqi2i/R8B+Wgte66QkuwAOB0j+oB9N+66EhehmWZlK5hD/22p,iv:z18U+LvAQgPDfBBewE3lJmWZd0NGCPwJIe/h3tupuZc=,tag:ZJar3spO66JbDXygdTHh2w==,type:str]
sops:
age:
- recipient: age1qzutny0mqpcccqw6myyfntu6wcskruu9ghzvt6r4te7afkqwnguq05ex37
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjUSt2REk2Mmd0bk9ubjJk
dXFiY2JNR1dyZW9qTUdzaWZhY3c3amVwQzA0CkZHNVpZVjhsWXhVQVNaR0xONzhh
Y0lQaWNaNmpYYVdrRnZIZUhvUFUzcWMKLS0tIDAvSmF0VmpxcnZEQStXUjNCUE5Z
RnA2Lzk2WHFxOEh6dHN0aGhVSVpLTW8KA7IOvGDMBtgo4pe0Sw3Lol243xCDAJ4i
PhcJFiUObVRFZN7ISlULnOlTO3pT9jWvvmC5rDZWId3PQ8qjPvnOUg==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2025-10-04T17:33:22Z"
mac: ENC[AES256_GCM,data:I7I7uDFEWfw9+4KROtjHMVhaxYrVK5QmLfFZShSajF0A2Zxu9lg+fDGiMHk40JC5zD31P70QS/ipye1mBGQbCbLEA7uBUhNzZ7G1g58cIXF6vSGmt0fovm0MVSxEJ44r05fx6uT4OJu5BYVxYSlG84gTj9rCFXxxcBJMrh+6yaI=,iv:c1vudsp9bg0Pc2ddRyvWn6Tf0LhqNuEjxG9D4PpHqxs=,tag:K/1PSHhrTdsNPcPmRv/2Ew==,type:str]
unencrypted_suffix: _unencrypted
version: 3.10.2

View file

@ -18,8 +18,8 @@ in {
]; ];
initialPassword = "ChangeMe"; initialPassword = "ChangeMe";
openssh.authorizedKeys.keys = [ openssh.authorizedKeys.keys = [
# TODO ChangeMe # HomeLab > NixOS > admin > ssh
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPrG+ldRBdCeHEXrsy/qHXIJYg8xQXVuiUR0DxhFjYNg" "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGWIOOEqTy8cWKpENVbzD4p7bsQgQb/Dgpzk8i0dZ00T"
]; ];
packages = with pkgs; [ packages = with pkgs; [
curl curl