diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..485dee6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea diff --git a/.sops.yaml b/.sops.yaml new file mode 100644 index 0000000..02cc451 --- /dev/null +++ b/.sops.yaml @@ -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 diff --git a/flake.lock b/flake.lock index 8df2f99..ca6e418 100644 --- a/flake.lock +++ b/flake.lock @@ -5,11 +5,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1726560853, - "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", "owner": "numtide", "repo": "flake-utils", - "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", "type": "github" }, "original": { @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1730785428, - "narHash": "sha256-Zwl8YgTVJTEum+L+0zVAWvXAGbWAuXHax3KzuejaDyo=", + "lastModified": 1759381078, + "narHash": "sha256-gTrEEp5gEspIcCOx9PD8kMaF1iEmfBcTbO0Jag2QhQs=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "4aa36568d413aca0ea84a1684d2d46f55dbabad7", + "rev": "7df7ff7d8e00218376575f0acdcc5d66741351ee", "type": "github" }, "original": { @@ -37,9 +37,30 @@ "inputs": { "flake-utils": "flake-utils", "nixpkgs": "nixpkgs", + "sops-nix": "sops-nix", "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": { "locked": { "lastModified": 1681028828, @@ -62,11 +83,11 @@ ] }, "locked": { - "lastModified": 1722363685, - "narHash": "sha256-XCf2PIAT6lH7BwytgioPmVf/wkzXjSKScC4KzcZgb64=", + "lastModified": 1738591040, + "narHash": "sha256-4WNeriUToshQ/L5J+dTSWC5OJIwT39SEP7V7oylndi8=", "owner": "gytis-ivaskevicius", "repo": "flake-utils-plus", - "rev": "6b10f51ff73a66bb29f3bc8151a59d217713f496", + "rev": "afcb15b845e74ac5e998358709b2b5fe42a948d1", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 57bbff8..446f4ce 100644 --- a/flake.nix +++ b/flake.nix @@ -5,6 +5,10 @@ nixpkgs.url = "nixpkgs/nixos-unstable"; flake-utils.url = "github:numtide/flake-utils"; + sops-nix = { + url = "github:Mic92/sops-nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; utils = { url = "github:gytis-ivaskevicius/flake-utils-plus"; inputs.flake-utils.follows = "flake-utils"; @@ -13,11 +17,11 @@ outputs = inputs@{ self, nixpkgs, - flake-utils, utils, + flake-utils, sops-nix, utils, ... }: let - system = "x86_64-linux"; + system = utils.lib.system.x86_64-linux; in utils.lib.mkFlake { inherit self inputs; @@ -28,20 +32,30 @@ modules = [ ./modules ./users + + sops-nix.nixosModules.sops ]; }; hosts = { - Niko.modules = [ ./hosts/Niko ]; + # Physical hosts + Niko.modules = [ ./hosts/Niko ]; + # Virtual machines + + # Single-service Ingress.modules = [ ./hosts/Ingress ]; Gitea.modules = [ ./hosts/Gitea ]; Vaultwarden.modules = [ ./hosts/Vaultwarden ]; + # Production multi-service Binnenpost.modules = [ ./hosts/Binnenpost ]; + Production.modules = [ ./hosts/Production ]; ProductionGPU.modules = [ ./hosts/ProductionGPU ]; + ProductionArr.modules = [ ./hosts/ProductionArr ]; ACE.modules = [ ./hosts/ACE ]; + # Others Template.modules = [ ./hosts/Template ]; Development.modules = [ ./hosts/Development ]; Testing.modules = [ ./hosts/Testing ]; diff --git a/hosts/Binnenpost/default.nix b/hosts/Binnenpost/default.nix index d78e2da..561fbe1 100644 --- a/hosts/Binnenpost/default.nix +++ b/hosts/Binnenpost/default.nix @@ -16,6 +16,7 @@ apps = { speedtest.enable = true; technitiumDNS.enable = true; + traefik.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"; }; } diff --git a/hosts/Development/default.nix b/hosts/Development/default.nix index 40983c9..b2237b7 100644 --- a/hosts/Development/default.nix +++ b/hosts/Development/default.nix @@ -3,8 +3,10 @@ { config = { homelab = { - apps.arr = { - qbittorrent.enable = true; + apps = { + bind9.enable = true; + traefik.enable = true; + plex.enable = true; }; virtualisation.guest.enable = true; }; diff --git a/hosts/Ingress/default.nix b/hosts/Ingress/default.nix index 63e3ced..68cdcfe 100644 --- a/hosts/Ingress/default.nix +++ b/hosts/Ingress/default.nix @@ -59,6 +59,7 @@ prefixLength = 24; }; "cloud.depeuter.dev" = { }; "git.depeuter.dev" = { }; + "home.depeuter.dev" = { }; "jelly.depeuter.dev" = { }; "vault.depeuter.dev" = { }; }; @@ -136,10 +137,27 @@ prefixLength = 24; 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"; "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" = { enableACME = true; forceSSL = true; @@ -176,7 +194,7 @@ prefixLength = 24; }; }; extraConfig = '' - client_max_body_size 20M; + client_max_body_size 512M; # Security / XSS Mitigation Headers # 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-Proto $scheme; - client_max_body_size 512M; + client_max_body_size 10G; keepalive_timeout 600s; proxy_buffers 4 256k; # Number and size of buffers for reading response proxy_buffer_size 256k; # Buffer for the first part of the response @@ -220,10 +238,18 @@ prefixLength = 24; enableACME = true; forceSSL = true; locations = { - "/".proxyPass = "http://192.168.0.22:10102"; + "/" = { + proxyPass = "http://192.168.0.22:10102"; + proxyWebSockets = true; + }; "~ ^/admin".return = 403; }; }; + "rss.depeuter.dev" = { + enableACME = true; + forceSSL = true; + locations."/".proxyPass = "http://192.168.92:${toString config.homelab.apps.freshrss.port}"; + }; }; }; }; diff --git a/hosts/Niko/default.nix b/hosts/Niko/default.nix index 57dbc27..910f325 100644 --- a/hosts/Niko/default.nix +++ b/hosts/Niko/default.nix @@ -7,7 +7,10 @@ ]; homelab = { - apps.technitiumDNS.enable = true; + apps = { + technitiumDNS.enable = true; + traefik.enable = true; + }; users.deploy.enable = true; }; @@ -34,12 +37,11 @@ hardware = { enableRedistributableFirmware = true; enableAllFirmware = true; - pulseaudio.enable = true; - opengl.enable = true; + graphics.enable = true; }; # Select internationalisation properties. - i18n.defaultLocale = "en_GB.utf8"; + i18n.defaultLocale = "en_GB.UTF-8"; networking = { hostName = "Niko"; @@ -79,6 +81,8 @@ user = config.users.users.jellyfin-mpv-shim.name; }; + pulseaudio.enable = true; + tailscale = { enable = true; useRoutingFeatures = "server"; @@ -94,8 +98,6 @@ # resolved.enable = true; }; - sound.enable = true; - # Define a user account. Don't forget to set a password with 'passwd'. users.users.jellyfin-mpv-shim = { description = "Jellyfin MPV Shim User"; @@ -114,67 +116,4 @@ systemd.services."cage-tty1".serviceConfig.Restart = "always"; 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; - }; - }; - }; - }; } diff --git a/hosts/Production/default.nix b/hosts/Production/default.nix new file mode 100644 index 0000000..9bb565d --- /dev/null +++ b/hosts/Production/default.nix @@ -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"; + }; +} diff --git a/hosts/ProductionArr/default.nix b/hosts/ProductionArr/default.nix new file mode 100644 index 0000000..ff4f4c2 --- /dev/null +++ b/hosts/ProductionArr/default.nix @@ -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"; + }; +} diff --git a/hosts/ProductionGPU/default.nix b/hosts/ProductionGPU/default.nix index 75e48e7..fa9ca8c 100644 --- a/hosts/ProductionGPU/default.nix +++ b/hosts/ProductionGPU/default.nix @@ -17,7 +17,7 @@ defaultGateway = { address = "192.168.0.1"; - interface = "enp6s18"; + interface = "ens18"; }; # Open ports in the firewall. @@ -25,7 +25,7 @@ enable = true; }; - interfaces.enp6s18 = { + interfaces.ens18 = { ipv4.addresses = [ { address = "192.168.0.94"; @@ -40,7 +40,7 @@ ]; }; - system.stateVersion = "unstable"; + system.stateVersion = "24.11"; ### Nvidia GPU support ### @@ -64,7 +64,7 @@ }; hardware = { - opengl = { + graphics = { enable = true; # driSupport = true; # driSupport32Bit = true; diff --git a/hosts/Testing/default.nix b/hosts/Testing/default.nix index 78abc58..cc353f6 100644 --- a/hosts/Testing/default.nix +++ b/hosts/Testing/default.nix @@ -3,10 +3,11 @@ { config = { homelab = { - virtualisation = { - containers.enable = true; - guest.enable = true; + apps = { + freshrss.enable = true; + traefik.enable = true; }; + virtualisation.guest.enable = true; }; networking = { diff --git a/hosts/Vaultwarden/default.nix b/hosts/Vaultwarden/default.nix index 9f98d84..d8115bc 100644 --- a/hosts/Vaultwarden/default.nix +++ b/hosts/Vaultwarden/default.nix @@ -3,7 +3,11 @@ { config = { homelab = { - apps.vaultwarden.enable = true; + apps.vaultwarden = { + enable = true; + domain = "https://vault.depeuter.dev"; + name = "Hugo's Vault"; + }; virtualisation.guest.enable = true; }; diff --git a/modules/apps/arr/default.nix b/modules/apps/arr/default.nix index 8be935f..7b530c3 100644 --- a/modules/apps/arr/default.nix +++ b/modules/apps/arr/default.nix @@ -4,61 +4,86 @@ let cfg = config.homelab.apps.arr; 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; PGID = toString config.users.groups.media.gid; UMASK = "002"; 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"; + 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"; - lidarr.enable = lib.mkEnableOption "Lidarr using Docker"; - prowlarr.enable = lib.mkEnableOption "Prowlarr using Docker"; - qbittorrent.enable = lib.mkEnableOption "qBittorrent using Docker"; - radarr.enable = lib.mkEnableOption "Radarr using Docker"; - sonarr.enable = lib.mkEnableOption "Sonarr using Docker"; + bazarr = mkAppOption "Bazarr"; + prowlarr = mkAppOption "Prowlarr"; + qbittorrent = mkAppOption "qBittorrent"; + radarr = mkAppOption "Radarr"; + sonarr = mkAppOption "Sonarr"; }; config = { homelab = { - users.media.enable = lib.mkIf inUse true; + users = lib.mkIf inUse { + apps.enable = true; + media.enable = true; + }; # "Master switch": Enable all apps. - apps.arr = { - bazarr.enable = lib.mkIf cfg.enable true; - lidarr.enable = lib.mkIf cfg.enable true; - prowlarr.enable = lib.mkIf cfg.enable true; - qbittorrent.enable = lib.mkIf cfg.enable true; - radarr.enable = lib.mkIf cfg.enable true; - sonarr.enable = lib.mkIf cfg.enable true; + apps.arr = lib.mkIf cfg.enable { + bazarr.enable = true; + prowlarr.enable = true; + qbittorrent.enable = true; + radarr.enable = true; + sonarr.enable = true; }; + + fileSystems.media.video = { + enable = true; + permissions = [ "read" "write" ]; + }; + + virtualisation.containers.enable = lib.mkIf inUse true; }; - fileSystems = lib.mkIf inUse { - "/srv/video" = { - device = "192.168.0.11:/mnt/SMALL/MEDIA/VIDEO"; + fileSystems = let + mkFileSystem = device: { + inherit device; fsType = "nfs"; options = [ "rw" + "auto" "nfsvers=4.2" - "async" "soft" - "timeo=100" "retry=50" "actimeo=1800" "lookupcache=all" - "nosuid" "tcp" + "rsize=1048576" "wsize=1048576" + "hard" + "timeo=600" "retrans=2" + "_netdev" "nosuid" "tcp" ]; }; - "/srv/qbittorrent" = { - device = "192.168.0.11:/mnt/SMALL/CONFIG/QBITTORRENT/qBittorrent"; - fsType = "nfs"; - options = [ - "rw" - "nfsvers=4.2" - "async" "soft" - "nosuid" "tcp" - ]; - }; + hugoBackup = "192.168.0.11:/mnt/BIG/BACKUP"; + in lib.mkIf inUse { + "/srv/bazarr-backup" = lib.mkIf cfg.bazarr.enable (mkFileSystem "${hugoBackup}/BAZARR"); + "/srv/prowlarr-backup" = lib.mkIf cfg.bazarr.enable (mkFileSystem "${hugoBackup}/PROWLARR"); + "/srv/qbittorrent" = lib.mkIf cfg.qbittorrent.enable (mkFileSystem "192.168.0.11:/mnt/SMALL/CONFIG/QBITTORRENT"); + "/srv/radarr-backup" = lib.mkIf cfg.radarr.enable (mkFileSystem "${hugoBackup}/RADARR"); + "/srv/sonarr-backup" = lib.mkIf cfg.sonarr.enable (mkFileSystem "${hugoBackup}/SONARR"); + "/srv/torrent" = mkFileSystem "192.168.0.11:/mnt/SMALL/MEDIA/TORRENT"; }; # Make sure the Docker network exists. @@ -66,7 +91,6 @@ in { description = "Create Docker network for ${networkName}"; requiredBy = [ "docker-bazarr.service" - "docker-lidarr.service" "docker-prowlarr.service" "docker-qbittorrent.service" "docker-radarr.service" @@ -84,109 +108,77 @@ in { }; # Create a user for each app. - users.users = { - bazarr = lib.mkIf cfg.bazarr.enable { - uid = lib.mkForce 3003; + users.users = let + mkUser = uid: { + uid = lib.mkForce uid; isSystemUser = true; group = config.users.groups.media.name; home = "/var/empty"; shell = null; }; - lidarr = lib.mkIf cfg.lidarr.enable { - uid = lib.mkForce 3002; - isSystemUser = true; - group = config.users.groups.media.name; - home = "/var/empty"; - shell = null; - }; - 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; + in { + bazarr = lib.mkIf cfg.bazarr.enable (mkUser 3003); + prowlarr = lib.mkIf cfg.prowlarr.enable (mkUser 3004); + qbittorrent = lib.mkIf cfg.qbittorrent.enable (mkUser 3005) // { + extraGroups = [ + config.users.groups.apps.name + ]; }; + radarr = lib.mkIf cfg.radarr.enable (mkUser 3006); + sonarr = lib.mkIf cfg.sonarr.enable (mkUser 3007); }; - virtualisation.oci-containers.containers = { - bazarr = lib.mkIf cfg.bazarr.enable { + virtualisation.oci-containers.containers = let + videoHostPath = config.homelab.fileSystems.media.video.hostPath; + in { + bazarr = let + port = 6767; + in lib.mkIf cfg.bazarr.enable { hostname = "bazarr"; - image = "ghcr.io/hotio/bazarr:release-1.4.4"; + image = "ghcr.io/hotio/bazarr:release-1.5.2"; autoStart = true; - ports = [ - "6767:6767/tcp" - "6767:6767/udp" + ports = lib.mkIf cfg.bazarr.exposePorts [ + "${toString port}:${toString port}/tcp" + "${toString port}:${toString port}/udp" ]; extraOptions = [ "--network=${networkName}" - - "--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"'' + "--network=${proxyNet}" ]; environment = { PUID = toString config.users.users.bazarr.uid; inherit PGID UMASK; TZ = config.time.timeZone; - WEBUI_PORTS = "6767/tcp,6767/udp"; + WEBUI_PORTS = "${toString port}/tcp,${toString port}/udp"; }; volumes = [ "bazarr-config:/config" - "/srv/video:/data" - ]; - }; - lidarr = lib.mkIf cfg.lidarr.enable { - hostname = "lidarr"; - image = "ghcr.io/hotio/lidarr:release-2.5.3.4341"; - autoStart = true; - ports = [ - "8686:8686/tcp" - ]; - extraOptions = [ - "--network=${networkName}" + "/srv/bazarr-backup:/config/backup" - "--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"'' + "${videoHostPath}/Films:/media/movies" + "${videoHostPath}/Series:/media/series" ]; - environment = { - PUID = toString config.users.users.lidarr.uid; - inherit PGID UMASK; - TZ = config.time.timeZone; + 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; }; - volumes = [ - "lidarr-config:/config" - # TODO "data:/data" - ]; }; - prowlarr = lib.mkIf cfg.prowlarr.enable { + prowlarr = let + port = 9696; + in lib.mkIf cfg.prowlarr.enable { hostname = "prowlarr"; - image = "ghcr.io/hotio/prowlarr:release-1.23.1.4708"; + image = "ghcr.io/hotio/prowlarr:release-2.0.5.5160"; autoStart = true; - ports = [ - "9696:9696/tcp" + ports = lib.mkIf cfg.prowlarr.exposePorts [ + "${toString port}:${toString port}/tcp" ]; extraOptions = [ "--network=${networkName}" + "--network=${proxyNet}" ]; environment = { PUID = toString config.users.users.prowlarr.uid; @@ -194,44 +186,63 @@ in { TZ = config.time.timeZone; }; 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"; - image = "ghcr.io/hotio/qbittorrent:release-4.6.7"; + image = "ghcr.io/hotio/qbittorrent:release-5.1.2"; autoStart = true; - ports = [ - "10095:10095/udp" - "10095:10095/tcp" + ports = lib.mkIf cfg.qbittorrent.exposePorts [ + "${toString port}:${toString port}/tcp" + "${toString port}:${toString port}/udp" ]; extraOptions = [ "--network=${networkName}" - - "--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"'' + "--network=${proxyNet}" ]; environment = { PUID = toString config.users.users.qbittorrent.uid; inherit PGID UMASK; TZ = config.time.timeZone; - WEBUI_PORTS = "10095/tcp,10095/udp"; + WEBUI_PORTS = "${toString port}/tcp,${toString port}/udp"; }; volumes = [ - "/srv/qbittorrent:/config/config" - "/srv/video:/media/video" + "/srv/qbittorrent:/config" + + "/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"; - image = "ghcr.io/hotio/radarr:release-5.9.1.9070"; + image = "ghcr.io/hotio/radarr:testing-5.28.0.10205"; autoStart = true; - ports = [ - "7878:7878/tcp" + ports = lib.mkIf cfg.radarr.exposePorts [ + "${toString port}:${toString port}/tcp" ]; extraOptions = [ "--network=${networkName}" + "--network=${proxyNet}" ]; environment = { PUID = toString config.users.users.radarr.uid; @@ -239,20 +250,33 @@ in { TZ = config.time.timeZone; }; volumes = [ - # TODO "config:/config" - # TODO "data:/data" + "radarr-config:/config" + + "/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"; - image = "ghcr.io/hotio/sonarr:release-4.0.9.2244"; + image = "ghcr.io/hotio/sonarr:release-4.0.15.2941"; autoStart = true; - ports = [ - "8989:8989/tcp" + ports = lib.mkIf cfg.sonarr.exposePorts [ + "${toString port}:${toString port}/tcp" ]; extraOptions = [ "--network=${networkName}" + "--network=${proxyNet}" ]; environment = { PUID = toString config.users.users.sonarr.uid; @@ -260,9 +284,19 @@ in { TZ = config.time.timeZone; }; volumes = [ - # TODO "config:/config" - # TODO "data:/data" + "sonarr-config:/config" + + "/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; + }; }; }; }; diff --git a/modules/apps/bind9/db.depeuter.dev b/modules/apps/bind9/db.depeuter.dev new file mode 100644 index 0000000..72f3825 --- /dev/null +++ b/modules/apps/bind9/db.depeuter.dev @@ -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 diff --git a/modules/apps/bind9/default.nix b/modules/apps/bind9/default.nix new file mode 100644 index 0000000..a2346c1 --- /dev/null +++ b/modules/apps/bind9/default.nix @@ -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 = { + }; + }; + }; +} diff --git a/modules/apps/bind9/named.conf b/modules/apps/bind9/named.conf new file mode 100644 index 0000000..d301bd7 --- /dev/null +++ b/modules/apps/bind9/named.conf @@ -0,0 +1,2 @@ +include "/etc/bind/named.conf.options"; +include "/etc/bind/named.conf.local"; diff --git a/modules/apps/bind9/named.conf.local b/modules/apps/bind9/named.conf.local new file mode 100644 index 0000000..442eca9 --- /dev/null +++ b/modules/apps/bind9/named.conf.local @@ -0,0 +1,4 @@ +zone "depeuter.dev" { + type primary; + file "/etc/bind/zones/db.depeuter.dev"; +}; diff --git a/modules/apps/bind9/named.conf.options b/modules/apps/bind9/named.conf.options new file mode 100644 index 0000000..b05f4bf --- /dev/null +++ b/modules/apps/bind9/named.conf.options @@ -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; }; +}; diff --git a/modules/apps/calibre/default.nix b/modules/apps/calibre/default.nix index 6fddb81..bddf5c8 100644 --- a/modules/apps/calibre/default.nix +++ b/modules/apps/calibre/default.nix @@ -1,17 +1,189 @@ -{ config, lib, ... }: +{ config, lib, pkgs, ... }: let cfg = config.homelab.apps.calibre; -in { - options.homelab.apps.calibre.enable = lib.mkEnableOption "Calibre"; - config = lib.mkIf cfg.enable { - users.users.calibre = { - uid = lib.mkForce 3010; - isSystemUser = true; - group = config.users.groups.media.name; - home = "/var/empty"; - shell = null; - }; + 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 = { + uid = lib.mkForce 3010; + isSystemUser = true; + group = config.users.groups.media.name; + home = "/var/empty"; + 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; + }; + }; + }) + ]; } diff --git a/modules/apps/changedetection/default.nix b/modules/apps/changedetection/default.nix new file mode 100644 index 0000000..ee88751 --- /dev/null +++ b/modules/apps/changedetection/default.nix @@ -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"; + }; + }; + }; +} diff --git a/modules/apps/default.nix b/modules/apps/default.nix index 268476e..7c8b8f8 100644 --- a/modules/apps/default.nix +++ b/modules/apps/default.nix @@ -1,12 +1,16 @@ { imports = [ ./arr + ./bind9 ./calibre + ./changedetection + ./freshrss ./gitea ./jellyfin ./plex ./speedtest ./technitium-dns + ./traefik ./vaultwarden ]; } diff --git a/modules/apps/freshrss/default.nix b/modules/apps/freshrss/default.nix new file mode 100644 index 0000000..4f4456f --- /dev/null +++ b/modules/apps/freshrss/default.nix @@ -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`)"; + }; + }; + }; +} diff --git a/modules/apps/gitea/default.nix b/modules/apps/gitea/default.nix index 7a99a55..0361bd5 100644 --- a/modules/apps/gitea/default.nix +++ b/modules/apps/gitea/default.nix @@ -124,7 +124,7 @@ in { gitea = { hostname = "gitea"; - image = "codeberg.org/forgejo/forgejo:8.0.3-rootless"; + image = "codeberg.org/forgejo/forgejo:11.0.1-rootless"; autoStart = true; user = "${toString UID}:${toString GID}"; ports = [ @@ -379,6 +379,25 @@ in { # A relative path is interpreted as _`AppWorkPath`_/%(ROOT)s 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. FORGEJO__repository__DISABLE_STARS = "true"; # Disable repository forking. @@ -557,7 +576,7 @@ in { #FORGEJO__picture__AVATAR_RENDERED_SIZE_FACTOR = "2"; # Maximum allowed file size for uploaded avatars. # 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. #FORGEJO__picture__AVATAR_MAX_ORIGIN_SIZE = "262144"; # Chinese users can choose "duoshuo" diff --git a/modules/apps/jellyfin/default.nix b/modules/apps/jellyfin/default.nix index 9a1bc14..011f56b 100644 --- a/modules/apps/jellyfin/default.nix +++ b/modules/apps/jellyfin/default.nix @@ -4,6 +4,7 @@ let cfg = config.homelab.apps.jellyfin; networkName = "jellyfin"; + inherit (config.homelab.fileSystems) media; UID = 3008; GID = config.users.groups.media.gid; @@ -12,6 +13,11 @@ in { config = lib.mkIf cfg.enable { homelab = { + fileSystems.media.video = { + enable = true; + permissions = [ "read" ]; + }; + users = { apps.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" = { device = "192.168.0.11:/mnt/BIG/MEDIA/HOMEVIDEO/ARCHIVE"; fsType = "nfs"; @@ -101,7 +95,7 @@ in { virtualisation.oci-containers.containers = { jellyfin = { hostname = "jellyfin"; - image = "jellyfin/jellyfin:10.10.0"; + image = "jellyfin/jellyfin:10.10.7"; user = "${toString UID}:${toString GID}"; autoStart = true; ports = [ @@ -117,7 +111,7 @@ in { "cache:/cache" "/srv/audio:/media/audio" - "/srv/video:/media/video" + "${media.video.hostPath}:/media/video" "/srv/homevideo:/media/homevideo" "/srv/photo:/media/photo" ]; @@ -126,11 +120,28 @@ in { }; }; - feishin = { - hostname = "feishin"; - image = "ghcr.io/jeffvli/feishin:0.7.1"; + jellyfin-vue = { + hostname = "jellyfin-vue"; + image = "ghcr.io/jellyfin/jellyfin-vue:unstable"; + autoStart = true; 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 = [ "--network=${networkName}" @@ -147,8 +158,11 @@ in { TZ = config.time.timeZone; }; 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; }; }; }; diff --git a/modules/apps/plex/default.nix b/modules/apps/plex/default.nix index 251f9dd..b307b86 100644 --- a/modules/apps/plex/default.nix +++ b/modules/apps/plex/default.nix @@ -6,44 +6,63 @@ in { options.homelab.apps.plex.enable = lib.mkEnableOption "Plex"; 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 = { uid = lib.mkForce 3009; isSystemUser = true; - group = config.users.groups.media; + group = config.users.groups.apps.name; + extraGroups = [ + config.users.groups.media.name + ]; home = "/var/empty"; shell = null; }; - virtualisation.oci-containers.containers = { - plex = { - hostname = "plex"; - image = "plexinc/pms-docker:1.41.0.8992-8463ad060"; - autoStart = true; - ports = [ - "32400:32400/tcp" # Plex Media Server - "1900:1900/udp" # Plex DLNA Server - "32469:32469/tcp" # Plex DLNA Server - "32410:32410/udp" # GDM network discovery - "32412:32412/udp" # GDM network discovery - "32413:32413/udp" # GDM network discovery - "32414:32414/udp" # GDM network discovery - # "8324:8324/tcp" # Controlling Plex for Roku via Plex Companion - ]; - environment = { - ADVERTISE_AP = "..."; # TODO Configure ip - ALLOWED_NETWORKS = "192.168.0.0/24,172.16.0.0/16"; - CHANGE_CONFIG_DIR_OWNERSHIP = "false"; - HOSTNAME = "PlexServer"; - PLEX_CLAIM = "..."; # TODO Add token - PLEX_UID = config.users.users.plex.uid; - PLEX_GID = config.users.groups.media.gid; - TZ = config.time.timeZone; - }; - volumes = [ - # TODO "config:/var/lib/plexmediaserver" - # TODO "transcode-temp:/transcode" - # TODO "media:/data" - ]; + virtualisation.oci-containers.containers.plex = let + videoHostPath = config.homelab.fileSystems.media.video.hostPath; + in { + hostname = "plex"; + image = "plexinc/pms-docker:1.41.6.9685-d301f511a"; + autoStart = true; + ports = [ + "32400:32400/tcp" # Plex Media Server + "1900:1900/udp" # Plex DLNA Server + "32469:32469/tcp" # Plex DLNA Server + "32410:32410/udp" # GDM network discovery + "32412:32412/udp" # GDM network discovery + "32413:32413/udp" # GDM network discovery + "32414:32414/udp" # GDM network discovery + # "8324:8324/tcp" # Controlling Plex for Roku via Plex Companion + ]; + environment = { + #ADVERTISE_AP = "..."; # TODO Configure ip + ALLOWED_NETWORKS = "192.168.0.0/24,172.16.0.0/16"; + CHANGE_CONFIG_DIR_OWNERSHIP = "false"; + HOSTNAME = "Hugo-Plex"; + PLEX_CLAIM = "claim-d5MqsjMeCZrUF6oUvssr"; + PLEX_UID = toString config.users.users.plex.uid; + PLEX_GID = toString config.users.groups.media.gid; + TZ = config.time.timeZone; + }; + volumes = [ + # TODO Backup over NFS + "plex-config:/config" + "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"; }; }; }; diff --git a/modules/apps/traefik/default.nix b/modules/apps/traefik/default.nix new file mode 100644 index 0000000..7f6ce38 --- /dev/null +++ b/modules/apps/traefik/default.nix @@ -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; + }; + }; + }; +} diff --git a/modules/apps/vaultwarden/default.nix b/modules/apps/vaultwarden/default.nix index 196beb4..4510299 100644 --- a/modules/apps/vaultwarden/default.nix +++ b/modules/apps/vaultwarden/default.nix @@ -5,7 +5,24 @@ let networkName = "vaultwarden"; 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 { homelab = { @@ -33,13 +50,16 @@ in { ''; }; - virtualisation.oci-containers.containers = { - vaultwarden-db = { - hostname = "vaultwarden-db"; + virtualisation.oci-containers.containers = let + dbHostname = "vaultwarden-db"; + dbPort = 5432; + in { + vaultwardenDb = { + hostname = dbHostname; image = "postgres:15.8-alpine"; autoStart = true; ports = [ - "5432:5432/tcp" + "${toString dbPort}:5432/tcp" ]; extraOptions = [ "--network=${networkName}" @@ -57,16 +77,16 @@ in { dataDir = "/data"; in { hostname = "vaultwarden"; - image = "vaultwarden/server:1.30.5-alpine"; + image = "vaultwarden/server:1.34.3-alpine"; autoStart = true; ports = [ - "10102:80/tcp" + "${toString cfg.port}:80/tcp" ]; extraOptions = [ "--network=${networkName}" ]; dependsOn = [ - "vaultwarden-db" + "vaultwardenDb" ]; volumes = [ "vaultwarden:${dataDir}" @@ -115,7 +135,7 @@ in { ## Details: ## - https://docs.diesel.rs/2.1.x/diesel/pg/struct.PgConnection.html ## - 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 ## Set to false to avoid enabling WAL during startup. @@ -244,7 +264,7 @@ in { ## For development # DOMAIN=http://localhost ## For public server - DOMAIN = "https://vault.depeuter.dev"; + DOMAIN = cfg.domain; ## For public server (URL with port number) # DOMAIN=https://vw.domain.tld:8443 ## 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! ## The welcome email will include a verification link, and login attempts will periodically ## 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 ## 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_ALLOWED=true ## 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, ## 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 SMTP_HOST = "smtp.gmail.com"; SMTP_FROM = "vault@depeuter.dev"; - SMTP_FROM_NAME = "Hugo's Vault"; + SMTP_FROM_NAME = cfg.name; # SMTP_USERNAME=username # SMTP_PASSWORD=password # SMTP_TIMEOUT=15 diff --git a/modules/default.nix b/modules/default.nix index 5d901bc..1a000c3 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -1,6 +1,7 @@ { imports = [ ./apps + ./fileSystems ./services ./virtualisation diff --git a/modules/fileSystems/default.nix b/modules/fileSystems/default.nix new file mode 100644 index 0000000..7c25689 --- /dev/null +++ b/modules/fileSystems/default.nix @@ -0,0 +1,5 @@ +{ + imports = [ + ./media + ]; +} diff --git a/modules/fileSystems/media/default.nix b/modules/fileSystems/media/default.nix new file mode 100644 index 0000000..41cb81f --- /dev/null +++ b/modules/fileSystems/media/default.nix @@ -0,0 +1,5 @@ +{ + imports = [ + ./video + ]; +} diff --git a/modules/fileSystems/media/video/default.nix b/modules/fileSystems/media/video/default.nix new file mode 100644 index 0000000..e46193c --- /dev/null +++ b/modules/fileSystems/media/video/default.nix @@ -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" + ]; + }; + }; +} diff --git a/modules/services/actions/default.nix b/modules/services/actions/default.nix index 338b963..ea6b025 100644 --- a/modules/services/actions/default.nix +++ b/modules/services/actions/default.nix @@ -44,6 +44,6 @@ in { ]; }; }; - }; } + diff --git a/secrets/secrets.yaml b/secrets/secrets.yaml new file mode 100644 index 0000000..e17dab1 --- /dev/null +++ b/secrets/secrets.yaml @@ -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 diff --git a/users/admin/default.nix b/users/admin/default.nix index 552909b..4038266 100644 --- a/users/admin/default.nix +++ b/users/admin/default.nix @@ -18,8 +18,8 @@ in { ]; initialPassword = "ChangeMe"; openssh.authorizedKeys.keys = [ - # TODO ChangeMe - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPrG+ldRBdCeHEXrsy/qHXIJYg8xQXVuiUR0DxhFjYNg" + # HomeLab > NixOS > admin > ssh + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGWIOOEqTy8cWKpENVbzD4p7bsQgQb/Dgpzk8i0dZ00T" ]; packages = with pkgs; [ curl