From 32849cc5d24c039872bc7c996436270a13df9f66 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Thu, 9 Jan 2025 22:25:00 +0100 Subject: [PATCH 01/44] Initial commit --- flake.lock | 81 +++ flake.nix | 51 ++ hosts/ACE/default.nix | 41 ++ hosts/Binnenpost/default.nix | 81 +++ hosts/Development/default.nix | 67 ++ hosts/Gitea/default.nix | 38 + hosts/Ingress/default.nix | 233 ++++++ hosts/Isabel/.keep | 0 hosts/Isabel/dashboard/config/bookmarks.yaml | 32 + hosts/Isabel/dashboard/config/services.yaml | 30 + hosts/Isabel/default.nix | 255 +++++++ hosts/Isabel/hardware-configuration.nix | 39 + hosts/Niko/default.nix | 180 +++++ hosts/Niko/hardware-configuration.nix | 53 ++ hosts/Production/default.nix | 45 ++ hosts/ProductionGPU/default.nix | 98 +++ hosts/Template/default.nix | 36 + hosts/Testing/default.nix | 46 ++ hosts/Vaultwarden/default.nix | 38 + modules/apps/arr/default.nix | 282 ++++++++ modules/apps/calibre/default.nix | 17 + modules/apps/changedetection/default.nix | 28 + modules/apps/default.nix | 14 + modules/apps/freshrss/default.nix | 76 ++ modules/apps/gitea/default.nix | 673 ++++++++++++++++++ modules/apps/jellyfin/default.nix | 175 +++++ modules/apps/plex/default.nix | 50 ++ modules/apps/speedtest/default.nix | 27 + modules/apps/technitium-dns/default.nix | 73 ++ modules/apps/vaultwarden/default.nix | 636 +++++++++++++++++ modules/common/default.nix | 16 + modules/default.nix | 9 + modules/services/actions/default.nix | 49 ++ modules/services/default.nix | 6 + modules/services/openssh/default.nix | 20 + modules/virtualisation/containers/default.nix | 23 + modules/virtualisation/default.nix | 6 + modules/virtualisation/guest/default.nix | 34 + users/admin/default.nix | 33 + users/apps/default.nix | 18 + users/backup/default.nix | 26 + users/default.nix | 9 + users/deploy/default.nix | 49 ++ users/media/default.nix | 18 + 44 files changed, 3811 insertions(+) create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 hosts/ACE/default.nix create mode 100644 hosts/Binnenpost/default.nix create mode 100644 hosts/Development/default.nix create mode 100644 hosts/Gitea/default.nix create mode 100644 hosts/Ingress/default.nix create mode 100644 hosts/Isabel/.keep create mode 100644 hosts/Isabel/dashboard/config/bookmarks.yaml create mode 100644 hosts/Isabel/dashboard/config/services.yaml create mode 100644 hosts/Isabel/default.nix create mode 100644 hosts/Isabel/hardware-configuration.nix create mode 100644 hosts/Niko/default.nix create mode 100644 hosts/Niko/hardware-configuration.nix create mode 100644 hosts/Production/default.nix create mode 100644 hosts/ProductionGPU/default.nix create mode 100644 hosts/Template/default.nix create mode 100644 hosts/Testing/default.nix create mode 100644 hosts/Vaultwarden/default.nix create mode 100644 modules/apps/arr/default.nix create mode 100644 modules/apps/calibre/default.nix create mode 100644 modules/apps/changedetection/default.nix create mode 100644 modules/apps/default.nix create mode 100644 modules/apps/freshrss/default.nix create mode 100644 modules/apps/gitea/default.nix create mode 100644 modules/apps/jellyfin/default.nix create mode 100644 modules/apps/plex/default.nix create mode 100644 modules/apps/speedtest/default.nix create mode 100644 modules/apps/technitium-dns/default.nix create mode 100644 modules/apps/vaultwarden/default.nix create mode 100644 modules/common/default.nix create mode 100644 modules/default.nix create mode 100644 modules/services/actions/default.nix create mode 100644 modules/services/default.nix create mode 100644 modules/services/openssh/default.nix create mode 100644 modules/virtualisation/containers/default.nix create mode 100644 modules/virtualisation/default.nix create mode 100644 modules/virtualisation/guest/default.nix create mode 100644 users/admin/default.nix create mode 100644 users/apps/default.nix create mode 100644 users/backup/default.nix create mode 100644 users/default.nix create mode 100644 users/deploy/default.nix create mode 100644 users/media/default.nix diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..e3284fd --- /dev/null +++ b/flake.lock @@ -0,0 +1,81 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1735291276, + "narHash": "sha256-NYVcA06+blsLG6wpAbSPTCyLvxD/92Hy4vlY9WxFI1M=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "634fd46801442d760e09493a794c4f15db2d0cbb", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "ref": "nixos-unstable", + "type": "indirect" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "utils": "utils" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "utils": { + "inputs": { + "flake-utils": [ + "flake-utils" + ] + }, + "locked": { + "lastModified": 1722363685, + "narHash": "sha256-XCf2PIAT6lH7BwytgioPmVf/wkzXjSKScC4KzcZgb64=", + "owner": "gytis-ivaskevicius", + "repo": "flake-utils-plus", + "rev": "6b10f51ff73a66bb29f3bc8151a59d217713f496", + "type": "github" + }, + "original": { + "owner": "gytis-ivaskevicius", + "repo": "flake-utils-plus", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..48c91de --- /dev/null +++ b/flake.nix @@ -0,0 +1,51 @@ +{ + description = "Homelab configuration using flakes"; + + inputs = { + nixpkgs.url = "nixpkgs/nixos-unstable"; + + flake-utils.url = "github:numtide/flake-utils"; + utils = { + url = "github:gytis-ivaskevicius/flake-utils-plus"; + inputs.flake-utils.follows = "flake-utils"; + }; + }; + + outputs = inputs@{ + self, nixpkgs, + flake-utils, utils, + ... + }: + let + system = "x86_64-linux"; + in + utils.lib.mkFlake { + inherit self inputs; + + hostDefaults = { + inherit system; + + modules = [ + ./modules + ./users + ]; + }; + + hosts = { + Niko.modules = [ ./hosts/Niko ]; + + Ingress.modules = [ ./hosts/Ingress ]; + Gitea.modules = [ ./hosts/Gitea ]; + Vaultwarden.modules = [ ./hosts/Vaultwarden ]; + + Binnenpost.modules = [ ./hosts/Binnenpost ]; + Production.modules = [ ./hosts/Production ]; + ProductionGPU.modules = [ ./hosts/ProductionGPU ]; + ACE.modules = [ ./hosts/ACE ]; + + Template.modules = [ ./hosts/Template ]; + Development.modules = [ ./hosts/Development ]; + Testing.modules = [ ./hosts/Testing ]; + }; + }; +} diff --git a/hosts/ACE/default.nix b/hosts/ACE/default.nix new file mode 100644 index 0000000..04aa284 --- /dev/null +++ b/hosts/ACE/default.nix @@ -0,0 +1,41 @@ +{ pkgs, ... }: + +{ + config = { + homelab = { + services.actions.enable = true; + virtualisation.guest.enable = true; + }; + + networking = { + hostName = "ACE"; + hostId = "aaaa4100"; + domain = "depeuter.dev"; + + useDHCP = false; + + enableIPv6 = true; + + defaultGateway = { + address = "192.168.0.1"; + interface = "ens18"; + }; + + interfaces.ens18 = { + ipv4.addresses = [ + { + address = "192.168.0.41"; + prefixLength = 24; + } + ]; + }; + + nameservers = [ + "1.1.1.1" # Cloudflare + "1.0.0.1" # Cloudflare + ]; + }; + + system.stateVersion = "24.05"; + }; +} diff --git a/hosts/Binnenpost/default.nix b/hosts/Binnenpost/default.nix new file mode 100644 index 0000000..d78e2da --- /dev/null +++ b/hosts/Binnenpost/default.nix @@ -0,0 +1,81 @@ +{ pkgs, ... }: + +{ + config = { + environment = { + etc."nixos/tailscale-authkey".text = '' + tskey-auth-k1tfJLTnGB11CNTRL-HhnegtfNzQ3G8h71SC2DR38PFXwseQiu + ''; + + systemPackages = with pkgs; [ + ethtool + ]; + }; + + homelab = { + apps = { + speedtest.enable = true; + technitiumDNS.enable = true; + }; + virtualisation.guest.enable = true; + }; + + networking = { + hostName = "Binnenpost"; + hostId = "aaaa1001"; + domain = "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.89"; + prefixLength = 24; + } + ]; + }; + + nameservers = [ + "1.1.1.1" # Cloudflare + "1.0.0.1" # Cloudflare + ]; + }; + + services = { + networkd-dispatcher = { + enable = true; + rules."50-tailscale" = { + onState = ["routable"]; + script = '' + ${pkgs.ethtool}/bin/ethtool -K ens18 rx-udp-gro-forwarding on rx-gro-list off + ''; + }; + }; + + tailscale = { + enable = true; + useRoutingFeatures = "server"; + authKeyFile = "/etc/nixos/tailscale-authkey"; + extraUpFlags = [ + "--advertise-routes=192.168.0.0/24" + "--exit-node" + ]; + }; + }; + + system.stateVersion = "24.05"; + }; +} diff --git a/hosts/Development/default.nix b/hosts/Development/default.nix new file mode 100644 index 0000000..da995f8 --- /dev/null +++ b/hosts/Development/default.nix @@ -0,0 +1,67 @@ +{ config, pkgs, lib, system, ... }: + +{ + config = { + homelab = { + apps = { + arr = { + qbittorrent.enable = true; + }; + }; + virtualisation.guest.enable = true; + }; + + networking = { + hostId = "aaaa9100"; + 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.91"; + prefixLength = 24; + } + ]; + }; + + nameservers = [ + "1.1.1.1" # Cloudflare + "1.0.0.1" # Cloudflare + ]; + }; + + system.stateVersion = "24.05"; + + virtualisation.oci-containers.containers = { + pgadmin = { + image = "dpage/pgadmin4:8.11.0"; + ports = [ + "30056:80/tcp" + ]; + environment = { + # NOTE Required + # The email address used when setting up the initial administrator account to login to pgAdmin. + PGADMIN_DEFAULT_EMAIL = "kmtl.hugo+pgadmin@gmail.com"; + # NOTE Required + # The password used when setting up the initial administrator account to login to pgAdmin. + PGADMIN_DEFAULT_PASSWORD = "ChangeMe"; + }; + autoStart = true; + }; + }; + }; +} diff --git a/hosts/Gitea/default.nix b/hosts/Gitea/default.nix new file mode 100644 index 0000000..5b2492f --- /dev/null +++ b/hosts/Gitea/default.nix @@ -0,0 +1,38 @@ +{ config, pkgs, lib, system, ... }: + +{ + config = { + homelab = { + apps.gitea.enable = true; + virtualisation.guest.enable = true; + }; + + networking = { + hostId = "aaaa1500"; + domain = "depeuter.dev"; + + enableIPv6 = true; + + useDHCP = false; + defaultGateway = { + address = "192.168.0.1"; + interface = "ens18"; + }; + interfaces.ens18 = { + ipv4.addresses = [ + { + address = "192.168.0.24"; + prefixLength = 24; + } + ]; + }; + + nameservers = [ + "1.1.1.1" # Cloudflare + "1.0.0.1" # Cloudflare + ]; + }; + + system.stateVersion = "24.05"; + }; +} diff --git a/hosts/Ingress/default.nix b/hosts/Ingress/default.nix new file mode 100644 index 0000000..63e3ced --- /dev/null +++ b/hosts/Ingress/default.nix @@ -0,0 +1,233 @@ +{ config, pkgs, modulesPath, lib, system, ... }: + +{ + config = { + homelab.virtualisation.guest.enable = true; + + networking = { + hostName = "Ingress"; + hostId = "aaaa1000"; + domain = "depeuter.dev"; + + enableIPv6 = true; + + useDHCP = false; + defaultGateway = { + address = "192.168.0.1"; + interface = "ens18"; + }; + interfaces.ens18 = { + ipv4.addresses = [ + { + address = "192.168.0.10"; +prefixLength = 24; + } + ]; + }; + + nameservers = [ + "1.1.1.1" # Cloudflare + "1.0.0.1" # Cloudflare + ]; + + firewall = { + enable = true; + allowedTCPPorts = [ + 80 # HTTP + 443 # HTTPS + ]; + }; + }; + + security.acme = { + acceptTerms = true; + defaults = { + inherit (config.services.nginx) group; + dnsPropagationCheck = true; + dnsProvider = "cloudflare"; + dnsResolver = "1.1.1.1:53"; + email = "tibo.depeuter@telenet.be"; + credentialFiles = { + CLOUDFLARE_DNS_API_TOKEN_FILE = "/var/lib/secrets/depeuter-dev-cloudflare-api-token"; + }; + reloadServices = [ "nginx" ]; + }; + certs = { + "depeuter.dev" = { + domain = "depeuter.dev"; + extraDomainNames = [ "*.depeuter.dev" ]; + }; + "cloud.depeuter.dev" = { }; + "git.depeuter.dev" = { }; + "jelly.depeuter.dev" = { }; + "vault.depeuter.dev" = { }; + }; + }; + + # List services that you want to enable. + services = { + # Enable Nginx as a reverse proxy + nginx = { + enable = true; + + # Use recommended settings + # recommendedGzipSettings = true; + # recommendedOptimisation = true; + # recommendedProxySettings = true; + # recommendedTlsSettings = true; + + # Only allow PFS-enabled ciphers with AES256 + sslCiphers = "AES256+EECDH:AES256+EDH:!aNULL"; + + upstreams.docservice.servers."192.168.0.14:8080" = {}; + + appendHttpConfig = '' + map $http_x_forwarded_proto $the_scheme { + default $http_x_forwarded_proto; + "" $scheme; + } + + map $http_x_forwarded_host $the_host { + default $http_x_forwarded_host; + "" $host; + } + + map $http_upgrade $proxy_connection { + default upgrade; + "" close; + } + ''; + + # Define hosts + virtualHosts = { + # Disable automatic routing. + "default" = { + locations."/".return = "301 https://youtu.be/dQw4w9WgXcQ"; + default = true; + }; + + "cloud.depeuter.dev" = { + enableACME = true; + forceSSL = true; + locations = { + "/" = { + proxyPass = "http://192.168.0.14"; + extraConfig = '' + add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always; + fastcgi_request_buffering off; + ''; + }; + "/office/" = { + proxyPass = "http://192.168.0.14:8080/"; + priority = 500; + recommendedProxySettings = false; + extraConfig = '' + proxy_http_version 1.1; + ''; + }; + }; + extraConfig = '' + client_max_body_size 10G; + + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $proxy_connection; + proxy_set_header X-Forwarded-Host $the_host/office; + proxy_set_header X-Forwarded-Proto $the_scheme; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + ''; + }; + "calendar.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"; + + "jelly.depeuter.dev" = { + enableACME = true; + forceSSL = true; + locations = { + "/" = { + proxyPass = "http://192.168.0.94:8096"; + extraConfig = '' + # Proxy main Jellyfin traffic + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Protocol $scheme; + proxy_set_header X-Forwarded-Host $http_host; + + # Disable buffering when the nginx proxy gets very resource heavy upon streaming + proxy_buffering off; + ''; + }; + "/socket" = { + proxyPass = "http://192.168.0.91:8096"; + extraConfig = '' + # Proxy Jellyfin Websockets traffic + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Protocol $scheme; + proxy_set_header X-Forwarded-Host $http_host; + ''; + }; + }; + extraConfig = '' + client_max_body_size 20M; + + # Security / XSS Mitigation Headers + # NOTE: X-Frame-Options may cause issues with the webOS app + add_header X-Frame-Options "SAMEORIGIN"; + add_header X-Content-Type-Options "nosniff"; + + # Permissions policy. May cause issues with some clients + add_header Permissions-Policy "accelerometer=(), ambient-light-sensor=(), battery=(), bluetooth=(), camera=(), clipboard-read=(), display-capture=(), document-domain=(), encrypted-media=(), gamepad=(), geolocation=(), gyroscope=(), hid=(), idle-detection=(), interest-cohort=(), keyboard-map=(), local-fonts=(), magnetometer=(), microphone=(), payment=(), publickey-credentials-get=(), serial=(), sync-xhr=(), usb=(), xr-spatial-tracking=()" always; + + # Content Security Policy + # See: https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP + # Enforces https content and restricts JS/CSS to origin + # External Javascript (such as cast_sender.js for Chromecast) must be whitelisted. + # NOTE: The default CSP headers may cause issues with the webOS app + add_header Content-Security-Policy "default-src https: data: blob: ; img-src 'self' https://* ; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' https://www.gstatic.com https://www.youtube.com blob:; worker-src 'self' blob:; connect-src 'self'; object-src 'none'; frame-ancestors 'self'"; + ''; + }; + "git.depeuter.dev" = { + enableACME = true; + forceSSL = true; + locations."/".proxyPass = "http://192.168.0.24:3000"; + extraConfig = '' + proxy_set_header Connection $http_connection; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + client_max_body_size 512M; + 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 + proxy_busy_buffers_size 256k; # Max size of busy buffers + proxy_http_version 1.1; + proxy_read_timeout 600s; + proxy_temp_file_write_size 256k; # Size of temp file for large responses + ''; + }; + "vault.depeuter.dev" = { + enableACME = true; + forceSSL = true; + locations = { + "/".proxyPass = "http://192.168.0.22:10102"; + "~ ^/admin".return = 403; + }; + }; + }; + }; + }; + + system.stateVersion = "24.05"; + }; +} diff --git a/hosts/Isabel/.keep b/hosts/Isabel/.keep new file mode 100644 index 0000000..e69de29 diff --git a/hosts/Isabel/dashboard/config/bookmarks.yaml b/hosts/Isabel/dashboard/config/bookmarks.yaml new file mode 100644 index 0000000..ac0566a --- /dev/null +++ b/hosts/Isabel/dashboard/config/bookmarks.yaml @@ -0,0 +1,32 @@ +- Office: + - Zoho Mail: + - icon: zohomail + href: https://mail.zoho.eu +- Network: + - Cloudlfare: + - icon: cloudflare + href: https://dash.cloudflare.com + - Pulsetic: + - href: https://status.depeuter.dev + icon: https://pulsetic.com/favicon-196x196.png + - Telenet Internet usage: + - icon: https://static.telenet.be/assets/favicon/favicon.ico + href: https://www2.telenet.be/nl/klantenservice/raadpleeg-je-internetverbruik/ + - Telenet Modem: + - icon: https://static.telenet.be/assets/favicon/favicon.ico + # href: https://mijn.telenet.be/mijntelenet/rgw/settings.do?identifier=u381160&action=showAdvancedSettings + href: https://www2.telenet.be/residential/nl/mijn-telenet/je-thuisnetwerk#/mainnavitem=hgw/mainnavitemid=item-1/subnavitem=modem_general + - TransIP: + - icon: https://www.transip.eu/cache-60c9b25f/img/transip-new/favicons/favicon.png + href: https://www.transip.eu/cp/ +- Homemade: + - AI-Transparency: + - href: https://ai-transparency.depeuter.dev + icon: https://ai-transparency.depeuter.dev/img/transparency.png + - Down-message: + - href: https://down.depeuter.dev + icon: https://down.depeuter.dev/assets/icon.jpg + - Portfolio: + - href: https://tibo.depeuter.dev + icon: https://tibo.depeuter.dev/assets/owl_circuit.png + diff --git a/hosts/Isabel/dashboard/config/services.yaml b/hosts/Isabel/dashboard/config/services.yaml new file mode 100644 index 0000000..e944db1 --- /dev/null +++ b/hosts/Isabel/dashboard/config/services.yaml @@ -0,0 +1,30 @@ +- Networking: + - AXE5400 Tri-Band Wi-Fi 6E Router: + description: Router + href: https://tplinkwifi.net + ping: http://192.168.0.1 + icon: tp-link + - Traefik Isabel: + description: Reverse proxy manager + href: https://traefik.isabel.depeuter.dev/dashboard/# + ping: https://traefik.isabel.depeuter.dev/dashboard/# + icon: traefik + widget: + type: traefik + url: https://traefik.isabel.depeuter.dev + - Traefik Niko: + description: Reverse proxy manager + href: https://traefik.niko.depeuter.dev/dashboard/# + ping: https://traefik.niko.depeuter.dev/dashboard/# + - Technitium DNS Isabel: + description: DNS server + href: https://dns.Isabel.depeuter.dev + ping: http://192.168.0.13:53 + icon: technitium + - Technitium DNS Niko: + description: DNS server + href: https://dns.niko.depeuter.dev + ping: http://192.168.0.30:53 + icon: technitium + + diff --git a/hosts/Isabel/default.nix b/hosts/Isabel/default.nix new file mode 100644 index 0000000..0a1f50f --- /dev/null +++ b/hosts/Isabel/default.nix @@ -0,0 +1,255 @@ +{ config, pkgs, ... }: + +{ + imports = [ + # Include the results of the hardware scan. + ./hardware-configuration.nix + ]; + + # Use the systemd-boot EFI boot loader. + boot.loader = { + systemd-boot.enable = true; + efi = { + canTouchEfiVariables = true; + efiSysMountPoint = "/boot/efi"; + }; + }; + + console = { + font = "Lat2-Terminus16"; + keyMap = "us"; + }; + + # List packages installed in the system profile. To search, run: + # $ nix search wget + environment.systemPackages = with pkgs; [ + ]; + + environment.etc = { + "homepage/bookmarks.yaml".text = '' +- Office: + - Zoho Mail: + - icon: zohomail + href: https://mail.zoho.eu +- Network: + - Cloudlfare: + - icon: cloudflare + href: https://dash.cloudflare.com + - TransIP: + - icon: https://www.transip.eu/cache-60c9b25f/img/transip-new/favicons/favicon.png + href: https://www.transip.eu/cp/ + - Telenet Internet usage: + - icon: https://static.telenet.be/assets/favicon/favicon.ico + href: https://www2.telenet.be/nl/klantenservice/raadpleeg-je-internetverbruik/ + - Telenet Modem: + - icon: https://static.telenet.be/assets/favicon/favicon.ico + # href: https://mijn.telenet.be/mijntelenet/rgw/settings.do?identifier=u381160&action=showAdvancedSettings + href: https://www2.telenet.be/residential/nl/mijn-telenet/je-thuisnetwerk#/mainnavitem=hgw/mainnavitemid=item-1/subnavitem=modem_general + - Pulsetic: + - href: https://status.depeuter.dev + icon: https://pulsetic.com/favicon-196x196.png +- Homemade: + - AI-Transparency: + - href: https://ai-transparency.depeuter.dev + icon: https://ai-transparency.depeuter.dev/img/transparency.png + - Down-message: + - href: https://down.depeuter.dev + icon: https://down.depeuter.dev/assets/icon.jpg + - Portfolio: + - href: https://tibo.depeuter.dev + icon: https://tibo.depeuter.dev/assets/owl_circuit.png + ''; + + "homepage/services.yaml".text = '' +- Networking: + - Traefik Isabel: + description: Reverse proxy manager + href: https://traefik.isabel.depeuter.dev/dashboard/# + ping: https://traefik.isabel.depeuter.dev/dashboard/# + icon: traefik + widget: + type: traefik + url: https://traefik.isabel.depeuter.dev + - Traefik Niko: + description: Reverse proxy manager + href: https://traefik.niko.depeuter.dev/dashboard/# + ping: https://traefik.niko.depeuter.dev/dashboard/# + icon: traefik + widget: + type: traefik + url: https://traefik.niko.depeuter.dev + ''; + + "homepage/settings.yaml".text = '' +--- +# For configuration options and examples, please see: +# https://gethomepage.dev/en/configs/settings + +providers: + openweathermap: openweathermapapikey + weatherapi: weatherapiapikey + ''; + }; + + homelab.apps.technitiumDNS.enable = true; + + # Select internationalisation properties. + i18n.defaultLocale = "en_GB.utf8"; + + networking = { + hostName = "Hugo-Isabel"; + domain = "depeuter.dev"; + + enableIPv6 = true; + + # Open ports in the firewall. + firewall = { + enable = true; + }; + + networkmanager.enable = true; + }; + + # List services that you want to enable: + services = { + tailscale = { + enable = true; + useRoutingFeatures = "server"; + authKeyFile = "/etc/nixos/tailscale-authkey"; + extraUpFlags = [ + "--advertise-routes=192.168.0.0/24" + "--exit-node" + ]; + }; + + # Fix DNS issues. See: + # https://github.com/tailscale/tailscale/issues/4254 + # resolved.enable = true; + }; + + system.stateVersion = "24.05"; + + security.sudo = { + enable = true; + }; + + virtualisation = { + 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=*.isabel.depeuter.dev" + "--entrypoints.websecure.http.tls.domains[2].sans=*.jelly.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" + + # Additional routes + ]; + 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.isabel.depeuter.dev`)"; + "traefik.http.services.traefik.loadbalancer.server.port" = "8080"; + }; + autoStart = true; + }; + feishin = { + hostname = "feishin"; + image = "ghcr.io/jeffvli/feishin:0.7.1"; + ports = [ + # "9180:9180/tcp" # Web player (HTTP) + ]; + environment = { + # pre defined server name + SERVER_NAME = "Hugo"; + # When true AND name/type/url are set, only username/password can be toggled + SERVER_LOCK = "true"; + # navidrome also works + SERVER_TYPE = "jellyfin"; + # http://address:port + SERVER_URL= "https://jelly.depeuter.dev"; + TZ = config.time.timeZone; + }; + labels = { + "traefik.enable" = "true"; + "traefik.http.routers.feishin.rule" = "Host(`music.depeuter.dev`)"; + "traefik.http.services.feishin.loadbalancer.server.port" = "9180"; + "traefik.tls.options.default.minVersion" = "VersionTLS13"; + }; + autoStart = true; + }; + dashboard = { + hostname = "dashboard"; + image = "ghcr.io/gethomepage/homepage:v0.9.3"; + ports = [ + # "3000:3000/tcp" + ]; + volumes = [ + "/etc/homepage:/app/config" # Make sure your local config directory exists + "/var/run/docker.sock:/var/run/docker.sock:ro" # optional, for docker integrations + ]; + labels = { + "traefik.enable" = "true"; + "traefik.http.routers.dashboard.rule" = "Host(`dash.depeuter.dev`)"; + "traefik.http.services.dashboard.loadbalancer.server.port" = "3000"; + "traefik.tls.options.default.minVersion" = "VersionTLS13"; + }; + autoStart = true; + }; + prometheus = { + hostname = "prometheus"; + image = "prom/prometheus:v2.45.6"; + ports = [ + # "127.0.0.1:9090:9090/tcp" + ]; + labels = { + "traefik.enable" = "true"; + "traefik.http.routers.prometheus.rule" = "Host(`prometheus.isabel.depeuter.dev`)"; + "traefik.http.services.prometheus.loadbalancer.server.port" = "9090"; + "traefik.tls.options.default.minVersion" = "VersionTLS13"; + }; + autoStart = true; + }; + }; + }; + }; +} diff --git a/hosts/Isabel/hardware-configuration.nix b/hosts/Isabel/hardware-configuration.nix new file mode 100644 index 0000000..ec7ffda --- /dev/null +++ b/hosts/Isabel/hardware-configuration.nix @@ -0,0 +1,39 @@ +# Do not modify this file! It was generated by ‘nixos-generate-config’ +# and may be overwritten by future invocations. Please make changes +# to /etc/nixos/configuration.nix instead. +{ config, lib, pkgs, modulesPath, ... }: + +{ + imports = + [ (modulesPath + "/profiles/qemu-guest.nix") + ]; + + boot.initrd.availableKernelModules = [ "ata_piix" "xhci_pci" "ahci" "sd_mod" "sr_mod" ]; + boot.initrd.kernelModules = [ ]; + boot.kernelModules = [ ]; + boot.extraModulePackages = [ ]; + + fileSystems."/" = + { device = "/dev/disk/by-label/NIX-ROOT"; + fsType = "ext4"; + }; + + fileSystems."/boot/efi" = + { device = "/dev/disk/by-label/NIX-BOOT"; + fsType = "vfat"; + }; + + swapDevices = + [ { device = "/dev/disk/by-label/SWAP"; } + ]; + + # Enables DHCP on each ethernet and wireless interface. In case of scripted networking + # (the default) this is the recommended approach. When using systemd-networkd it's + # still possible to use this option, but it's recommended to use it in conjunction + # with explicit per-interface declarations with `networking.interfaces..useDHCP`. + networking.useDHCP = lib.mkDefault true; + # networking.interfaces.ens3.useDHCP = lib.mkDefault true; + + nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; + hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware; +} diff --git a/hosts/Niko/default.nix b/hosts/Niko/default.nix new file mode 100644 index 0000000..57dbc27 --- /dev/null +++ b/hosts/Niko/default.nix @@ -0,0 +1,180 @@ +{ config, pkgs, ... }: + +{ + imports = [ + # Include the results of the hardware scan. + ./hardware-configuration.nix + ]; + + homelab = { + apps.technitiumDNS.enable = true; + users.deploy.enable = true; + }; + + # Use the systemd-boot EFI boot loader. + boot.loader = { + systemd-boot.enable = true; + efi = { + canTouchEfiVariables = true; + efiSysMountPoint = "/boot/efi"; + }; + }; + + console = { + font = "Lat2-Terminus16"; + keyMap = "us"; + }; + + # List packages installed in the system profile. To search, run: + # $ nix search wget + environment.systemPackages = with pkgs; [ + cifs-utils + ]; + + hardware = { + enableRedistributableFirmware = true; + enableAllFirmware = true; + pulseaudio.enable = true; + opengl.enable = true; + }; + + # Select internationalisation properties. + i18n.defaultLocale = "en_GB.utf8"; + + networking = { + hostName = "Niko"; + domain = "depeuter.dev"; + + enableIPv6 = true; + + # Open ports in the firewall. + firewall = { + enable = true; + }; + + networkmanager.enable = true; + + extraHosts = '' + 192.168.0.11 jelly.depeuter.dev + ''; + }; + + nixpkgs.config.allowUnfree = true; + + # List services that you want to enable: + services = { + # Cage, a wayland kiosk service + cage = { + enable = true; + environment = { + # Do not fail when there are no input devices. + # WLR_LIBINPUT_NO_DEVICES = "1"; + }; + extraArguments = [ + "-d" # Don't draw client side decorations, when possible + # "-m" "last" # Use only the last connected output + "-s" # Allow VT switching + ]; + program = "/home/jellyfin-mpv-shim/start.sh"; + user = config.users.users.jellyfin-mpv-shim.name; + }; + + tailscale = { + enable = true; + useRoutingFeatures = "server"; + authKeyFile = "/etc/nixos/tailscale-authkey"; + extraUpFlags = [ + "--advertise-routes=192.168.0.0/24" + "--exit-node" + ]; + }; + + # Fix DNS issues. See: + # https://github.com/tailscale/tailscale/issues/4254 + # 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"; + isNormalUser = true; + extraGroups = [ + config.users.groups.audio.name + config.users.groups.video.name + ]; + packages = with pkgs; [ + jellyfin-mpv-shim + mpv + socat + ]; + }; + + 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/Niko/hardware-configuration.nix b/hosts/Niko/hardware-configuration.nix new file mode 100644 index 0000000..34c1dc6 --- /dev/null +++ b/hosts/Niko/hardware-configuration.nix @@ -0,0 +1,53 @@ +{ config, lib, pkgs, modulesPath, ... }: + +{ + imports = [ + (modulesPath + "/installer/scan/not-detected.nix") + ]; + + boot = { + initrd = { + availableKernelModules = [ + "xhci_pci" + "ahci" + "usb_storage" + "sd_mod" + ]; + }; + kernelModules = [ ]; + extraModulePackages = [ ]; + }; + + fileSystems = { + "/" = { + device = "/dev/disk/by-uuid/20b7eff3-fca5-4b60-a5a9-13219f70ce23"; + fsType = "ext4"; + }; + + "/boot/efi" = { + device = "/dev/disk/by-uuid/0B6D-0DCD"; + fsType = "vfat"; + }; + + "/media/photos" = { + device = "//192.168.0.11/CANVAS"; + fsType = "cifs"; + options = let + # This line prevents hanging on network split + automount_opts = "x-systemd.automount,noauto,x-systemd.idle-timeout=60,x-systemd.device-timeout=5s,x-systemd.mount-timeout=5s,user,users"; + in ["${automount_opts},credentials=/etc/nixos/smb-secrets,uid=1002,gid=100"]; + }; + }; + + swapDevices = [ + { device = "/dev/disk/by-uuid/f3679da0-45b3-45c0-a1d0-af8d771a7dbf"; } + ]; + + networking = { + hostId = "7a139e16"; + useDHCP = lib.mkDefault true; + }; + + powerManagement.cpuFreqGovernor = lib.mkDefault "powersave"; + hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware; +} diff --git a/hosts/Production/default.nix b/hosts/Production/default.nix new file mode 100644 index 0000000..cd929ff --- /dev/null +++ b/hosts/Production/default.nix @@ -0,0 +1,45 @@ +{ config, pkgs, lib, system, ... }: + +{ + config = { + homelab = { + apps.changedetection.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/ProductionGPU/default.nix b/hosts/ProductionGPU/default.nix new file mode 100644 index 0000000..75e48e7 --- /dev/null +++ b/hosts/ProductionGPU/default.nix @@ -0,0 +1,98 @@ +{ config, pkgs, lib, system, ... }: + +{ + config = { + homelab = { + apps.jellyfin.enable = true; + virtualisation.guest.enable = true; + }; + + networking = { + hostId = "aaaa2200"; + domain = "roxanne.depeuter.dev"; + + useDHCP = false; + + enableIPv6 = true; + + defaultGateway = { + address = "192.168.0.1"; + interface = "enp6s18"; + }; + + # Open ports in the firewall. + firewall = { + enable = true; + }; + + interfaces.enp6s18 = { + ipv4.addresses = [ + { + address = "192.168.0.94"; + prefixLength = 24; + } + ]; + }; + + nameservers = [ + "1.1.1.1" # Cloudflare + "1.0.0.1" # Cloudflare + ]; + }; + + system.stateVersion = "unstable"; + + ### Nvidia GPU support ### + + services.xserver.videoDrivers = [ "nvidia" ]; + + # virtualisation.docker.package = pkgs.nvidia-docker; + + nixpkgs.config = { + allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [ + "nvidia-x11" + "nvidia-settings" + "nvidia-persistenced" + ]; + + # enable vaapi on OS-level + # packageOverrides = pkgs: { + # vaapiIntel = pkgs.vaapiIntel.override { + # enableHybridCodec = true; + # }; + # }; + }; + + hardware = { + opengl = { + enable = true; + # driSupport = true; + # driSupport32Bit = true; + extraPackages = with pkgs; [ + # intel-media-driver + # intel-vaapi-driver # previously vaapiIntel + # vaapiVdpau + # intel-compute-runtime # OpenCL filter support (hardware tonemapping and subtitle burn-in) + # unstable.vpl-gpu-rt # QSV on 11th gen or newer + # intel-media-sdk # QSV up to 11th gen + ]; + }; + + nvidia = { + package = config.boot.kernelPackages.nvidiaPackages.stable; + # Whether to enable kernel modesetting when using the NVIDIA proprietary driver. + modesetting.enable = true; +# powerManagement = { +# enable = false; +# finegrained = false; +# }; + open = false; + nvidiaSettings = false; + + # Whether to enable nvidia-persistenced a update for NVIDIA GPU headless mode, i.e. It ensures all GPUs stay awake even during headless mode . + # nvidiaPersistenced = true; + }; + nvidia-container-toolkit.enable = true; + }; + }; +} diff --git a/hosts/Template/default.nix b/hosts/Template/default.nix new file mode 100644 index 0000000..21e54b7 --- /dev/null +++ b/hosts/Template/default.nix @@ -0,0 +1,36 @@ +{ config, pkgs, lib, system, ... }: + +{ + config = { + homelab.virtualisation.guest.enable = true; + + networking = { + # TODO hostName = "nixos"; + # TODO hostId = "aaaa9000"; + domain = "depeuter.dev"; + + enableIPv6 = true; + + useDHCP = false; + defaultGateway = { + address = "192.168.0.1"; + interface = "ens18"; + }; + interfaces.ens18 = { + ipv4.addresses = [ + { + address = "192.168.0.90"; + prefixLength = 24; + } + ]; + }; + + nameservers = [ + "1.1.1.1" # Cloudflare + "1.0.0.1" # Cloudflare + ]; + }; + + system.stateVersion = "24.05"; + }; +} diff --git a/hosts/Testing/default.nix b/hosts/Testing/default.nix new file mode 100644 index 0000000..2da6563 --- /dev/null +++ b/hosts/Testing/default.nix @@ -0,0 +1,46 @@ +{ config, pkgs, lib, system, ... }: + +{ + config = { + homelab = { + apps.freshrss.enable = true; + virtualisation.guest.enable = true; + }; + + networking = { + hostName = "Testing"; + hostId = "aaaa9200"; + 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.92"; + prefixLength = 24; + } + ]; + }; + + nameservers = [ + "1.1.1.1" # Cloudflare + "1.0.0.1" # Cloudflare + ]; + }; + + system.stateVersion = "24.05"; + }; +} diff --git a/hosts/Vaultwarden/default.nix b/hosts/Vaultwarden/default.nix new file mode 100644 index 0000000..9f98d84 --- /dev/null +++ b/hosts/Vaultwarden/default.nix @@ -0,0 +1,38 @@ +{ config, pkgs, lib, system, ... }: + +{ + config = { + homelab = { + apps.vaultwarden.enable = true; + virtualisation.guest.enable = true; + }; + + networking = { + hostId = "aaaa1300"; + domain = "depeuter.dev"; + + enableIPv6 = true; + + useDHCP = false; + defaultGateway = { + address = "192.168.0.1"; + interface = "ens18"; + }; + interfaces.ens18 = { + ipv4.addresses = [ + { + address = "192.168.0.22"; + prefixLength = 24; + } + ]; + }; + + nameservers = [ + "1.1.1.1" # Cloudflare + "1.0.0.1" # Cloudflare + ]; + }; + + system.stateVersion = "24.05"; + }; +} diff --git a/modules/apps/arr/default.nix b/modules/apps/arr/default.nix new file mode 100644 index 0000000..a88ed9c --- /dev/null +++ b/modules/apps/arr/default.nix @@ -0,0 +1,282 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.homelab.apps.arr; + + networkName = "arrStack"; + appNames = [ "bazarr" "lidarr" "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 = { + enable = lib.mkEnableOption "Arr Stack using Docker"; + + 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"; + }; + + config = { + homelab = { + users = lib.mkIf inUse { + apps.enable = true; + media.enable = true; + }; + + # "Master switch": Enable all apps. + apps.arr = lib.mkIf cfg.enable { + bazarr.enable = true; + lidarr.enable = true; + prowlarr.enable = true; + qbittorrent.enable = true; + radarr.enable = true; + sonarr.enable = true; + }; + + virtualisation.containers.enable = lib.mkIf inUse true; + }; + + fileSystems = lib.mkIf inUse { + "/srv/video" = { + device = "192.168.0.11:/mnt/SMALL/MEDIA/VIDEO"; + fsType = "nfs"; + options = [ + "rw" + "auto" + "nfsvers=4.2" + "soft" + "rsize=1048576" "wsize=1048576" + "timeo=600" "retrans=2" + "_netdev" "nosuid" "tcp" + ]; + }; + + "/srv/qbittorrent" = { + device = "192.168.0.11:/mnt/SMALL/CONFIG/QBITTORRENT"; + fsType = "nfs"; + options = [ + "rw" + "auto" + "nfsvers=4.2" + "hard" + "rsize=1048576" "wsize=1048576" + "timeo=600" "retrans=2" + "_netdev" "nosuid" "tcp" + ]; + }; + }; + + # Make sure the Docker network exists. + systemd.services."docker-${networkName}-create-network" = lib.mkIf inUse { + description = "Create Docker network for ${networkName}"; + requiredBy = [ + "docker-bazarr.service" + "docker-lidarr.service" + "docker-prowlarr.service" + "docker-qbittorrent.service" + "docker-radarr.service" + "docker-sonarr.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 + ''; + }; + + # Create a user for each app. + users.users = { + bazarr = lib.mkIf cfg.bazarr.enable { + uid = lib.mkForce 3003; + 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; + extraGroups = [ + config.users.groups.apps.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; + }; + }; + + virtualisation.oci-containers.containers = { + bazarr = lib.mkIf cfg.bazarr.enable { + hostname = "bazarr"; + image = "ghcr.io/hotio/bazarr:release-1.4.4"; + autoStart = true; + ports = [ + "6767:6767/tcp" + "6767:6767/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"'' + ]; + environment = { + PUID = toString config.users.users.bazarr.uid; + inherit PGID UMASK; + TZ = config.time.timeZone; + WEBUI_PORTS = "6767/tcp,6767/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}" + + "--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"; + image = "ghcr.io/hotio/prowlarr:release-1.23.1.4708"; + autoStart = true; + ports = [ + "9696:9696/tcp" + ]; + extraOptions = [ + "--network=${networkName}" + ]; + environment = { + PUID = toString config.users.users.prowlarr.uid; + inherit PGID UMASK; + TZ = config.time.timeZone; + }; + volumes = [ + # TODO "config:/config" + ]; + }; + + qbittorrent = lib.mkIf cfg.qbittorrent.enable { + hostname = "qbittorrent"; + image = "ghcr.io/hotio/qbittorrent:release-4.6.7"; + autoStart = true; + ports = [ + "10095:10095/udp" + "10095:10095/tcp" + ]; + 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,auto,nfsvers=4.2,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,_netdev,nosuid,tcp"'' + ]; + environment = { + PUID = toString config.users.users.qbittorrent.uid; + inherit PGID UMASK; + TZ = config.time.timeZone; + WEBUI_PORTS = "10095/tcp,10095/udp"; + }; + volumes = [ + "/srv/qbittorrent:/config/config" + "/srv/video:/media/video" + ]; + }; + + radarr = lib.mkIf cfg.radarr.enable { + hostname = "radarr"; + image = "ghcr.io/hotio/radarr:release-5.9.1.9070"; + autoStart = true; + ports = [ + "7878:7878/tcp" + ]; + extraOptions = [ + "--network=${networkName}" + ]; + environment = { + PUID = toString config.users.users.radarr.uid; + inherit PGID UMASK; + TZ = config.time.timeZone; + }; + volumes = [ + # TODO "config:/config" + # TODO "data:/data" + ]; + }; + + sonarr = lib.mkIf cfg.sonarr.enable { + hostname = "sonarr"; + image = "ghcr.io/hotio/sonarr:release-4.0.9.2244"; + autoStart = true; + ports = [ + "8989:8989/tcp" + ]; + extraOptions = [ + "--network=${networkName}" + ]; + environment = { + PUID = toString config.users.users.sonarr.uid; + inherit PGID UMASK; + TZ = config.time.timeZone; + }; + volumes = [ + # TODO "config:/config" + # TODO "data:/data" + ]; + }; + }; + }; +} diff --git a/modules/apps/calibre/default.nix b/modules/apps/calibre/default.nix new file mode 100644 index 0000000..6fddb81 --- /dev/null +++ b/modules/apps/calibre/default.nix @@ -0,0 +1,17 @@ +{ config, lib, ... }: + +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; + }; + }; +} 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 new file mode 100644 index 0000000..2d487e8 --- /dev/null +++ b/modules/apps/default.nix @@ -0,0 +1,14 @@ +{ + imports = [ + ./arr + ./calibre + ./changedetection + ./freshrss + ./gitea + ./jellyfin + ./plex + ./speedtest + ./technitium-dns + ./vaultwarden + ]; +} diff --git a/modules/apps/freshrss/default.nix b/modules/apps/freshrss/default.nix new file mode 100644 index 0000000..f2ea7ba --- /dev/null +++ b/modules/apps/freshrss/default.nix @@ -0,0 +1,76 @@ +{ 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" + "sync" "hard" "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.24.0"; + autoStart = true; + user = "0:33"; + ports = [ + "${toString port}:${toString port}/tcp" + ]; + extraOptions = [ + "--network=${networkName}" + ]; + environment = { + TZ = config.time.timeZone; + CRON_TIME = "3,18,33,48"; # Alternatively, configure cron inside container. + LISTEN = "0.0.0.0:${toString port}"; + }; + volumes = [ + "/srv/freshrss/www/freshrss/data:/var/www/FreshRSS/data" + "/srv/freshrss/www/freshrss/extensions:/var/www/FreshRSS/extensions" + ]; + }; + }; +} diff --git a/modules/apps/gitea/default.nix b/modules/apps/gitea/default.nix new file mode 100644 index 0000000..02f60cd --- /dev/null +++ b/modules/apps/gitea/default.nix @@ -0,0 +1,673 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.homelab.apps.gitea; + + networkName = "gitea"; + + UID = 3015; + GID = config.users.groups.apps.gid; + postgresPassword = "ChangeMe"; + repoDir = "/srv/git"; + webPort = 3000; + sshPort = 2222; + dbPort = 5432; + redisPort = 6379; + + title = "Hugo's Forge"; + slogan = "Forging ideas into reality."; + description = "Personal git server for projects that don't need collaboration."; +in { + options.homelab.apps.gitea.enable = lib.mkEnableOption "Gitea"; + + config = lib.mkIf cfg.enable { + homelab = { + users = { + apps.enable = true; + backup.enable = true; + }; + + virtualisation.containers.enable = true; + }; + + users.users.gitea = { + uid = lib.mkForce UID; + isSystemUser = true; + group = config.users.groups.apps.name; + home = "/var/empty"; + shell = null; + }; + + # Use filesystem mounts because rootless containers otherwise don't have access to the mount path (nested in docker directories). + # You could probably fix this by modifying the access rights on the path, but what would the point of that be? + fileSystems = { + # Mount options: + # - hard: retry requests indefinitely if the server becomes unresponsive. + # - nosuid: prevent set-user-id and set-group-id bits + "/srv/gitea-config" = { + device = "192.168.0.11:/mnt/SMALL/CONFIG/GITEA"; + fsType = "nfs"; + options = [ + "rw" + "nfsvers=4.2" + "async" "soft" "timeo=100" "retry=50" "actimeo=1800" "lookupcache=all" + "nosuid" + "tcp" + ]; + }; + + "/srv/gitea-git" = { + device = "192.168.0.11:/mnt/SMALL/DATA/GIT"; + fsType = "nfs"; + options = [ + "rw" + "nfsvers=4.2" + "async" "soft" "timeo=100" "retry=50" "actimeo=1800" "lookupcache=all" + "nosuid" + "tcp" + ]; + }; + }; + + # Make sure the Docker network exists. + systemd.services."docker-${networkName}-create-network" = { + description = "Create Docker network for ${networkName}"; + requiredBy = [ + "docker-gitea-db.service" + "docker-gitea.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 = { + gitea-db = { + hostname = "gitea-db"; + image = "postgres:15.8-alpine"; + autoStart = true; + ports = [ + "5432:${toString dbPort}/tcp" + ]; + extraOptions = [ + "--network=${networkName}" + ]; + environment = { + POSTGRES_PASSWORD = "ChangeMe"; + PGDATA = "/var/lib/postgresql/data/pgdata"; + }; + volumes = [ + "gitea-db:/var/lib/postgresql/data/pgdata" + ]; + }; + + gitea-redis = { + hostname = "gitea-redis"; + image = "redis:7.4.0-alpine3.20"; + autoStart = true; + ports = [ + "6379:${toString redisPort}/tcp" + ]; + extraOptions = [ + "--network=${networkName}" + ]; + volumes = [ + "gitea-redis:/data" + ]; + }; + + gitea = { + hostname = "gitea"; + image = "codeberg.org/forgejo/forgejo:8.0.3-rootless"; + autoStart = true; + user = "${toString UID}:${toString GID}"; + ports = [ + "3000:${toString webPort}/tcp" + "2222:${toString sshPort}/tcp" + ]; + extraOptions = [ + "--network=${networkName}" + ]; + dependsOn = [ + "gitea-db" + "gitea-redis" + ]; + volumes = [ + "/srv/gitea-config:/var/lib/gitea" + "/srv/gitea-git:/srv/git" + "/etc/timezone:/etc/timezone:ro" + "/etc/localtime:/etc/localtime:ro" + ]; + environmentFiles = [ + # NOTE Don't forget to create this file. + # TODO Put in place using age(nix)? + "/var/lib/gitea.env" + ]; + environment = { + # App name that shows in every page title. + FORGEJO__APP_NAME = title; + # Shows a slogan near the App name in every page title. + FORGEJO__APP_SLOGAN = slogan; + # Defines how the AppDisplayName should be presented. + #FORGEJO__APP_DISPLAY_NAME_FORMAT = ""; + # Will automaticaly detect the current user - but you can set it here. + FORGEJO__RUN_USER = "gitea"; + # Application run mode, affects performance and debugging: "dev" or "prod", default is + # "prod". Mode "dev" makes Gitea easier to develop and debug, values other than "dev" are + # treated as "prod" which is for production use. + FORGEJO__RUN_MODE = "prod"; + # The working directory. + #WORK_PATH = ""; + + # Disable SSH feature when not available. + FORGEJO__server__DISABLE_SSH = "false"; + # Whether to use the builltin SSH server or not. + FORGEJO__server__START_SSH_SERVER = "true"; + # Username to use for the builtin SSH server. If blank, then it is the value of RUN_USER. + #FORGEJO__server__BUILTIN_SSH_SERVER_USER = "git"; + # Domain to be exposed in clone URL. + #FORGEJO__server__SSH_DOMAIN = ""; + # SSH username displayed in clone URLs. + #FORGEJO__server__SSH_USER = "git"; + # The network interface the builtin SSH server should listen on. + #FORGEJO__server__SSH_LISTEN_HOST = "ens18"; + # Port number to be exposed in clone URL. + FORGEJO__server__SSH_PORT = "22"; + # Port number the builtin SSH server should listen on. + FORGEJO__server__SSH_LISTEN_PORT = toString sshPort; + # Root path of SSH directory, default is '~/.ssh', but you have to use '/home/git/.ssh'. + FORGEJO__server__SSH_ROOT_PATH = "/var/lib/gitea/ssh"; + # Gitea will create a authorized_keys file by default when it is not using the internal ssh server + # If you intend to use the AuthorizedKeysCommand functionality then you should turn this off. + #FORGEJO__server__SSH_CREATE_AUTHORIZED_KEYS_FILE = "true"; + # Gitea will create a authorized_principals file by default when it is not using the internal ssh server + # If you intend to use the AuthorizedPrincipalsCommand functionality then you should turn this off. + #FORGEJO__server__SSH_CREATE_AUTHORIZED_PRINCIPALS_FILE = "true"; + # For the built-in SSH server, choose the ciphers to support for SSH connections, + # for system SSH this setting has no effect + #FORGEJO__server__SSH_SERVER_CIPHERS = "chacha20-poly1305@openssh.com, aes128-ctr, aes192-ctr, aes256-ctr, aes128-gcm@openssh.com, aes256-gcm@openssh.com"; + # For the built-in SSH server, choose the key exchange algorithms to support for SSH connections, + # for system SSH this setting has no effect + #FORGEJO__server__SSH_SERVER_KEY_EXCHANGES = "curve25519-sha256, ecdh-sha2-nistp256, ecdh-sha2-nistp384, ecdh-sha2-nistp521, diffie-hellman-group14-sha256, diffie-hellman-group14-sha1"; + # For the built-in SSH server, choose the MACs to support for SSH connections, + # for system SSH this setting has no effect + #FORGEJO__server__SSH_SERVER_MACS = "hmac-sha2-256-etm@openssh.com, hmac-sha2-256, hmac-sha1"; + # For the built-in SSH server, choose the keypair to offer as the host key + # The private key should be at SSH_SERVER_HOST_KEY and the public SSH_SERVER_HOST_KEY.pub + # relative paths are made absolute relative to the %(APP_DATA_PATH)s + FORGEJO__server__SSH_SERVER_HOST_KEYS = "/var/lib/gitea/ssh/forgejo.ed25519"; + # Directory to create temporary files in when testing public keys using ssh-keygen, + # default is the system temporary directory. + #FORGEJO__server__SSH_KEY_TEST_PATH = ""; + # Use `ssh-keygen` to parse public SSH keys. The value is passed to the shell. By default, Gitea does the parsing itself. + #FORGEJO__server__SSH_KEYGEN_PATH = ""; + # Enable SSH Authorized Key Backup when rewriting all keys, default is false + FORGEJO__server__SSH_AUTHORIZED_KEYS_BACKUP = "false"; + # ... + # Enable exposure of SSH clone URL to anonymous visitors, default is false. + FORGEJO__server__EXPOSE_ANONYMOUS = "false"; + # ... + # Enables git-lfs support. true or false, default is false. + FORGEJO__server__LFS_START_SERVER = "false"; + # ... + + # Database to use. Either "mysql", "postgres" or "sqlite3". + FORGEJO__database__DB_TYPE = "postgres"; + FORGEJO__database__HOST = "gitea-db:${toString dbPort}"; + FORGEJO__database__NAME = "gitea"; + FORGEJO__database__USER = "gitea"; + FORGEJO__database__PASSWD = postgresPassword; + #FORGEJO__database__SCHEMA = ""; + #FORGEJO__database__SSL_MODE = "disable"; + + # Whether the installer is disabled (set to true to disable the installer). + #FORGEJO__security__INSTALL_LOCK = "false"; + # Global security key that will be used. + # This key is VERY IMPORTANT. If you lose it, the data encrypted by it can't be decrypted anymore. + #FORGEJO__security__SECRET_KEY = ""; + # Alternatively, specify the location of the secret key. + #FORGEJO__security__SECRET_KEY_URI = "file:/etc/gitea/secret_key"; + # ... + + # IF the camo is enabled. + #FORGEJO__camo__ENABLED = "false"; + # .... + + # Enables OAuth2 provider + FORGEJO__oauth2__ENABLED = "false"; + # ... + + # Root path for the log files - defaults to %(GITEA_WORK_DIR)/log + #FORGEJO__log__ROOT_PATH = ""; + # Either "console", "file" or "conn", default is "console" + FORGEJO__log__MODE = "file"; + # Either "Trace", "Debug", "Info", "Warn", "Error" or "None", default is "Info". + FORGEJO__log__LEVEL = "Warn"; + # ... + # Collect SSH logs (Creates logs from ssh git requests) + FORGEJO__log__ENABLE_SSH_LOG = "true"; + # ... + + # The path of git executable. If empty, Gitea searches through the PATH environment. + #FORGEJO__git__PATH = ""; + # ... + FORGEJO__git_0x2E_timeout__MIGRATE = "600"; + FORGEJO__git_0x2E_timeout__MIRROR = "600"; + + # Time limit to confirm account/email registration. + #FORGEJO__service__ACTIVE_CODE_LIVE_MINUTES = "180"; + # Time limit to perform the reset of a forgotten password. + #FORGEJO__service__RESET_PASSWD_CODE_LIVE_MINUTES = "180"; + # Whether a new user needs to confirm their email when registering. + FORGEJO__service__REGISTER_EMAIL_CONFIRM = "true"; + # Whether a new user needs to be confirmed manually after registration. + FORGEJO__service__REGISTER_MANUAL_CONFIRM = "true"; + # List of domain names that are allowed to be used to register on a Gitea instance, wildcard is supported. + #FORGEJO__service__EMAIL_DOMAIN_ALLOWLIST = ""; + # Comma-separated list of domain names that are not allowed to be used to register on a Gitea instance, wildcard is supported. + #FORGEJO__service__EMAIL_DOMAIN_BLOCKLIST = ""; + # Disallow registration, only allow admins to create accounts. + FORGEJO__service__DISABLE_REGISTRATION = "true"; + # Allow registration only using gitea itself, it works only when DISABLE_REGISTRATION is false. + FORGEJO__service__ALLOW_ONLY_INTERNAL_REGISTRATION = "true"; + # Allow registration only using third-party services, it works only when DISABLE_REGISTRATION is false. + FORGEJO__service__ALLOW_ONLY_EXTERNAL_REGISTRATION = "false"; + # User must sign in to view anything. + FORGEJO__service__REQUIRE_SIGNIN_VIEW = "false"; + # Mail notification + FORGEJO__service__ENABLE_NOTIFY_MAIL = "true"; + # This setting enables gitea to be signed in with HTTP BASIC Authentication using the user's password. + # If you set this to false you will not be able to access the tokens endpoints on the API with your password. + # Please note that setting this to false will not disable OAuth Basic or Basic authentication using a token. + FORGEJO__service__ENABLE_BASIC_AUTHENTICATION = "false"; + # ... + # Enable captcha validation for registration. + FORGEJO__service__ENABLE_CAPTCHA = "true"; + # Enable this to require captcha validation for login. + FORGEJO__service__REQUIRE_CAPTCHA_FOR_LOGIN = "true"; + # Requires captcha for external registrations + #FORGEJO__service__REQUIRE_EXTERNAL_REGISTRATION_CAPTCHA = "false"; + # Requires a password for external registrations. + #FORGEJO__service__REQUIRE_EXTERNAL_REGISTRATION_PASSWORD = "false"; + # Type of captcha you want to use. Options: image, recaptcha, hcaptcha, mcaptcha, cfturnstile. + FORGEJO__service__CAPTCHA_TYPE = "image"; + # ... + # Default value for KeepEmailPrivate + # Each new user will get the value of this setting copied into their profile + FORGEJO__service__DEFAULT_KEEP_EMAIL_PRIVATE = "true"; + # Default value for AllowCreateOrganization + # Every new user will have rights set to create organizations depending on this setting. + FORGEJO__service__DEFAULT_ALLOW_CREATE_ORGANIZATION = "true"; + # Default value for IsRestricted + # Every new user will have restricted permissions depending on this setting. + FORGEJO__service__DEFAULT_USER_IS_RESTRICTED = "false"; + # Users will be able to use dots when choosing their username. Disabling this is + # helpful if your usersare having issues with e.g. RSS feeds or advanced third-party + # extensions that use strange regex patterns. + FORGEJO__service__ALLOW_DOTS_IN_USERNAMES = "false"; + # Either "public", "limited" or "private", default is "public". + # Limited is for users visible only to signed users. + # Private is for users visible only to members of their organizations + # Public is for users visible for everyone + FORGEJO__service__DEFAULT_USER_VISIBILITY = "limited"; + # Set which visibility modes a user can have + FORGEJO__service__ALLOWED_USER_VISIBILITY_MODES = "public,limited,private"; + # Either "public", "limited" or "private", default is "public". + # Limited is for organizations visible only to signed users + # Private is for organizations visible only to members of the organization + # Public is for organizations visible to everyone + FORGEJO__service__DEFAULT_ORG_VISIBILITY = "limited"; + # Default value for DefaultOrgMemberVisible + # True will make the membership of the users visible when added to the organisation + FORGEJO__service__DEFAULT_ORG_MEMBER_VISIBLE = "false"; + # Default value for EnableDependencies + # Repositories will use dependencies by default depending on this setting + #FORGEJO__service__DEFAULT_ENABLE_DEPENDENCIES = "true"; + # Dependencies can be added from any repository where the user is granted access or only from the current repository depending on this setting. + #FORGEJO__service__ALLOW_CROSS_REPOSITORY_DEPENDENCIES = "true"; + # Default map service. No external API support has been included. A service has to allow + # searching using URL parameters, the location will be appended to the URL as escaped query parameter. + # Some example values are: + # - OpenStreetMap: https://www.openstreetmap.org/search?query= + # - Google Maps: https://www.google.com/maps/place/ + # - MapQuest: https://www.mapquest.com/search/ + # - Bing Maps: https://www.bing.com/maps?where1= + #FORGEJO__service__USER_LOCATION_MAP_URL = "https://www.openstreetmap.org/search?query="; + # Enable heatmap on users profiles. + FORGEJO__service__ENABLE_USER_HEATMAP = "true"; + # Enable Timetracking + FORGEJO__service__ENABLE_TIMETRACKING = "true"; + # Default value for EnableTimetracking + # Repositories will use timetracking by default depending on this setting + FORGEJO__service__DEFAULT_ENABLE_TIMETRACKING = "false"; + # Default value for AllowOnlyContributorsToTrackTime + # Only users with write permissions can track time if this is true + #FORGEJO__service__DEFAULT_ALLOW_ONLY_CONTRIBUTORS_TO_TRACK_TIME = "true"; + # Value for the domain part of the user's email address in the git log if user + # has set KeepEmailPrivate to true. The user's email will be replaced with a + # concatenation of the user name in lower case, "@" and NO_REPLY_ADDRESS. Default + # value is "noreply." + DOMAIN, where DOMAIN resolves to the value from server.DOMAIN + # Note: do not use the notation below + FORGEJO__service__NO_REPLY_ADDRESS = "noreply.depeuter.dev"; + # Show Registration button. + FOGEJO__service__SHOW_REGISTRATION_BUTTON = "false"; + # Show milestones dashboard page - a view of all the user's milestones. + #FORGEJO__service__SHOW_MILESTONES_DASHBOARD_PAGE = "true"; + # Default value for AutoWatchNewRepos + # When adding a repo to a team or creating a new repo all team members will watch the + # repo automatically if enabled + #FORGEJO__service__AUTO_WATCH_NEW_REPOS = "true"; + # Default value for AutoWatchOnChanges + # Make the user watch a repository When they commit for the first time + #FORGEJO__service__AUTO_WATCH_ON_CHANGES = "false"; + # Minimum amount of time a user must exist before comments are kept when the user is deleted. + #FORGEJO__service__USER_DELETE_WITH_COMMENTS_MAX_TIME = "0"; + # Valid site url schemes for user profiles + #FORGEJO__service__VALID_SITE_URL_SCHEMES = "http,https"; + + # Enable repository badges (via shields.io or a similar generator) + #FORGEJO__badges__ENABLED = "true"; + # ... + + # Root path for storing all repository data. By default, it is set to %(APP_DATA_PATH)s/gitea-repositories. + # 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. + #FORGEJO__repository__DISABLE_FORKS = "false"; + # The default branch name of new repositories + FORGEJO__repository__DEFAULT_BRANCH = "main"; + # ... + + # List of prefixes used in Pull Request title to mark them as Work In Progress (matched in a case-insensitive manner) + FORGEJO__repository_0x2E_pull_0X2D_request__WORK_IN_PROGRESS_PREFIXES = "WIP:,[WIP],WIP"; + # ... + # In the default merge message for squash commits walk all commits to include all authors in the Co-authored-by otherwise just use those in the limited list. + FORGEJO__repository_0x2E_pull_0X2D_request__DEFAULT_MERGE_MESSAGE_ALL_AUTHORS = "true"; + # ... + + # Enable cors headers (disabled by default) + FORGEJO__cors__ENABLED = "true"; + # list of requesting origins that are allowed, eg: "https://*.example.com". + FORGEJO__cors__ALLOW_DOMAINS = "https://git.depeuter.dev,http://192.168.0.24:${toString webPort}"; + + # Set the default theme for the Gitea install. + FORGEJO__ui__DEFAULT_THEME = "gitea-auto"; + # All available themes. Allow users to select personalized themes regardless of `DEFAULT_THEME`. + FORGEJO__ui__THEMES = "gitea-auto,gitea-light,gitea-dark,forgejo-auto,forgejo-light,forgejo-dark,forgejo-auto-deuteranopia-protanopia,forgejo-light-deuteranopia-protanopia,forgejo-dark-deuteranopia-protanopia,forgejo-auto-tritanopia,forgejo-light-tritanopia-forgejo-dark-tritanopia,github-auto,github,github-dark,edge-auto,edge-light,edge-dark,everforest-auto,everforest-light,everforest-dark,gruvbox-auto,gruvbox-light,gruvbox-dark,gruvbox-material-auto,grubox-material-dark,gruvbox-material-light,sonokai-andromeda,sonokai-atlantis,sonokai-espresso,sonokai-maia,sonokai-shusia,sonokai,catppuccin-frappe-green,catppuccin-frappe-teal,catppuccin-frappe-sky,catppuccin-frappe-sapphire,catppuccin-frappe-blue,catppuccin-frappe-lavender,catppuccin-macchiato-green,catppuccin-macchiato-teal,catppuccin-macchiato-sky,catppuccin-macchiato-sapphire,catppuccin-macchiato-blue,catppuccin-macchiato-lavender,catppuccin-mocha-green,catppuccin-mocha-teal,catppuccin-mocha-sky,catppuccin-mocha-sapphire,catppuccin-mocha-blue,catppuccin-mocha-lavender,nord,pitchblack,matrix,dark-arc"; + + FORGEJO__ui_0x2E_meta__AUTHOR = "${title} - ${slogan}"; + FORGEJO__ui_0x2E_meta__DESCRIPTION = description; + FORGEJO__ui_0x2E_meta__KEYWORDS = "git,self-hosted,projects,code"; + + # Whether to render SVG files as images. If SVG rendering is disabled, SVG files are displayed as text and cannot be embedded in markdown files as images. + FORGEJO__ui_0x2E_svg__ENABLE_RENDER = "true"; + + # ... + # Enables math inline and block detection + FORGEJO__markdown__ENABLE_MATH = "true"; + + # Define allowed algorithms and their minimum key length (use -1 to disable a type) + #FORGEJO__ssh__0x2E__minimum_key_sizes__ED25519 = "256"; + #FORGEJO__ssh__0x2E__minimum_key_sizes__ECDSA = "256"; + FORGEJO__ssh_0x2E_minimum_key_sizes__RSA = "-1"; + FORGEJO__ssh_0x2E_minimum_key_sizes__DSA = "-1"; + + # ... indexer + + # ... queue + + # Disallow regular (non-admin) users from creating organizations. + #FORGEJO__admin__DISABLE_REGULAR_ORG_CREATION = "false"; + # Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled + FORGEJO__admin__DEFAULT_EMAIL_NOTIFICATIONS = "enabled"; + # Send an email to all admins when a new user signs up to inform the admins about this act. Options: true, false + FORGEJO__admin__SEND_NOTIFICATION_EMAIL_ON_NEW_USER = "true"; + # Disabled features for users, could be "deletion", "manage_ssh_keys","manage_gpg_keys" more features can be disabled in future + # - deletion: a user cannot delete their own account + # - manage_ssh_keys: a user cannot configure ssh keys + # - manage_gpg_keys: a user cannot configure gpg keys + #FORGEJO__admin__USER_DISABLED_FEATURES = ""; + # Comma separated list of disabled features ONLY if the user has an external login type (eg. LDAP, Oauth, etc.), could be `deletion`, `manage_ssh_keys`, `manage_gpg_keys`. This setting is independent from `USER_DISABLED_FEATURES` and supplements its behavior. + # - deletion: a user cannot delete their own account + # - manage_ssh_keys: a user cannot configure ssh keys + # - manage_gpg_keys: a user cannot configure gpg keys + #FORGEJO__admin__EXTERNAL_USER_DISABLE_FEATURES = ""; + + # Whether to allow signin in via OpenID + FORGEJO__openid__ENABLE_OPENID_SIGNIN = "false"; + # Whether to allow registering via OpenID + # Do not include to rely on rhw DISABLE_REGISTRATION setting + FORGEJO__openid__ENABLE_OPENID_SIGNUP = "false"; + # ... + + # ... oath2_client + + # ... webhook + + FORGEJO__mailer__ENABLED = "true"; + # Buffer length of channel, keep it as it is if you don't know what it is. + #FORGEJO__mailer__SEND_BUFFER_LEN = "100"; + # Prefix displayed before subject in mail. + #FORGEJO__mailer__SUBJECT_PREFIX = ""; + # Mail server protocol. One of "smtp", "smtps", "smtp+starttls", "smtp+unix", "sendmail", "dummy" + FORGEJO__mailer__PROTOCOL = "smtps"; + # Mail server address + FORGEJO__mailer__SMTP_ADDR = "smtp.gmail.com"; + # Mail server port. If no protocol is specified, it will be inferred by this setting. + FORGEJO__mailer__SMTP_PORT = "465"; + # Enable HELO operation. Defaults to true. + #FORGEJO__mailer__ENABLE_HELO = "true"; + # Custom hostname fo the HELO operation. If no value is provided, one is retrieved from + # the system. + #FORGEJO__mailer__HELO_HOSTNAME = ""; + # If set to 'true', completely ignores server certificate validation errors. UNSAFE! + #FORGEJO__mailer__FORCE_TRUST_SERVER_CERT = "false"; + # Use client certificate in connection. + #FORGEJO__mailer__USE_CLIENT_CERT = "false"; + #FORGEJO__mailer__CLIENT_CERT_FILE = "custom/mailer/cert.pem"; + #FORGEJO__mailer__CLIENT_KEY_FILE = "custom/mailer/key.pem"; + # Mail from address, RFC 5322. This can be just an email address, or the + # `"Name" ` format. + FORGEJO__mailer__FROM = ''"${title}" ''; + # Sometimes it is helpful to use a different address on the envelope. Set this to use + # ENVELOPE_FROM as the from on the envelope. Set to `<>` to send an empty address. + #FORGEJO__mailer__ENVELOPE_FROM = ""; + # If gitea sends mails on behave of users, it will just use the name also displayed in the + # WebUI. If you want e.g. `Mister X (by CodeIt) `, set it to + # `{{ .DisplayName }} (by {{ .AppName }})`. + # Available Variables: `.DisplayName`, `.AppName` and `.Domain`. + #FORGEJO__mailer__FROM_DISPLAY_NAME_FORMAT = "{{ .DisplayName }}"; + # Mailer user name and password, if required by provider. + #FORGEJO__mailer__USER = ""; + # Use PASSWD = `your password` for quoting if you use special characters in the password. + #FORGEJO__mailer__PASSWD = ""; + # Send mails only in plain text, without HTML alternative + #FORGEJO__mailer__SEND_AS_PLAIN_TEXT = "false"; + # Specify an alternative sendmail binary + #FORGEJO__mailer__SENDMAIL_PATH = "sendmail"; + # Specify any extra sendmail arguments + # WARNING: if your sendmail program interprets options you should set this to "--" or terminate these args with "--" + #FORGEJO__mailer__SENDMAIL_ARGS = ""; + # Timeout for Sendmail + #FORGEJO__mailer__SENDMAIL_TIMEOUT = "5m"; + # convert \r\n to \n for Sendmail + #FORGEJO__mailer__SENDMAIL_CONVERT_CRLF = "true"; + + # ... email.incoming + + # Either "memory", "redis", "memcache", or "twoqueue". default is "memory" + FORGEJO__cache__ADAPTER = "redis"; + # For "memory" only, GC interval in seconds, default is 60. + #FORGEJO__cache__INTERVAL = "60"; + # For "redis" and "memcache", connection host address + # redis: `redis://127.0.0.1:6379/0?pool_size=100&idle_timeout=180s` (or `redis+cluster://127.0.0.1:6379/0?pool_size=100&idle_timeout=180s` for a Redis cluster) + # memcache: `127.0.0.1:11211` + # twoqueue: `{"size":50000,"recent_ratio":0.25,"ghost_ratio":0.5}` or `50000` + FORGEJO__cache__HOST = "redis://gitea-redis:${toString redisPort}/0?pool_size=100&idle_timeout=180s"; + # Time to keep items in cache if not used, default is 16 hours. + # Setting it to -1 disables caching + FORGEJO__cache__ITEM_TTL = "16h"; + # Time to keep items in cache if not used, default is 8760 hours. + # Setting it to -1 disables caching + FORGEJO__cache_0X2E_last_0X2D_commit__ITEM_TTL = "8760h"; + # Only enable the cache when repository's commits count great than + FORGEJO__cache_0X2E_last_0X2D_commit__COMMITS_COUNT = "100"; + + # Either "memory", "file", "redis", "db", "mysql", "couchbase", "memcache" or "postgres" + # Default is "memory". "db" will reuse the configuration in [database] + #FORGEJO__session__PROVIDER = "memory"; + # Provider config options + # memory: doesn't have any config yet + # file: session file path, e.g. `data/sessions` + # redis: `redis://127.0.0.1:6379/0?pool_size=100&idle_timeout=180s` (or `redis+cluster://127.0.0.1:6379/0?pool_size=100&idle_timeout=180s` for a Redis cluster) + # mysql: go-sql-driver/mysql dsn config string, e.g. `root:password@/session_table` + #FORGEJO__session__PROVIDER_CONFIG = "data/sessions"; # Relative paths will be made absolute against _`AppWorkPath`_. + # Session cookie name + FORGEJO__session__COOKIE_NAME = "i_like_tibo"; + # If you use session in https only: true or false. If not set, it defaults to `true` if the ROOT_URL is an HTTPS URL. + FORGEJO__session__COOKIE_SECURE = "true"; + # Session GC time interval in seconds, default is 86400 (1 day) + #FORGEJO__session__GC_0X2E_INTERVAL_0X2E_TIME = "86400"; + # Session life time in seconds, default is 86400 (1 day) + #FORGEJO__session__SESSION_0X2E_LIFE_0X2E_TIME = "86400"; + # Cookie domain name. Default is empty + FORGEJO__session__DOMAIN = "git.depeuter.dev"; + # SameSite settings. Either "none", "lax", or "strict" + FORGEJO__session__SAME_SITE = "strict"; + + # How Gitea deals with missing repository avatars + # none = no avatar will be displayed; random = random avatar will be displayed; image = default image will be used + #FORGEJO__picture__REPOSITORY_AVATAR_FALLBACK = "none"; + #FORGEJO__picture__REPOSITORY_AVATAR_FALLBACK_IMAGE = "/img/repo_default.png"; + # Max Width and Height of uploaded avatars. + # This is to limit the amount of RAM used when resizing the image. + FORGEJO__picture__AVATAR_MAX_WIDTH = "10000"; + FORGEJO__picture__AVATAR_MAX_HEIGTH = "10000"; + # The multiplication factor for rendered avatar images. + # Larger values result in finer rendering on HiDPI devices. + #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"; + # 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" + # or a custom avatar source, like: http://cn.gravatar.com/avatar/ + #FORGEJO__picture__GRAVATAR_SOURCE = "gravatar"; + # This value will always be true in offline mode. + #FORGEJO__picture__DISABLE_GRAVATAR = "false"; + # Federated avatar lookup uses DNS to discover avatar associated. + # with emails, see https://www.libravatar.org + # This value will always be false in offline mode or when Gravatar is disabled. + #FORGEJO__picture__ENABLE_FEDERATED_AVATAR = "false"; + + # ... attachment + + # ... time + + # ... cron + + # Enables the mirror functionality. Set to **false** to disable all mirrors. Pre-existing mirrors remain valid but won't be updated; may be converted to regular repo. + FORGEJO__mirror__ENABLED = "true"; + # Disable the creation of **new** pull mirrors. Pre-existing mirrors remain valid. Will be ignored if `mirror.ENABLED` is `false`. + FORGEJO__mirror__DISABLE_NEW_PULL = "false"; + # Disable the creation of **new** push mirrors. Pre-existing mirrors remain valid. Will be ignored if `mirror.ENABLED` is `false`. + FORGEJO__mirror__DISABLE_NEW_PUSH = "false"; + # Default interval as a duration between each check + FORGEJO__mirror__DEFAULT_INTERVAL = "1h"; + # Min interval as a duration must be > 1m + FORGEJO__mirror__MIN_INTERVAL = "5m"; + + # ... api + + # ... i18n + + # .. highlight.mapping + + # Show version information about Gitea and Go in the footer + FORGEJO__other__SHOW_FOOTER_VERSION = "false"; + # Show template execution time in the footer + FORGEJO__other__SHOW_FOOTER_TEMPLATE_LOAD_TIME = "false"; + # Show the "powered by" text in the footer + FORGEJO__other__SHOW_FOOTER_POWERED_BY = "false"; + # Generate sitemap. Defaults to `true`. + FORGEJO__other__ENABLE_SITEMAP = "true"; + # Enable/Disable RSS/Atom feed + FORGEJO__other__ENABLE_FEED = "true"; + + # ... markup + + # ... metrics + + # ... migrations + + # ... f3 + + # Enable/Disable federation capabilities + FORGEJO__federation_ENABLED = "false"; + # ... + + # Enable/Disable package registry capabilities + FORGEJO__packages__ENABLED = "true"; + + # ... storage + + # Repo-archive storage will override storage. + #FORGEJO__repo_0X2D_archive__STORAGE_TYPE = "local"; + # Where your lfs files reside, default is data/lfs + FORGEJO__repo_0X2D_archive__PATH = ""; + # Override the minio base path if storage type is minio. + #FORGEJO__repo_0X2D_archive__MINIO_BASE_PATH = ""; + + # lfs storage will override storage. + #FORGEJO__lfs__STORAGE_TYPE = "local"; + # Where your lfs files reside, default is data/lfs + FORGEJO__lfs__PATH = ""; + # Override the minio base path if storage is set to minio. + #FORGEJO__lfs__MINIO_BASE_PATH = "lfs/"; + + # Enable the proxy, all requests to external via HTTP will be affected + FORGEJO__proxy__PROXY_ENABLED = "false"; + # Proxy server URL, support http://, https//, socks://, blank will follow environment http_proxy/https_proxy/no_proxy + #FORGEJO__proxy__PROXY_URL = ""; + # Comma separated list of host names requiring proxy. Glob patterns (*) are accepted; use ** to match all hosts. + #FORGEJO__proxy__PROXY_HOSTS = ""; + + # Enable/Disable actions capabilities + FORGEJO__actions__ENABLED = "true"; + # Default address to get action plugins, e.g. the default value means downloading from "https://code.forgejo.org/actions/checkout" for "uses: actions/checkout@v3" + #FORGEJO__actions__DEFAULT_ACTIONS_URL = "https://code.forgejo.org"; + # ... + }; + }; + }; + }; +} diff --git a/modules/apps/jellyfin/default.nix b/modules/apps/jellyfin/default.nix new file mode 100644 index 0000000..5b4081a --- /dev/null +++ b/modules/apps/jellyfin/default.nix @@ -0,0 +1,175 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.homelab.apps.jellyfin; + + networkName = "jellyfin"; + + UID = 3008; + GID = config.users.groups.media.gid; +in { + options.homelab.apps.jellyfin.enable = lib.mkEnableOption "Jellyfin using Docker"; + + config = lib.mkIf cfg.enable { + homelab = { + users = { + apps.enable = true; + media.enable = true; + }; + virtualisation.containers.enable = true; + }; + + fileSystems = { + "/srv/audio" = { + device = "192.168.0.11:/mnt/SMALL/MEDIA/AUDIO"; + fsType = "nfs"; + options = [ + "ro" + "nfsvers=4.2" + "async" "soft" + "timeo=100" "retry=50" "actimeo=1800" "lookupcache=all" + "nosuid" "tcp" + ]; + }; + + "/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"; + options = [ + "ro" + "nfsvers=4.2" + "async" "soft" + "timeo=100" "retry=50" "actimeo=1800" "lookupcache=all" + "nosuid" "tcp" + ]; + }; + + "/srv/photo" = { + device = "192.168.0.11:/mnt/BIG/MEDIA/PHOTO/ARCHIVE"; + fsType = "nfs"; + options = [ + "ro" + "nfsvers=4.2" + "async" "soft" + "timeo=100" "retry=50" "actimeo=1800" "lookupcache=all" + "nosuid" "tcp" + ]; + }; + }; + + users.users.jellyfin = { + uid = lib.mkForce UID; + isSystemUser = true; + group = config.users.groups.apps.name; + extraGroups = [ + config.users.groups.media.name + ]; + home = "/var/empty"; + shell = null; + }; + + # Make sure the Docker network exists. + systemd.services."docker-${networkName}-create-network" = { + description = "Create Docker network for ${networkName}"; + requiredBy = [ + "docker-jellyfin.service" + "docker-feishin.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 = { + jellyfin = { + hostname = "jellyfin"; + image = "jellyfin/jellyfin:10.10.0"; + user = "${toString UID}:${toString GID}"; + autoStart = true; + ports = [ + "8096:8096/tcp" + # "8920:8920/tcp" + ]; + extraOptions = [ + "--network=${networkName}" + "--device=nvidia.com/gpu=all" # Equivalent to --gpus=all + ]; + volumes = [ + "jellyfin-config:/config" + "cache:/cache" + + "/srv/audio:/media/audio" + "/srv/video:/media/video" + "/srv/homevideo:/media/homevideo" + "/srv/photo:/media/photo" + ]; + environment = { + # TODO + }; + }; + + jellyfin-vue = { + hostname = "jellyfin-vue"; + image = "ghcr.io/jellyfin/jellyfin-vue:unstable"; + autoStart = true; + ports = [ + "8080:80/tcp" + ]; + extraOptions = [ + "--network=${networkName}" + ]; + labels = { + }; + }; + + feishin = let + feishinPort = "9180"; + in { + hostname = "feishin"; + image = "ghcr.io/jeffvli/feishin:0.7.1"; + autoStart = true; + ports = [ + "${feishinPort}:9180/tcp" # Web player (HTTP) + ]; + extraOptions = [ + "--network=${networkName}" + ]; + environment = { + # pre defined server name + SERVER_NAME = "Hugo"; + # When true AND name/type/url are set, only username/password can be toggled + SERVER_LOCK = "true"; + # Either "jellyfin" or "navidrome" + SERVER_TYPE = "jellyfin"; + # http://address:port + SERVER_URL= "https://jelly.depeuter.dev"; + 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"; + }; + }; + }; + }; +} diff --git a/modules/apps/plex/default.nix b/modules/apps/plex/default.nix new file mode 100644 index 0000000..251f9dd --- /dev/null +++ b/modules/apps/plex/default.nix @@ -0,0 +1,50 @@ +{ config, lib, ... }: + +let + cfg = config.homelab.apps.plex; +in { + options.homelab.apps.plex.enable = lib.mkEnableOption "Plex"; + + config = lib.mkIf cfg.enable { + users.users.plex = { + uid = lib.mkForce 3009; + isSystemUser = true; + group = config.users.groups.media; + 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" + ]; + }; + }; + }; +} diff --git a/modules/apps/speedtest/default.nix b/modules/apps/speedtest/default.nix new file mode 100644 index 0000000..4c42c64 --- /dev/null +++ b/modules/apps/speedtest/default.nix @@ -0,0 +1,27 @@ +{ config, lib, ... }: + +let + cfg = config.homelab.apps.speedtest; +in { + options.homelab.apps.speedtest.enable = lib.mkEnableOption "Speedtest"; + + config = lib.mkIf cfg.enable { + homelab.virtualisation.containers.enable = true; + + virtualisation.oci-containers.containers.speedtest = { + hostname = "speedtest"; + image = "openspeedtest/latest:v2.0.5"; + ports = [ + "3000:3000" + "3001:3001" + ]; + labels = { + "traefik.enable" = "true"; + "traefik.http.routers.speedtest.rule" = "Host(`speedtest.${config.networking.hostName}.${config.networking.domain}`)"; + "traefik.http.services.speedtest.loadbalancer.server.port" = "9090"; + "traefik.tls.options.default.minVersion" = "VersionTLS13"; + }; + autoStart = true; + }; + }; +} diff --git a/modules/apps/technitium-dns/default.nix b/modules/apps/technitium-dns/default.nix new file mode 100644 index 0000000..0d0c71c --- /dev/null +++ b/modules/apps/technitium-dns/default.nix @@ -0,0 +1,73 @@ +{ config, lib, ... }: + +let + cfg = config.homelab.apps.technitiumDNS; +in { + options.homelab.apps.technitiumDNS.enable = lib.mkEnableOption "Technitium DNS"; + + config = lib.mkIf cfg.enable { + homelab.virtualisation.containers.enable = true; + + virtualisation.oci-containers.containers.technitium-dns = { + hostname = "technitium-dns"; + image = "technitium/dns-server:12.1"; + ports = [ + # "5380:5380/tcp" #DNS web console (HTTP) + # "53443:53443/tcp" #DNS web console (HTTPS) + "53:53/udp" #DNS service + "53:53/tcp" #DNS service + # "853:853/udp" #DNS-over-QUIC service + # "853:853/tcp" #DNS-over-TLS service + # "443:443/udp" #DNS-over-HTTPS service (HTTP/3) + # "443:443/tcp" #DNS-over-HTTPS service (HTTP/1.1, HTTP/2) + # "80:80/tcp" #DNS-over-HTTP service (use with reverse proxy or certbot certificate renewal) + # "8053:8053/tcp" #DNS-over-HTTP service (use with reverse proxy) + # "67:67/udp" #DHCP service + ]; + environment = { + # The primary domain name used by this DNS Server to identify itself. + DNS_SERVER_DOMAIN = config.networking.hostName; + # DNS Server will use IPv6 for querying whenever possible with this option enabled. + DNS_SERVER_PREFER_IPV6 = "true"; + # The TCP port number for the DNS web console over HTTP protocol. + # DNS_SERVER_WEB_SERVICE_HTTP_PORT=5380 + # The TCP port number for the DNS web console over HTTPS protocol. + # DNS_SERVER_WEB_SERVICE_HTTPS_PORT=53443 + # Enables HTTPS for the DNS web console. + # DNS_SERVER_WEB_SERVICE_ENABLE_HTTPS=false + # Enables self signed TLS certificate for the DNS web console. + # DNS_SERVER_WEB_SERVICE_USE_SELF_SIGNED_CERT=false + # Enables DNS server optional protocol DNS-over-HTTP on TCP port 8053 to be used with a TLS terminating reverse proxy like nginx. + # DNS_SERVER_OPTIONAL_PROTOCOL_DNS_OVER_HTTP=false + # Recursion options: Allow, Deny, AllowOnlyForPrivateNetworks, UseSpecifiedNetworks. + #nDNS_SERVER_RECURSION=AllowOnlyForPrivateNetworks + # Comma separated list of IP addresses or network addresses to deny recursion. Valid only for `UseSpecifiedNetworks` recursion option. + # DNS_SERVER_RECURSION_DENIED_NETWORKS=1.1.1.0/24 + # Comma separated list of IP addresses or network addresses to allow recursion. Valid only for `UseSpecifiedNetworks` recursion option. + # DNS_SERVER_RECURSION_ALLOWED_NETWORKS=127.0.0.1, 192.168.1.0/24 + # Sets the DNS server to block domain names using Blocked Zone and Block List Zone. + DNS_SERVER_ENABLE_BLOCKING = "false"; + # Specifies if the DNS Server should respond with TXT records containing a blocked domain report for TXT type requests. + # DNS_SERVER_ALLOW_TXT_BLOCKING_REPORT=false + # A comma separated list of block list URLs. + # DNS_SERVER_BLOCK_LIST_URLS= + #Comma separated list of forwarder addresses. + DNS_SERVER_FORWARDERS="195.130.130.2,195.130.131.2"; + # Forwarder protocol options: Udp, Tcp, Tls, Https, HttpsJson. + # DNS_SERVER_FORWARDER_PROTOCOL=Tcp + # Enable this option to use local time instead of UTC for logging. + # DNS_SERVER_LOG_USING_LOCAL_TIME=true + }; + volumes = [ + "technitium_dns:/etc/dns" + ]; + labels = { + "traefik.enable" = "true"; + "traefik.http.routers.technitium-dns.rule" = "Host(`dns.${config.networking.hostName}.${config.networking.domain}`)"; + "traefik.http.services.technitium-dns.loadbalancer.server.port" = "5380"; + "traefik.tls.options.default.minVersion" = "VersionTLS13"; + }; + autoStart = true; + }; + }; +} diff --git a/modules/apps/vaultwarden/default.nix b/modules/apps/vaultwarden/default.nix new file mode 100644 index 0000000..6d06287 --- /dev/null +++ b/modules/apps/vaultwarden/default.nix @@ -0,0 +1,636 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.homelab.apps.vaultwarden; + + networkName = "vaultwarden"; +in { + options.homelab.apps.vaultwarden.enable = lib.mkEnableOption "Vaultwarden"; + + config = lib.mkIf cfg.enable { + homelab = { + # Allow remote backups. + users.backup.enable = true; + + virtualisation.containers.enable = true; + }; + + # Make sure the Docker network exists. + systemd.services."docker-${networkName}-create-network" = { + description = "Create Docker network for ${networkName}"; + requiredBy = [ + "docker-vaultwarden-db.service" + "docker-vaultwarden.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 = { + vaultwarden-db = { + hostname = "vaultwarden-db"; + image = "postgres:15.8-alpine"; + autoStart = true; + ports = [ + "5432:5432/tcp" + ]; + extraOptions = [ + "--network=${networkName}" + ]; + environment = { + POSTGRES_PASSWORD = "ChangeMe"; + PGDATA = "/var/lib/postgresql/data/pgdata"; + }; + volumes = [ + "vaultwarden-db:/var/lib/postgresql/data" + ]; + }; + + vaultwarden = let + dataDir = "/data"; + in { + hostname = "vaultwarden"; + image = "vaultwarden/server:1.32.5-alpine"; + autoStart = true; + ports = [ + "10102:80/tcp" + ]; + extraOptions = [ + "--network=${networkName}" + ]; + dependsOn = [ + "vaultwarden-db" + ]; + volumes = [ + "vaultwarden:${dataDir}" + ]; + environmentFiles = [ + # NOTE Don't forget to create this file + # TODO Put in place using age(nix)? + "/var/lib/vaultwarden.env" + ]; + environment = { + #################### + ### Data folders ### + #################### + + ## Main data folder + DATA_FOLDER = dataDir; + + ## Individual folders, these override %DATA_FOLDER% + # ICON_CACHE_FOLDER=data/icon_cache + # ATTACHMENTS_FOLDER=data/attachments + # SENDS_FOLDER=data/sends + # TMP_FOLDER=data/tmp + + ## Templates data folder, by default uses embedded templates + ## Check source code to see the format + # TEMPLATES_FOLDER=data/templates + ## Automatically reload the templates for every request, slow, use only for development + # RELOAD_TEMPLATES=false + + ## Web vault settings + # WEB_VAULT_FOLDER=web-vault/ + # WEB_VAULT_ENABLED=true + + ######################### + ### Database settings ### + ######################### + + ## Database URL + ## When using SQLite, this is the path to the DB file, default to %DATA_FOLDER%/db.sqlite3 + # DATABASE_URL=data/db.sqlite3 + ## When using MySQL, specify an appropriate connection URI. + ## Details: https://docs.diesel.rs/2.1.x/diesel/mysql/struct.MysqlConnection.html + # DATABASE_URL=mysql://user:password@host[:port]/database_name + ## When using PostgreSQL, specify an appropriate connection URI (recommended) + ## or keyword/value connection string. + ## 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"; + + ## Enable WAL for the DB + ## Set to false to avoid enabling WAL during startup. + ## Note that if the DB already has WAL enabled, you will also need to disable WAL in the DB, + ## this setting only prevents Vaultwarden from automatically enabling it on start. + ## Please read project wiki page about this setting first before changing the value as it can + ## cause performance degradation or might render the service unable to start. + # ENABLE_DB_WAL=true + + ## Database connection retries + ## Number of times to retry the database connection during startup, with 1 second delay between each retry, set to 0 to retry indefinitely + # DB_CONNECTION_RETRIES=15 + + ## Database timeout + ## Timeout when acquiring database connection + # DATABASE_TIMEOUT=30 + + ## Database max connections + ## Define the size of the connection pool used for connecting to the database. + # DATABASE_MAX_CONNS=10 + + ## Database connection initialization + ## Allows SQL statements to be run whenever a new database connection is created. + ## This is mainly useful for connection-scoped pragmas. + ## If empty, a database-specific default is used: + ## - SQLite: "PRAGMA busy_timeout = 5000; PRAGMA synchronous = NORMAL;" + ## - MySQL: "" + ## - PostgreSQL: "" + # DATABASE_CONN_INIT="" + + ################# + ### WebSocket ### + ################# + + ## Enable websocket notifications + # ENABLE_WEBSOCKET=true + + ########################## + ### Push notifications ### + ########################## + + ## Enables push notifications (requires key and id from https://bitwarden.com/host) + ## Details about mobile client push notification: + ## - https://github.com/dani-garcia/vaultwarden/wiki/Enabling-Mobile-Client-push-notification + # PUSH_ENABLED=false + # PUSH_INSTALLATION_ID=CHANGEME + # PUSH_INSTALLATION_KEY=CHANGEME + + # WARNING: Do not modify the following settings unless you fully understand their implications! + # Default Push Relay and Identity URIs + # PUSH_RELAY_URI=https://push.bitwarden.com + # PUSH_IDENTITY_URI=https://identity.bitwarden.com + # European Union Data Region Settings + # If you have selected "European Union" as your data region, use the following URIs instead. + # PUSH_RELAY_URI=https://api.bitwarden.eu + # PUSH_IDENTITY_URI=https://identity.bitwarden.eu + + ##################### + ### Schedule jobs ### + ##################### + + ## Job scheduler settings + ## + ## Job schedules use a cron-like syntax (as parsed by https://crates.io/crates/cron), + ## and are always in terms of UTC time (regardless of your local time zone settings). + ## + ## The schedule format is a bit different from crontab as crontab does not contains seconds. + ## You can test the the format here: https://crontab.guru, but remove the first digit! + ## SEC MIN HOUR DAY OF MONTH MONTH DAY OF WEEK + ## "0 30 9,12,15 1,15 May-Aug Mon,Wed,Fri" + ## "0 30 * * * * " + ## "0 30 1 * * * " + ## + ## How often (in ms) the job scheduler thread checks for jobs that need running. + ## Set to 0 to globally disable scheduled jobs. + # JOB_POLL_INTERVAL_MS=30000 + ## + ## Cron schedule of the job that checks for Sends past their deletion date. + ## Defaults to hourly (5 minutes after the hour). Set blank to disable this job. + # SEND_PURGE_SCHEDULE="0 5 * * * *" + ## + ## Cron schedule of the job that checks for trashed items to delete permanently. + ## Defaults to daily (5 minutes after midnight). Set blank to disable this job. + # TRASH_PURGE_SCHEDULE="0 5 0 * * *" + ## + ## Cron schedule of the job that checks for incomplete 2FA logins. + ## Defaults to once every minute. Set blank to disable this job. + # INCOMPLETE_2FA_SCHEDULE="30 * * * * *" + ## + ## Cron schedule of the job that sends expiration reminders to emergency access grantors. + ## Defaults to hourly (3 minutes after the hour). Set blank to disable this job. + # EMERGENCY_NOTIFICATION_REMINDER_SCHEDULE="0 3 * * * *" + ## + ## Cron schedule of the job that grants emergency access requests that have met the required wait time. + ## Defaults to hourly (7 minutes after the hour). Set blank to disable this job. + # EMERGENCY_REQUEST_TIMEOUT_SCHEDULE="0 7 * * * *" + ## + ## Cron schedule of the job that cleans old events from the event table. + ## Defaults to daily. Set blank to disable this job. Also without EVENTS_DAYS_RETAIN set, this job will not start. + # EVENT_CLEANUP_SCHEDULE="0 10 0 * * *" + ## Number of days to retain events stored in the database. + ## If unset (the default), events are kept indefinitely and the scheduled job is disabled! + # EVENTS_DAYS_RETAIN= + ## + ## Cron schedule of the job that cleans old auth requests from the auth request. + ## Defaults to every minute. Set blank to disable this job. + # AUTH_REQUEST_PURGE_SCHEDULE="30 * * * * *" + ## + ## Cron schedule of the job that cleans expired Duo contexts from the database. Does nothing if Duo MFA is disabled or set to use the legacy iframe prompt. + ## Defaults to every minute. Set blank to disable this job. + # DUO_CONTEXT_PURGE_SCHEDULE="30 * * * * *" + + ######################## + ### General settings ### + ######################## + + ## Domain settings + ## The domain must match the address from where you access the server + ## It's recommended to configure this value, otherwise certain functionality might not work, + ## like attachment downloads, email links and U2F. + ## For U2F to work, the server must use HTTPS, you can use Let's Encrypt for free certs + ## To use HTTPS, the recommended way is to put Vaultwarden behind a reverse proxy + ## Details: + ## - https://github.com/dani-garcia/vaultwarden/wiki/Enabling-HTTPS + ## - https://github.com/dani-garcia/vaultwarden/wiki/Proxy-examples + ## For development + # DOMAIN=http://localhost + ## For public server + DOMAIN = "https://vault.depeuter.dev"; + ## For public server (URL with port number) + # DOMAIN=https://vw.domain.tld:8443 + ## For public server (URL with path) + # DOMAIN=https://domain.tld/vw + + ## Controls whether users are allowed to create Bitwarden Sends. + ## This setting applies globally to all users. + ## To control this on a per-org basis instead, use the "Disable Send" org policy. + # SENDS_ALLOWED=true + + ## HIBP Api Key + ## HaveIBeenPwned API Key, request it here: https://haveibeenpwned.com/API/Key + # HIBP_API_KEY= + + ## Per-organization attachment storage limit (KB) + ## Max kilobytes of attachment storage allowed per organization. + ## When this limit is reached, organization members will not be allowed to upload further attachments for ciphers owned by that organization. + # ORG_ATTACHMENT_LIMIT= + ## Per-user attachment storage limit (KB) + ## Max kilobytes of attachment storage allowed per user. + ## When this limit is reached, the user will not be allowed to upload further attachments. + # USER_ATTACHMENT_LIMIT= + ## Per-user send storage limit (KB) + ## Max kilobytes of send storage allowed per user. + ## When this limit is reached, the user will not be allowed to upload further sends. + # USER_SEND_LIMIT= + + ## Number of days to wait before auto-deleting a trashed item. + ## If unset (the default), trashed items are not auto-deleted. + ## This setting applies globally, so make sure to inform all users of any changes to this setting. + # TRASH_AUTO_DELETE_DAYS= + + ## Number of minutes to wait before a 2FA-enabled login is considered incomplete, + ## resulting in an email notification. An incomplete 2FA login is one where the correct + ## master password was provided but the required 2FA step was not completed, which + ## potentially indicates a master password compromise. Set to 0 to disable this check. + ## This setting applies globally to all users. + # INCOMPLETE_2FA_TIME_LIMIT=3 + + ## Disable icon downloading + ## Set to true to disable icon downloading in the internal icon service. + ## This still serves existing icons from $ICON_CACHE_FOLDER, without generating any external + ## network requests. $ICON_CACHE_TTL must also be set to 0; otherwise, the existing icons + ## will be deleted eventually, but won't be downloaded again. + # DISABLE_ICON_DOWNLOAD=false + + ## Controls if new users can register + SIGNUPS_ALLOWED = "false"; + + ## Controls if new users need to verify their email address upon registration + ## 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 = "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 + # SIGNUPS_VERIFY_RESEND_TIME=3600 + + ## If SIGNUPS_VERIFY is set to true, this limits how many times an email verification + ## email will be re-sent upon an attempted login. + # SIGNUPS_VERIFY_RESEND_LIMIT=6 + + ## Controls if new users from a list of comma-separated domains can register + ## even if SIGNUPS_ALLOWED is set to false + # SIGNUPS_DOMAINS_WHITELIST=example.com,example.net,example.org + + ## Controls whether event logging is enabled for organizations + ## This setting applies to organizations. + ## Disabled by default. Also check the EVENT_CLEANUP_SCHEDULE and EVENTS_DAYS_RETAIN settings. + # ORG_EVENTS_ENABLED=false + + ## Controls which users can create new orgs. + ## Blank or 'all' means all users can create orgs (this is the default): + # ORG_CREATION_USERS= + ## 'none' means no users can create orgs: + # ORG_CREATION_USERS=none + ## A comma-separated list means only those users can create orgs: + # ORG_CREATION_USERS=admin1@example.com,admin2@example.com + + ## 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"; + + ## 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) + # INVITATION_EXPIRATION_HOURS=120 + + ## Controls whether users can enable emergency access to their accounts. + ## This setting applies globally to all users. + # EMERGENCY_ACCESS_ALLOWED=true + + ## Controls whether users can change their email. + ## This setting applies globally to all users + # EMAIL_CHANGE_ALLOWED=true + + ## Number of server-side passwords hashing iterations for the password hash. + ## The default for new users. If changed, it will be updated during login for existing users. + # PASSWORD_ITERATIONS=600000 + + ## Controls whether users can set password hints. This setting applies globally to all users. + # PASSWORD_HINTS_ALLOWED=true + + ## Controls whether a password hint should be shown directly in the web page if + ## SMTP service is not configured. Not recommended for publicly-accessible instances + ## as this provides unauthenticated access to potentially sensitive data. + SHOW_PASSWORD_HINT = "false"; + + ######################### + ### Advanced settings ### + ######################### + + ## Client IP Header, used to identify the IP of the client, defaults to "X-Real-IP" + ## Set to the string "none" (without quotes), to disable any headers and just use the remote IP + # IP_HEADER=X-Real-IP + + ## Icon service + ## The predefined icon services are: internal, bitwarden, duckduckgo, google. + ## To specify a custom icon service, set a URL template with exactly one instance of `{}`, + ## which is replaced with the domain. For example: `https://icon.example.com/domain/{}`. + ## + ## `internal` refers to Vaultwarden's built-in icon fetching implementation. + ## If an external service is set, an icon request to Vaultwarden will return an HTTP + ## redirect to the corresponding icon at the external service. An external service may + ## be useful if your Vaultwarden instance has no external network connectivity, or if + ## you are concerned that someone may probe your instance to try to detect whether icons + ## for certain sites have been cached. + # ICON_SERVICE=internal + + ## Icon redirect code + ## The HTTP status code to use for redirects to an external icon service. + ## The supported codes are 301 (legacy permanent), 302 (legacy temporary), 307 (temporary), and 308 (permanent). + ## Temporary redirects are useful while testing different icon services, but once a service + ## has been decided on, consider using permanent redirects for cacheability. The legacy codes + ## are currently better supported by the Bitwarden clients. + # ICON_REDIRECT_CODE=302 + + ## Cache time-to-live for successfully obtained icons, in seconds (0 is "forever") + ## Default: 2592000 (30 days) + # ICON_CACHE_TTL=2592000 + ## Cache time-to-live for icons which weren't available, in seconds (0 is "forever") + ## Default: 2592000 (3 days) + # ICON_CACHE_NEGTTL=259200 + + ## Icon download timeout + ## Configure the timeout value when downloading the favicons. + ## The default is 10 seconds, but this could be to low on slower network connections + # ICON_DOWNLOAD_TIMEOUT=10 + + ## Block HTTP domains/IPs by Regex + ## Any domains or IPs that match this regex won't be fetched by the internal HTTP client. + ## Useful to hide other servers in the local network. Check the WIKI for more details + ## NOTE: Always enclose this regex withing single quotes! + # HTTP_REQUEST_BLOCK_REGEX='^(192\.168\.0\.[0-9]+|192\.168\.1\.[0-9]+)$' + + ## Enabling this will cause the internal HTTP client to refuse to connect to any non global IP address. + ## Useful to secure your internal environment: See https://en.wikipedia.org/wiki/Reserved_IP_addresses for a list of IPs which it will block + # HTTP_REQUEST_BLOCK_NON_GLOBAL_IPS=true + + ## Client Settings + ## Enable experimental feature flags for clients. + ## This is a comma-separated list of flags, e.g. "flag1,flag2,flag3". + ## + ## The following flags are available: + ## - "autofill-overlay": Add an overlay menu to form fields for quick access to credentials. + ## - "autofill-v2": Use the new autofill implementation. + ## - "browser-fileless-import": Directly import credentials from other providers without a file. + ## - "fido2-vault-credentials": Enable the use of FIDO2 security keys as second factor. + # EXPERIMENTAL_CLIENT_FEATURE_FLAGS=fido2-vault-credentials + + ## Require new device emails. When a user logs in an email is required to be sent. + ## If sending the email fails the login attempt will fail!! + # REQUIRE_DEVICE_EMAIL=false + + ## Enable extended logging, which shows timestamps and targets in the logs + # EXTENDED_LOGGING=true + + ## Timestamp format used in extended logging. + ## Format specifiers: https://docs.rs/chrono/latest/chrono/format/strftime + # LOG_TIMESTAMP_FORMAT="%Y-%m-%d %H:%M:%S.%3f" + + ## Logging to Syslog + ## This requires extended logging + # USE_SYSLOG=false + + ## Logging to file + # LOG_FILE=/path/to/log + + ## Log level + ## Change the verbosity of the log output + ## Valid values are "trace", "debug", "info", "warn", "error" and "off" + ## Setting it to "trace" or "debug" would also show logs for mounted routes and static file, websocket and alive requests + ## For a specific module append a comma separated `path::to::module=log_level` + ## For example, to only see debug logs for icons use: LOG_LEVEL="info,vaultwarden::api::icons=debug" + LOG_LEVEL = "warn"; + + ## Token for the admin interface, preferably an Argon2 PCH string + ## Vaultwarden has a built-in generator by calling `vaultwarden hash` + ## For details see: https://github.com/dani-garcia/vaultwarden/wiki/Enabling-admin-page#secure-the-admin_token + ## If not set, the admin panel is disabled + ## New Argon2 PHC string + ## Note that for some environments, like docker-compose you need to escape all the dollar signs `$` with an extra dollar sign like `$$` + ## Also, use single quotes (') instead of double quotes (") to enclose the string when needed + # ADMIN_TOKEN='$argon2id$v=19$m=65540,t=3,p=4$MmeKRnGK5RW5mJS7h3TOL89GrpLPXJPAtTK8FTqj9HM$DqsstvoSAETl9YhnsXbf43WeaUwJC6JhViIvuPoig78' + ## Old plain text string (Will generate warnings in favor of Argon2) + # ADMIN_TOKEN=Vy2VyYTTsKPv8W5aEOWUbB/Bt3DEKePbHmI4m9VcemUMS2rEviDowNAFqYi1xjmp + + ## Enable this to bypass the admin panel security. This option is only + ## meant to be used with the use of a separate auth layer in front + # DISABLE_ADMIN_TOKEN=false + + ## Number of seconds, on average, between admin login requests from the same IP address before rate limiting kicks in. + # ADMIN_RATELIMIT_SECONDS=300 + ## Allow a burst of requests of up to this size, while maintaining the average indicated by `ADMIN_RATELIMIT_SECONDS`. + # ADMIN_RATELIMIT_MAX_BURST=3 + + ## Set the lifetime of admin sessions to this value (in minutes). + # ADMIN_SESSION_LIFETIME=20 + + ## Allowed iframe ancestors (Know the risks!) + ## https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-ancestors + ## Allows other domains to embed the web vault into an iframe, useful for embedding into secure intranets + ## This adds the configured value to the 'Content-Security-Policy' headers 'frame-ancestors' value. + ## Multiple values must be separated with a whitespace. + # ALLOWED_IFRAME_ANCESTORS= + + ## Number of seconds, on average, between login requests from the same IP address before rate limiting kicks in. + # LOGIN_RATELIMIT_SECONDS=60 + ## Allow a burst of requests of up to this size, while maintaining the average indicated by `LOGIN_RATELIMIT_SECONDS`. + ## Note that this applies to both the login and the 2FA, so it's recommended to allow a burst size of at least 2. + # LOGIN_RATELIMIT_MAX_BURST=10 + + ## BETA FEATURE: Groups + ## Controls whether group support is enabled for organizations + ## This setting applies to organizations. + ## Disabled by default because this is a beta feature, it contains known issues! + ## KNOW WHAT YOU ARE DOING! + # ORG_GROUPS_ENABLED=false + + ## Increase secure note size limit (Know the risks!) + ## Sets the secure note size limit to 100_000 instead of the default 10_000. + ## WARNING: This could cause issues with clients. Also exports will not work on Bitwarden servers! + ## KNOW WHAT YOU ARE DOING! + # INCREASE_NOTE_SIZE_LIMIT=false + + ## Enforce Single Org with Reset Password Policy + ## Enforce that the Single Org policy is enabled before setting the Reset Password policy + ## Bitwarden enforces this by default. In Vaultwarden we encouraged to use multiple organizations because groups were not available. + ## Setting this to true will enforce the Single Org Policy to be enabled before you can enable the Reset Password policy. + # ENFORCE_SINGLE_ORG_WITH_RESET_PW_POLICY=false + + ######################## + ### MFA/2FA settings ### + ######################## + + ## Yubico (Yubikey) Settings + ## Set your Client ID and Secret Key for Yubikey OTP + ## You can generate it here: https://upgrade.yubico.com/getapikey/ + ## You can optionally specify a custom OTP server + # YUBICO_CLIENT_ID=11111 + # YUBICO_SECRET_KEY=AAAAAAAAAAAAAAAAAAAAAAAA + # YUBICO_SERVER=http://yourdomain.com/wsapi/2.0/verify + + ## Duo Settings + ## You need to configure the DUO_IKEY, DUO_SKEY, and DUO_HOST options to enable global Duo support. + ## Otherwise users will need to configure it themselves. + ## Create an account and protect an application as mentioned in this link (only the first step, not the rest): + ## https://help.bitwarden.com/article/setup-two-step-login-duo/#create-a-duo-security-account + ## Then set the following options, based on the values obtained from the last step: + # DUO_IKEY= + # DUO_SKEY= + # DUO_HOST= + ## After that, you should be able to follow the rest of the guide linked above, + ## ignoring the fields that ask for the values that you already configured beforehand. + ## + ## If you want to attempt to use Duo's 'Traditional Prompt' (deprecated, iframe based) set DUO_USE_IFRAME to 'true'. + ## Duo no longer supports this, but it still works for some integrations. + ## If you aren't sure, leave this alone. + # DUO_USE_IFRAME=false + + ## Email 2FA settings + ## Email token size + ## Number of digits in an email 2FA token (min: 6, max: 255). + ## Note that the Bitwarden clients are hardcoded to mention 6 digit codes regardless of this setting! + # EMAIL_TOKEN_SIZE=6 + ## + ## Token expiration time + ## Maximum time in seconds a token is valid. The time the user has to open email client and copy token. + # EMAIL_EXPIRATION_TIME=600 + ## + ## Maximum attempts before an email token is reset and a new email will need to be sent. + # EMAIL_ATTEMPTS_LIMIT=3 + ## + ## Setup email 2FA regardless of any organization policy + # EMAIL_2FA_ENFORCE_ON_VERIFIED_INVITE=false + ## Automatically setup email 2FA as fallback provider when needed + # EMAIL_2FA_AUTO_FALLBACK=false + + ## Other MFA/2FA settings + ## Disable 2FA remember + ## Enabling this would force the users to use a second factor to login every time. + ## Note that the checkbox would still be present, but ignored. + # DISABLE_2FA_REMEMBER=false + ## + ## Authenticator Settings + ## Disable authenticator time drifted codes to be valid. + ## TOTP codes of the previous and next 30 seconds will be invalid + ## + ## According to the RFC6238 (https://tools.ietf.org/html/rfc6238), + ## we allow by default the TOTP code which was valid one step back and one in the future. + ## This can however allow attackers to be a bit more lucky with there attempts because there are 3 valid codes. + ## You can disable this, so that only the current TOTP Code is allowed. + ## Keep in mind that when a sever drifts out of time, valid codes could be marked as invalid. + ## In any case, if a code has been used it can not be used again, also codes which predates it will be invalid. + # AUTHENTICATOR_DISABLE_TIME_DRIFT=false + + ########################### + ### SMTP Email settings ### + ########################### + + ## Mail specific settings, set SMTP_FROM and either SMTP_HOST or USE_SENDMAIL to enable the mail service. + ## To make sure the email links are pointing to the correct host, set the DOMAIN variable. + ## 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_USERNAME=username + # SMTP_PASSWORD=password + # SMTP_TIMEOUT=15 + + ## Choose the type of secure connection for SMTP. The default is "starttls". + ## The available options are: + ## - "starttls": The default port is 587. + ## - "force_tls": The default port is 465. + ## - "off": The default port is 25. + ## Ports 587 (submission) and 25 (smtp) are standard without encryption and with encryption via STARTTLS (Explicit TLS). Port 465 (submissions) is used for encrypted submission (Implicit TLS). + SMTP_SECURITY = "starttls"; + SMTP_PORT = "587"; + + # Whether to send mail via the `sendmail` command + # USE_SENDMAIL=false + # Which sendmail command to use. The one found in the $PATH is used if not specified. + # SENDMAIL_COMMAND="/path/to/sendmail" + + ## Defaults for SSL is "Plain" and "Login" and nothing for Non-SSL connections. + ## Possible values: ["Plain", "Login", "Xoauth2"]. + ## Multiple options need to be separated by a comma ','. + SMTP_AUTH_MECHANISM = "Login"; + + ## Server name sent during the SMTP HELO + ## By default this value should be is on the machine's hostname, + ## but might need to be changed in case it trips some anti-spam filters + # HELO_NAME= + + ## Embed images as email attachments + # SMTP_EMBED_IMAGES=true + + ## SMTP debugging + ## When set to true this will output very detailed SMTP messages. + ## WARNING: This could contain sensitive information like passwords and usernames! Only enable this during troubleshooting! + # SMTP_DEBUG=false + + ## Accept Invalid Certificates + ## DANGEROUS: This option introduces significant vulnerabilities to man-in-the-middle attacks! + ## Only use this as a last resort if you are not able to use a valid certificate. + ## If the Certificate is valid but the hostname doesn't match, please use SMTP_ACCEPT_INVALID_HOSTNAMES instead. + # SMTP_ACCEPT_INVALID_CERTS=false + + ## Accept Invalid Hostnames + ## DANGEROUS: This option introduces significant vulnerabilities to man-in-the-middle attacks! + ## Only use this as a last resort if you are not able to use a valid certificate. + # SMTP_ACCEPT_INVALID_HOSTNAMES=false + + ####################### + ### Rocket settings ### + ####################### + + ## Rocket specific settings + ## See https://rocket.rs/v0.5/guide/configuration/ for more details. + # ROCKET_ADDRESS=0.0.0.0 + ## The default port is 8000, unless running in a Docker container, in which case it is 80. + # ROCKET_PORT=8000 + # ROCKET_TLS={certs="/path/to/certs.pem",key="/path/to/key.pem"} + }; + }; + }; + }; +} diff --git a/modules/common/default.nix b/modules/common/default.nix new file mode 100644 index 0000000..44309f5 --- /dev/null +++ b/modules/common/default.nix @@ -0,0 +1,16 @@ +{ + config = { + homelab = { + services.openssh.enable = true; + users.admin.enable = true; + }; + + nix.settings.experimental-features = [ + "flakes" + "nix-command" + ]; + + # Set your time zone. + time.timeZone = "Europe/Brussels"; + }; +} diff --git a/modules/default.nix b/modules/default.nix new file mode 100644 index 0000000..5d901bc --- /dev/null +++ b/modules/default.nix @@ -0,0 +1,9 @@ +{ + imports = [ + ./apps + ./services + ./virtualisation + + ./common + ]; +} diff --git a/modules/services/actions/default.nix b/modules/services/actions/default.nix new file mode 100644 index 0000000..338b963 --- /dev/null +++ b/modules/services/actions/default.nix @@ -0,0 +1,49 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.homelab.services.actions; +in { + options.homelab.services.actions.enable = lib.mkEnableOption "Actions runner"; + + config = lib.mkIf cfg.enable { + homelab.virtualisation.containers.enable = true; + + services.gitea-actions-runner = { + instances.depeuter-dev = { + enable = true; + url = "https://git.depeuter.dev"; + tokenFile = "/etc/runner/depeuter-dev"; + name = config.networking.hostName; + labels = [ + "debian-11:docker://debian:11" + "debian-12:docker://debian:12" + "debian-latest:docker://debian:latest" + "docker:host" + "Linux:host" + "self-hosted:host" + "ubuntu-22.04:docker://ubuntu:22.04" + "ubuntu-24.04:docker://ubuntu:24.04" + "ubuntu-latest:docker://ubuntu:latest" + ]; + settings = { + cache.enabled = true; + container.privileged = true; + }; + hostPackages = with pkgs; [ + bash + cmake + coreutils + curl + docker + gawk + git + gnused + nodejs + openssh + wget + ]; + }; + }; + + }; +} diff --git a/modules/services/default.nix b/modules/services/default.nix new file mode 100644 index 0000000..f70bc54 --- /dev/null +++ b/modules/services/default.nix @@ -0,0 +1,6 @@ +{ + imports = [ + ./actions + ./openssh + ]; +} diff --git a/modules/services/openssh/default.nix b/modules/services/openssh/default.nix new file mode 100644 index 0000000..4b9cb5e --- /dev/null +++ b/modules/services/openssh/default.nix @@ -0,0 +1,20 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.homelab.services.openssh; +in { + options.homelab.services.openssh.enable = lib.mkEnableOption "OpenSSH daemon"; + + config = lib.mkIf cfg.enable { + services.openssh = { + # Enable the OpenSSH daemon. + enable = true; + settings = { + PasswordAuthentication = false; + PermitRootLogin = "no"; + # Disable keyboard-interactive authentication. + KbdInteractiveAuthentication = false; + }; + }; + }; +} diff --git a/modules/virtualisation/containers/default.nix b/modules/virtualisation/containers/default.nix new file mode 100644 index 0000000..ed87990 --- /dev/null +++ b/modules/virtualisation/containers/default.nix @@ -0,0 +1,23 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.homelab.virtualisation.containers; +in { + options.homelab.virtualisation.containers.enable = lib.mkEnableOption "OCI containers"; + + config = lib.mkIf cfg.enable { + environment.systemPackages = with pkgs; [ + nfs-utils + ]; + + virtualisation = { + docker = { + enable = true; + enableOnBoot = true; + autoPrune.enable = true; + }; + + oci-containers.backend = "docker"; + }; + }; +} diff --git a/modules/virtualisation/default.nix b/modules/virtualisation/default.nix new file mode 100644 index 0000000..2290be2 --- /dev/null +++ b/modules/virtualisation/default.nix @@ -0,0 +1,6 @@ +{ + imports = [ + ./containers + ./guest + ]; +} diff --git a/modules/virtualisation/guest/default.nix b/modules/virtualisation/guest/default.nix new file mode 100644 index 0000000..bbb9a50 --- /dev/null +++ b/modules/virtualisation/guest/default.nix @@ -0,0 +1,34 @@ +{ config, lib, modulesPath, ... }: + +let + cfg = config.homelab.virtualisation.guest; +in { + options.homelab.virtualisation.guest.enable = lib.mkEnableOption "Settings for devices running on virtualisation, e.g. Proxmox"; + + imports = [ + (modulesPath + "/profiles/qemu-guest.nix") + ]; + + config = lib.mkIf cfg.enable { + boot = { + # Whether to enable growing the root partition on boot. + growPartition = true; + # Use Grub bootloader + loader.grub = { + enable = true; + devices = [ + "nodev" + ]; + }; + }; + + fileSystems."/" = lib.mkDefault { + device = "/dev/disk/by-label/nixos"; + autoResize = true; + fsType = "ext4"; + }; + + # Enable QEMU Guest for Proxmox + services.qemuGuest.enable = true; + }; +} diff --git a/users/admin/default.nix b/users/admin/default.nix new file mode 100644 index 0000000..552909b --- /dev/null +++ b/users/admin/default.nix @@ -0,0 +1,33 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.homelab.users.admin; +in { + options.homelab.users.admin.enable = lib.mkEnableOption "user System Administrator"; + + config = lib.mkIf cfg.enable { + nix.settings.trusted-users = [ + config.users.users.admin.name + ]; + + users.users.admin = { + description = "System Administrator"; + isNormalUser = true; + extraGroups = [ + config.users.groups.wheel.name # Enable 'sudo' for the user. + ]; + initialPassword = "ChangeMe"; + openssh.authorizedKeys.keys = [ + # TODO ChangeMe + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPrG+ldRBdCeHEXrsy/qHXIJYg8xQXVuiUR0DxhFjYNg" + ]; + packages = with pkgs; [ + curl + git + tmux + vim + wget + ]; + }; + }; +} diff --git a/users/apps/default.nix b/users/apps/default.nix new file mode 100644 index 0000000..e4f7011 --- /dev/null +++ b/users/apps/default.nix @@ -0,0 +1,18 @@ +{ config, lib, ... }: + +let + cfg = config.homelab.users.apps; +in { + options.homelab.users.apps.enable = lib.mkEnableOption "user Apps"; + + config.users = lib.mkIf cfg.enable { + groups.apps.gid = lib.mkForce 568; + users.apps = { + uid = lib.mkForce 568; + isSystemUser = true; + group = config.users.groups.apps.name; + home = "/var/empty"; + shell = null; + }; + }; +} diff --git a/users/backup/default.nix b/users/backup/default.nix new file mode 100644 index 0000000..8181d02 --- /dev/null +++ b/users/backup/default.nix @@ -0,0 +1,26 @@ +{ config, lib, ... }: + +let + cfg = config.homelab.users.backup; +in { + options.homelab.users.backup.enable = lib.mkEnableOption "user Backup"; + + config = lib.mkIf cfg.enable { + users.users.backup = { + description = "Backup User"; + isNormalUser = true; + extraGroups = [ + "docker" # Allow access to the docker socket. + ]; + openssh.authorizedKeys.keys = [ + # TODO ChangeMe + + # Tibo-NixFat + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPrG+ldRBdCeHEXrsy/qHXIJYg8xQXVuiUR0DxhFjYNg" + + # Hugo + "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDAxR813vqq5zbu1NHrIybu5Imlu3k0rDCGxHiuGEhPoVV9c5FpnKNGLCi3ctm15ZcVBX4HcponYsKRBsCzM2pI4uXjxhHkLzbss5LttFuSzv5v/QHfLW1bvyJEMBEPxguGqAydAeWrBFdI9uHBEXeb325uKxMKBZHYvvpyAQ115c1wKy1bL8BfR0LTkhsFqexRvI86q59AVrAU/KFf6RXO0T9QA6H/vyWLlIPc7Ta+tSWwQ68bMmS5Pwn8q58tOAOAd6Lpt4TqUDJSppPjLEPKyKC6ShwMdEjwmwpEG0hxfsvaU8XERyQbSbEE9sLHRA2LoEdtMx3J8nzX3AwYUNspsqIv6NQZksnVqJ8OfL45ngUFcSJ6kBsUvCZfzEUGUTJ6Js0v84NOIXxNG/ZfPsk6ArXm3dvj2TYeK8llO6wpJnMMyztmmiODWoj9tepZSij44IgVM5wdWYIK/RZoYTsCQbmvJFfB8jhyJnf/7F19Vo5+LwhmCOsQh/KEK0F1DVc= admin@Hugo" + ]; + }; + }; +} diff --git a/users/default.nix b/users/default.nix new file mode 100644 index 0000000..fe82019 --- /dev/null +++ b/users/default.nix @@ -0,0 +1,9 @@ +{ + imports = [ + ./admin + ./apps + ./backup + ./deploy + ./media + ]; +} diff --git a/users/deploy/default.nix b/users/deploy/default.nix new file mode 100644 index 0000000..0509d1e --- /dev/null +++ b/users/deploy/default.nix @@ -0,0 +1,49 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.homelab.users.deploy; +in { + options.homelab.users.deploy.enable = lib.mkEnableOption "user Deploy"; + + config = lib.mkIf cfg.enable { + users = { + groups.deploy = { }; + + # The user used to deploy rebuilds without password authentication + users.deploy = { + group = config.users.groups.deploy.name; + isSystemUser = true; + home = "/var/empty"; + shell = pkgs.bashInteractive; + openssh.authorizedKeys.keys = [ + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPrG+ldRBdCeHEXrsy/qHXIJYg8xQXVuiUR0DxhFjYNg" + ]; + }; + }; + + security.sudo.extraRules = [ + { + groups = [ + config.users.groups.deploy.name + ]; + commands = [ + { + command = "/nix/store/*-nix-*/bin/nix-env -p /nix/var/nix/profile/system --set /nix/store/*-*"; + options = [ "NOPASSWD" ]; + } + ]; + } + { + groups = [ + config.users.groups.deploy.name + ]; + commands = [ + { + command = "/nix/store/*/bin/switch-to-configuration"; + options = [ "NOPASSWD" ]; + } + ]; + } + ]; + }; +} diff --git a/users/media/default.nix b/users/media/default.nix new file mode 100644 index 0000000..e70a2b3 --- /dev/null +++ b/users/media/default.nix @@ -0,0 +1,18 @@ +{ config, lib, ... }: + +let + cfg = config.homelab.users.media; +in { + options.homelab.users.media.enable = lib.mkEnableOption "user Media"; + + config.users = lib.mkIf cfg.enable { + groups.media.gid = lib.mkForce 3000; + users.media = { + uid = lib.mkForce 3001; + isSystemUser = true; + group = config.users.groups.media.name; + home = "/var/empty"; + shell = null; + }; + }; +} From 217fee5b3fbd2513a9ea98f63a4b803894f3cf59 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Wed, 12 Feb 2025 19:26:43 +0100 Subject: [PATCH 02/44] feat: Configure *arr stack --- modules/apps/arr/default.nix | 228 ++++++++++++++++++++++++++++------- 1 file changed, 183 insertions(+), 45 deletions(-) diff --git a/modules/apps/arr/default.nix b/modules/apps/arr/default.nix index a88ed9c..e45b1bf 100644 --- a/modules/apps/arr/default.nix +++ b/modules/apps/arr/default.nix @@ -42,6 +42,104 @@ in { }; fileSystems = lib.mkIf inUse { + "/srv/bazarr-backup" = lib.mkIf cfg.bazarr.enable { + device = "192.168.0.11:/mnt/BIG/BACKUP/BAZARR"; + fsType = "nfs"; + options = [ + "rw" + "auto" + "nfsvers=4.2" + "rsize=1048576" "wsize=1048576" + "hard" + "timeo=600" "retrans=2" + "_netdev" "nosuid" "tcp" + ]; + }; + + "/srv/lidarr-backup" = lib.mkIf cfg.lidarr.enable { + device = "192.168.0.11:/mnt/BIG/BACKUP/LIDARR"; + fsType = "nfs"; + options = [ + "rw" + "auto" + "nfsvers=4.2" + "rsize=1048576" "wsize=1048576" + "hard" + "timeo=600" "retrans=2" + "_netdev" "nosuid" "tcp" + ]; + }; + + "/srv/prowlarr-backup" = lib.mkIf cfg.prowlarr.enable { + device = "192.168.0.11:/mnt/BIG/BACKUP/PROWLARR"; + fsType = "nfs"; + options = [ + "rw" + "auto" + "nfsvers=4.2" + "rsize=1048576" "wsize=1048576" + "hard" + "timeo=600" "retrans=2" + "_netdev" "nosuid" "tcp" + ]; + }; + + "/srv/qbittorrent" = lib.mkIf cfg.qbittorrent.enable { + device = "192.168.0.11:/mnt/SMALL/CONFIG/QBITTORRENT"; + fsType = "nfs"; + options = [ + "rw" + "auto" + "nfsvers=4.2" + "rsize=1048576" "wsize=1048576" + "hard" + "timeo=600" "retrans=2" + "_netdev" "nosuid" "tcp" + ]; + }; + + "/srv/radarr-backup" = lib.mkIf cfg.radarr.enable { + device = "192.168.0.11:/mnt/BIG/BACKUP/RADARR"; + fsType = "nfs"; + options = [ + "rw" + "auto" + "nfsvers=4.2" + "rsize=1048576" "wsize=1048576" + "hard" + "timeo=600" "retrans=2" + "_netdev" "nosuid" "tcp" + ]; + }; + + "/srv/sonarr-backup" = lib.mkIf cfg.sonarr.enable { + device = "192.168.0.11:/mnt/BIG/BACKUP/SONARR"; + fsType = "nfs"; + options = [ + "rw" + "auto" + "nfsvers=4.2" + "rsize=1048576" "wsize=1048576" + "hard" + "timeo=600" "retrans=2" + "_netdev" "nosuid" "tcp" + ]; + }; + + "/srv/torrent" = { + device = "192.168.0.11:/mnt/SMALL/MEDIA/TORRENT"; + fsType = "nfs"; + options = [ + "rw" + "auto" + "nfsvers=4.2" + "rsize=1048576" "wsize=1048576" + "hard" + "timeo=600" "retrans=2" + "_netdev" "nosuid" "tcp" + ]; + }; + "/srv/video" = { device = "192.168.0.11:/mnt/SMALL/MEDIA/VIDEO"; fsType = "nfs"; @@ -55,20 +153,6 @@ in { "_netdev" "nosuid" "tcp" ]; }; - - "/srv/qbittorrent" = { - device = "192.168.0.11:/mnt/SMALL/CONFIG/QBITTORRENT"; - fsType = "nfs"; - options = [ - "rw" - "auto" - "nfsvers=4.2" - "hard" - "rsize=1048576" "wsize=1048576" - "timeo=600" "retrans=2" - "_netdev" "nosuid" "tcp" - ]; - }; }; # Make sure the Docker network exists. @@ -143,42 +227,53 @@ in { }; virtualisation.oci-containers.containers = { - bazarr = lib.mkIf cfg.bazarr.enable { + bazarr = let + port = 6767; + in lib.mkIf cfg.bazarr.enable { hostname = "bazarr"; image = "ghcr.io/hotio/bazarr:release-1.4.4"; autoStart = true; ports = [ - "6767:6767/tcp" - "6767:6767/udp" + # Open ports if you don't use Traefik + # "${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"'' ]; 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" + + "/srv/bazarr-backup:/config/backup" + + "/srv/video/Films:/media/movies" + "/srv/video/Series:/media/series" ]; + labels = { + "traefik.enable" = "true"; + "traefik.http.routers.bazarr.rule" = "Host(`bazarr.depeuter.dev`)"; + "traefik.http.services.bazarr.loadbalancer.server.port" = toString port; + }; }; - lidarr = lib.mkIf cfg.lidarr.enable { + lidarr = let + port = 8686; + in lib.mkIf cfg.lidarr.enable { hostname = "lidarr"; image = "ghcr.io/hotio/lidarr:release-2.5.3.4341"; autoStart = true; ports = [ - "8686:8686/tcp" + # Open ports if you don't use Traefik + # "${toString port}:${toString port}/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; @@ -187,16 +282,21 @@ in { }; volumes = [ "lidarr-config:/config" - # TODO "data:/data" + + # TODO Fix path + "/srv/lidarr-backup:/media/Backups" ]; }; - 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"; autoStart = true; ports = [ - "9696:9696/tcp" + # Open ports if you don't use Traefik + # "${toString port}:${toString port}/tcp" ]; extraOptions = [ "--network=${networkName}" @@ -207,41 +307,58 @@ in { TZ = config.time.timeZone; }; volumes = [ - # TODO "config:/config" + "prowlarr-config:/config" + + "/srv/prowlarr-backup:/config/Backups" ]; + labels = { + "traefik.enable" = "true"; + "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"; autoStart = true; ports = [ - "10095:10095/udp" - "10095:10095/tcp" + # Open ports if you don't use Traefik + # "${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,auto,nfsvers=4.2,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,_netdev,nosuid,tcp"'' ]; 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.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"; autoStart = true; ports = [ - "7878:7878/tcp" + # Open ports if you don't use Traefik + # "${toString port}:${toString port}/tcp" ]; extraOptions = [ "--network=${networkName}" @@ -252,17 +369,29 @@ in { TZ = config.time.timeZone; }; volumes = [ - # TODO "config:/config" - # TODO "data:/data" + "radarr-config:/config" + + "/srv/radarr-backup:/config/Backups" + + "/srv/torrent:/media/cache" + "/srv/video/Films:/media/movies" ]; + labels = { + "traefik.enable" = "true"; + "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"; autoStart = true; ports = [ - "8989:8989/tcp" + # Open ports if you don't use Traefik + # "${toString port}:${toString port}/tcp" ]; extraOptions = [ "--network=${networkName}" @@ -273,9 +402,18 @@ in { TZ = config.time.timeZone; }; volumes = [ - # TODO "config:/config" - # TODO "data:/data" + "sonarr-config:/config" + + "/srv/sonarr-backup:/config/Backups" + + "/srv/torrent:/media/cache" + "/srv/video/Series:/media/series" ]; + labels = { + "traefik.enable" = "true"; + "traefik.http.routers.sonarr.rule" = "Host(`sonarr.depeuter.dev`)"; + "traefik.http.services.sonarr.loadbalancer.server.port" = toString port; + }; }; }; }; From 8c3bb2b3cea9efe1a2202c2bf533029aab06dbd6 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Fri, 21 Mar 2025 21:53:33 +0100 Subject: [PATCH 03/44] feat: Plex without synchronized config --- modules/apps/plex/default.nix | 44 ++++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/modules/apps/plex/default.nix b/modules/apps/plex/default.nix index 251f9dd..7e21745 100644 --- a/modules/apps/plex/default.nix +++ b/modules/apps/plex/default.nix @@ -6,14 +6,37 @@ in { options.homelab.apps.plex.enable = lib.mkEnableOption "Plex"; config = lib.mkIf cfg.enable { + homelab = { + users = { + apps.enable = true; + media.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; }; + fileSystems."/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" + ]; + }; + virtualisation.oci-containers.containers = { plex = { hostname = "plex"; @@ -30,20 +53,25 @@ in { # "8324:8324/tcp" # Controlling Plex for Roku via Plex Companion ]; environment = { - ADVERTISE_AP = "..."; # TODO Configure ip + #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; + #PLEX_CLAIM = "..."; # TODO Add token + PLEX_UID = toString config.users.users.plex.uid; + PLEX_GID = toString config.users.groups.media.gid; TZ = config.time.timeZone; }; volumes = [ - # TODO "config:/var/lib/plexmediaserver" - # TODO "transcode-temp:/transcode" - # TODO "media:/data" + "plex-config:/var/lib/plexmediaserver" + "plex-transcode:/transcode" + "/srv/video:/data/video" ]; + labels = { + "traefik.enable" = "true"; + "traefik.http.routers.plex.rule=" = "Host(`plex.depeuter.dev`)"; + "traefik.http.services.plex.loadbalancer.server.port" = "32400"; + }; }; }; }; From f3090538d802d4cfa5d8b239581ef727a69466ee Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Fri, 28 Mar 2025 12:37:27 +0100 Subject: [PATCH 04/44] chore: Update Vaultwarden --- hosts/Vaultwarden/default.nix | 6 +++- modules/apps/vaultwarden/default.nix | 42 ++++++++++++++++++++-------- 2 files changed, 36 insertions(+), 12 deletions(-) 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/vaultwarden/default.nix b/modules/apps/vaultwarden/default.nix index 6d06287..a2f8d0a 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 = { + virtualisation.oci-containers.containers = let + dbHostname = "vaultwarden-db"; + dbPort = 5432; + in { vaultwarden-db = { - hostname = "vaultwarden-db"; + 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.32.5-alpine"; + image = "vaultwarden/server:1.33.2-alpine"; autoStart = true; ports = [ - "10102:80/tcp" + "${toString cfg.port}:80/tcp" ]; extraOptions = [ "--network=${networkName}" ]; dependsOn = [ - "vaultwarden-db" + dbHostname ]; 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) @@ -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 From c541fa4e6e2d07dd7a4da538d4e35b4ce09285db Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Sat, 24 May 2025 20:04:26 +0200 Subject: [PATCH 05/44] feat: Calibre (Desktop + Web) --- modules/apps/calibre/default.nix | 171 +++++++++++++++++++++++++++++-- 1 file changed, 160 insertions(+), 11 deletions(-) diff --git a/modules/apps/calibre/default.nix b/modules/apps/calibre/default.nix index 6fddb81..fc7cd57 100644 --- a/modules/apps/calibre/default.nix +++ b/modules/apps/calibre/default.nix @@ -1,17 +1,166 @@ -{ 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; + + networkName = "calibre"; +in { + options.homelab.apps.calibre = { + enable = lib.mkEnableOption "Calibre (Desktop + Web)"; + desktop = lib.mkEnableOption "Calibre Desktop (KasmVNC)"; + web = lib.mkEnableOption "Calibre Web"; }; + + config = lib.mkMerge [ + { + homelab.apps.calibre = lib.mkIf cfg.enable { + desktop = true; + web = true; + }; + } + + # Common + (lib.mkIf (cfg.desktop || cfg.web) { + 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."/srv/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 + { + fileSystems."/srv/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 = { + hostname = "calibre"; + image = "lscr.io/linuxserver/calibre:latest"; + autoStart = true; + ports = [ + # Open ports if you don't use Traefik + "9480:8080" # Calibre desktop GUI + #"9481:8181" # Calibre desktop GUI HTTPS + #"9581:8081" # Calibre webserver gui + ]; + extraOptions = [ + "--network=${networkName}" + + # syscalls are unkown to Docker + #"--security-opt" "seccomp=unconfined" + ]; + environment = { + inherit PUID PGID; + #UMASK = "022"; + + TZ = config.time.timeZone; + + #PASSWORD = ""; + #CLI_ARGS = ""; + }; + volumes = [ + "/srv/calibre-config:/config" + + "/srv/books:/media/books" + ]; + }; + } + + # Calibre Web + { + fileSystems."/srv/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 = { + hostname = "calibre-web"; + image = "lscr.io/linuxserver/calibre-web:latest"; + autoStart = true; + ports = [ + # Open ports if you don't use Traefik + "8083:8083" # Web UI + ]; + extraOptions = [ + "--network=${networkName}" + ]; + 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 = [ + "/srv/calibre-web-config:/config" + + "/srv/books:/media/books" + ]; + }; + } + ]; } From 48fb68c2fd24a0ee2815402bd88a3290f83a290e Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Sun, 25 May 2025 14:32:47 +0200 Subject: [PATCH 06/44] fix: Separate nfs config --- modules/apps/arr/default.nix | 31 +++----- modules/apps/plex/default.nix | 81 +++++++++------------ modules/fileSystems/default.nix | 5 ++ modules/fileSystems/media/default.nix | 5 ++ modules/fileSystems/media/video/default.nix | 42 +++++++++++ 5 files changed, 100 insertions(+), 64 deletions(-) create mode 100644 modules/fileSystems/default.nix create mode 100644 modules/fileSystems/media/default.nix create mode 100644 modules/fileSystems/media/video/default.nix diff --git a/modules/apps/arr/default.nix b/modules/apps/arr/default.nix index e45b1bf..2687e2a 100644 --- a/modules/apps/arr/default.nix +++ b/modules/apps/arr/default.nix @@ -38,6 +38,11 @@ in { sonarr.enable = true; }; + fileSystems.media.video = { + enable = true; + permissions = [ "read" "write" ]; + }; + virtualisation.containers.enable = lib.mkIf inUse true; }; @@ -139,20 +144,6 @@ in { "_netdev" "nosuid" "tcp" ]; }; - - "/srv/video" = { - device = "192.168.0.11:/mnt/SMALL/MEDIA/VIDEO"; - fsType = "nfs"; - options = [ - "rw" - "auto" - "nfsvers=4.2" - "soft" - "rsize=1048576" "wsize=1048576" - "timeo=600" "retrans=2" - "_netdev" "nosuid" "tcp" - ]; - }; }; # Make sure the Docker network exists. @@ -226,7 +217,9 @@ in { }; }; - virtualisation.oci-containers.containers = { + virtualisation.oci-containers.containers = let + videoHostPath = config.homelab.fileSystems.media.video.hostPath; + in { bazarr = let port = 6767; in lib.mkIf cfg.bazarr.enable { @@ -252,8 +245,8 @@ in { "/srv/bazarr-backup:/config/backup" - "/srv/video/Films:/media/movies" - "/srv/video/Series:/media/series" + "${videoHostPath}/Films:/media/movies" + "${videoHostPath}/Series:/media/series" ]; labels = { "traefik.enable" = "true"; @@ -374,7 +367,7 @@ in { "/srv/radarr-backup:/config/Backups" "/srv/torrent:/media/cache" - "/srv/video/Films:/media/movies" + "${videoHostPath}/Films:/media/movies" ]; labels = { "traefik.enable" = "true"; @@ -407,7 +400,7 @@ in { "/srv/sonarr-backup:/config/Backups" "/srv/torrent:/media/cache" - "/srv/video/Series:/media/series" + "${videoHostPath}/Series:/media/series" ]; labels = { "traefik.enable" = "true"; diff --git a/modules/apps/plex/default.nix b/modules/apps/plex/default.nix index 7e21745..b307b86 100644 --- a/modules/apps/plex/default.nix +++ b/modules/apps/plex/default.nix @@ -11,6 +11,7 @@ in { apps.enable = true; media.enable = true; }; + fileSystems.media.video.enable = true; virtualisation.containers.enable = true; }; @@ -25,53 +26,43 @@ in { shell = null; }; - fileSystems."/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" + 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" - 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 = toString config.users.users.plex.uid; - PLEX_GID = toString config.users.groups.media.gid; - TZ = config.time.timeZone; - }; - volumes = [ - "plex-config:/var/lib/plexmediaserver" - "plex-transcode:/transcode" - "/srv/video:/data/video" - ]; - labels = { - "traefik.enable" = "true"; - "traefik.http.routers.plex.rule=" = "Host(`plex.depeuter.dev`)"; - "traefik.http.services.plex.loadbalancer.server.port" = "32400"; - }; + "${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/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" + ]; + }; + }; +} From c294e159e2a2fed0b1265aff92e18ea3e7007f5b Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Mon, 26 May 2025 22:38:29 +0200 Subject: [PATCH 07/44] feat: Basic recursive dns --- modules/apps/bind9/db.depeuter.dev | 16 ++++++++ modules/apps/bind9/default.nix | 54 +++++++++++++++++++++++++++ modules/apps/bind9/named.conf | 2 + modules/apps/bind9/named.conf.local | 4 ++ modules/apps/bind9/named.conf.options | 35 +++++++++++++++++ modules/apps/default.nix | 1 + 6 files changed, 112 insertions(+) create mode 100644 modules/apps/bind9/db.depeuter.dev create mode 100644 modules/apps/bind9/default.nix create mode 100644 modules/apps/bind9/named.conf create mode 100644 modules/apps/bind9/named.conf.local create mode 100644 modules/apps/bind9/named.conf.options diff --git a/modules/apps/bind9/db.depeuter.dev b/modules/apps/bind9/db.depeuter.dev new file mode 100644 index 0000000..fbd06c3 --- /dev/null +++ b/modules/apps/bind9/db.depeuter.dev @@ -0,0 +1,16 @@ +$TTL 604800 +@ IN SOA ns1.depeuter.dev. admin.depeuter.dev. ( + 5 ; Serial + 604800 ; Refresh + 86400 ; Retry + 2419200 ; Expire + 604800 ) ; Negative Cache TTL + +; name servers - NS records + IN NS ns1.depeuter.dev. +; IN NS ns2.depeuter.dev. + +ns1.depeuter.dev. IN A 192.168.0.91 +;ns1.depeuter.dev. IN A 192.158.0.X + +hugo.depeuter.dev. IN A 192.168.0.11 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/default.nix b/modules/apps/default.nix index 2d487e8..81c6a06 100644 --- a/modules/apps/default.nix +++ b/modules/apps/default.nix @@ -1,6 +1,7 @@ { imports = [ ./arr + ./bind9 ./calibre ./changedetection ./freshrss From d693c4a93b0aa75204465269d79d050cb0c0d1c5 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Thu, 26 Jun 2025 11:50:52 +0200 Subject: [PATCH 08/44] feat(calibre): Pin image versions --- modules/apps/calibre/default.nix | 64 ++++++++++++++++++++------------ 1 file changed, 41 insertions(+), 23 deletions(-) diff --git a/modules/apps/calibre/default.nix b/modules/apps/calibre/default.nix index fc7cd57..aa00c89 100644 --- a/modules/apps/calibre/default.nix +++ b/modules/apps/calibre/default.nix @@ -6,24 +6,28 @@ let 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"; in { options.homelab.apps.calibre = { - enable = lib.mkEnableOption "Calibre (Desktop + Web)"; - desktop = lib.mkEnableOption "Calibre Desktop (KasmVNC)"; - web = lib.mkEnableOption "Calibre Web"; + 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 = true; - web = true; + desktop.enable = true; + web.enable = true; }; } # Common - (lib.mkIf (cfg.desktop || cfg.web) { + (lib.mkIf (cfg.desktop.enable || cfg.web.enable) { homelab = { users.media.enable = true; virtualisation.containers.enable = true; @@ -37,7 +41,7 @@ in { shell = null; }; - fileSystems."/srv/books" = { + fileSystems."${books}" = { device = "192.168.0.11:/mnt/SMALL/MEDIA/BOOKS"; fsType = "nfs"; options = [ @@ -69,8 +73,8 @@ in { }) # Calibre desktop - { - fileSystems."/srv/calibre-config" = { + (lib.mkIf cfg.desktop.enable { + fileSystems."${calibre-config}" = { device = "192.168.0.11:/mnt/SMALL/CONFIG/CALIBRE"; fsType = "nfs"; options = [ @@ -84,13 +88,15 @@ in { ]; }; - virtualisation.oci-containers.containers.calibre = { + virtualisation.oci-containers.containers.calibre = let + innerPort = 8080; + in { hostname = "calibre"; - image = "lscr.io/linuxserver/calibre:latest"; + image = "lscr.io/linuxserver/calibre:8.5.0"; autoStart = true; ports = [ # Open ports if you don't use Traefik - "9480:8080" # Calibre desktop GUI + "9480:${toString innerPort}" # Calibre desktop GUI #"9481:8181" # Calibre desktop GUI HTTPS #"9581:8081" # Calibre webserver gui ]; @@ -110,16 +116,21 @@ in { #CLI_ARGS = ""; }; volumes = [ - "/srv/calibre-config:/config" + "${calibre-config}:/config" - "/srv/books:/media/books" + "${books}:/media/books" ]; + labels = { + "traefik.enable" = "true"; + "traefik.http.routers.calibre.rule" = "Host(`calibre.depeuter.dev`)"; + "traefik.http.services.calibre.loadbalancer.server.port" = toString innerPort; + }; }; - } + }) # Calibre Web - { - fileSystems."/srv/calibre-web-config" = { + (lib.mkIf cfg.web.enable { + fileSystems."${calibre-web-config}" = { device = "192.168.0.11:/mnt/SMALL/CONFIG/CALIBRE-WEB"; fsType = "nfs"; options = [ @@ -133,13 +144,15 @@ in { ]; }; - virtualisation.oci-containers.containers.calibre-web = { + virtualisation.oci-containers.containers.calibre-web = let + innerPort = 8083; + in { hostname = "calibre-web"; - image = "lscr.io/linuxserver/calibre-web:latest"; + image = "lscr.io/linuxserver/calibre-web:0.6.24"; autoStart = true; ports = [ # Open ports if you don't use Traefik - "8083:8083" # Web UI + "8083:${toString innerPort}" # Web UI ]; extraOptions = [ "--network=${networkName}" @@ -156,11 +169,16 @@ in { #OAUTHLIB_RELAX_TOKEN_SCOPE = "1"; }; volumes = [ - "/srv/calibre-web-config:/config" + "${calibre-web-config}:/config" - "/srv/books:/media/books" + "${books}:/media/books" ]; + labels = { + "traefik.enable" = "true"; + "traefik.http.routers.calibre-web.rule" = "Host(`books.depeuter.dev`)"; + "traefik.http.services.calibre-web.loadbalancer.server.port" = toString innerPort; + }; }; - } + }) ]; } From d0d6fac7ef9410ca0471d5c9f9bd0cc7554ad177 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Mon, 1 Sep 2025 18:06:25 +0200 Subject: [PATCH 09/44] chore(vaultwarden): Update image --- modules/apps/vaultwarden/default.nix | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/apps/vaultwarden/default.nix b/modules/apps/vaultwarden/default.nix index a2f8d0a..4510299 100644 --- a/modules/apps/vaultwarden/default.nix +++ b/modules/apps/vaultwarden/default.nix @@ -54,7 +54,7 @@ in { dbHostname = "vaultwarden-db"; dbPort = 5432; in { - vaultwarden-db = { + vaultwardenDb = { hostname = dbHostname; image = "postgres:15.8-alpine"; autoStart = true; @@ -77,7 +77,7 @@ in { dataDir = "/data"; in { hostname = "vaultwarden"; - image = "vaultwarden/server:1.33.2-alpine"; + image = "vaultwarden/server:1.34.3-alpine"; autoStart = true; ports = [ "${toString cfg.port}:80/tcp" @@ -86,7 +86,7 @@ in { "--network=${networkName}" ]; dependsOn = [ - dbHostname + "vaultwardenDb" ]; volumes = [ "vaultwarden:${dataDir}" From ac47ec4689a227d71c928250589409be0db60ca9 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Thu, 4 Sep 2025 10:39:02 +0200 Subject: [PATCH 10/44] fix(traefik): Add proxy network --- modules/apps/arr/default.nix | 8 +++ modules/apps/traefik/default.nix | 90 ++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 modules/apps/traefik/default.nix diff --git a/modules/apps/arr/default.nix b/modules/apps/arr/default.nix index 2687e2a..de696f5 100644 --- a/modules/apps/arr/default.nix +++ b/modules/apps/arr/default.nix @@ -4,6 +4,8 @@ let cfg = config.homelab.apps.arr; networkName = "arrStack"; + proxyNet = config.homelab.apps.traefik.sharedNetworkName; + appNames = [ "bazarr" "lidarr" "prowlarr" "qbittorrent" "radarr" "sonarr" ]; inUse = builtins.any (app: cfg.${app}.enable) appNames; @@ -233,6 +235,7 @@ in { ]; extraOptions = [ "--network=${networkName}" + "--network=${proxyNet}" ]; environment = { PUID = toString config.users.users.bazarr.uid; @@ -267,6 +270,7 @@ in { ]; extraOptions = [ "--network=${networkName}" + "--network=${proxyNet}" ]; environment = { PUID = toString config.users.users.lidarr.uid; @@ -293,6 +297,7 @@ in { ]; extraOptions = [ "--network=${networkName}" + "--network=${proxyNet}" ]; environment = { PUID = toString config.users.users.prowlarr.uid; @@ -324,6 +329,7 @@ in { ]; extraOptions = [ "--network=${networkName}" + "--network=${proxyNet}" ]; environment = { PUID = toString config.users.users.qbittorrent.uid; @@ -355,6 +361,7 @@ in { ]; extraOptions = [ "--network=${networkName}" + "--network=${proxyNet}" ]; environment = { PUID = toString config.users.users.radarr.uid; @@ -388,6 +395,7 @@ in { ]; extraOptions = [ "--network=${networkName}" + "--network=${proxyNet}" ]; environment = { PUID = toString config.users.users.sonarr.uid; 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; + }; + }; + }; +} From a7f56a7cf73b7a3c8f385e983b6d6af25ae702db Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Thu, 4 Sep 2025 10:46:41 +0200 Subject: [PATCH 11/44] fix(traefik): Specify network --- modules/apps/arr/default.nix | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/modules/apps/arr/default.nix b/modules/apps/arr/default.nix index de696f5..cedd01e 100644 --- a/modules/apps/arr/default.nix +++ b/modules/apps/arr/default.nix @@ -253,6 +253,7 @@ in { ]; 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; }; @@ -283,6 +284,12 @@ in { # TODO Fix path "/srv/lidarr-backup:/media/Backups" ]; + labels = { + "traefik.enable" = "true"; + "traefik.docker.network" = proxyNet; + "traefik.http.routers.lidarr.rule" = "Host(`lidarr.depeuter.dev`)"; + "traefik.http.services.lidarr.loadbalancer.server.port" = toString port; + }; }; prowlarr = let @@ -311,6 +318,7 @@ in { ]; 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; }; @@ -344,6 +352,7 @@ in { ]; 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; }; @@ -378,6 +387,7 @@ in { ]; 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; }; @@ -412,6 +422,7 @@ in { ]; 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; }; From badc78ece6eca01060d06a1d5fece1353452578d Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Fri, 5 Sep 2025 10:24:55 +0200 Subject: [PATCH 12/44] chore: Add new host --- flake.nix | 1 + hosts/ProductionArr/default.nix | 44 +++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 hosts/ProductionArr/default.nix diff --git a/flake.nix b/flake.nix index 48c91de..44d63c2 100644 --- a/flake.nix +++ b/flake.nix @@ -41,6 +41,7 @@ Binnenpost.modules = [ ./hosts/Binnenpost ]; Production.modules = [ ./hosts/Production ]; ProductionGPU.modules = [ ./hosts/ProductionGPU ]; + ProductionArr.modules = [ ./hosts/ProductionArr ]; ACE.modules = [ ./hosts/ACE ]; Template.modules = [ ./hosts/Template ]; diff --git a/hosts/ProductionArr/default.nix b/hosts/ProductionArr/default.nix new file mode 100644 index 0000000..3a430d9 --- /dev/null +++ b/hosts/ProductionArr/default.nix @@ -0,0 +1,44 @@ +{ config, pkgs, lib, system, ... }: + +{ + config = { + homelab = { + 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"; + }; +} From d9e020a0c7ab3fff27460af7d40bf16632e46794 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Fri, 5 Sep 2025 10:28:00 +0200 Subject: [PATCH 13/44] chore(arr): Move to new host --- hosts/Development/default.nix | 6 +++--- hosts/ProductionArr/default.nix | 10 ++++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/hosts/Development/default.nix b/hosts/Development/default.nix index da995f8..b2237b7 100644 --- a/hosts/Development/default.nix +++ b/hosts/Development/default.nix @@ -4,9 +4,9 @@ config = { homelab = { apps = { - arr = { - qbittorrent.enable = true; - }; + bind9.enable = true; + traefik.enable = true; + plex.enable = true; }; virtualisation.guest.enable = true; }; diff --git a/hosts/ProductionArr/default.nix b/hosts/ProductionArr/default.nix index 3a430d9..929750a 100644 --- a/hosts/ProductionArr/default.nix +++ b/hosts/ProductionArr/default.nix @@ -3,6 +3,16 @@ { config = { homelab = { + apps = { + arr = { + bazarr.enable = true; + prowlarr.enable = true; + qbittorrent.enable = true; + radarr.enable = true; + sonarr.enable = true; + }; + traefik.enable = true; + }; virtualisation.guest.enable = true; }; From 9f5c9ecdd9d51cc512c275874e1c4e607d0dea8a Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Fri, 5 Sep 2025 10:30:57 +0200 Subject: [PATCH 14/44] chore(arr): Remove lidarr config --- hosts/ProductionArr/default.nix | 8 +---- modules/apps/arr/default.nix | 59 +-------------------------------- 2 files changed, 2 insertions(+), 65 deletions(-) diff --git a/hosts/ProductionArr/default.nix b/hosts/ProductionArr/default.nix index 929750a..ff4f4c2 100644 --- a/hosts/ProductionArr/default.nix +++ b/hosts/ProductionArr/default.nix @@ -4,13 +4,7 @@ config = { homelab = { apps = { - arr = { - bazarr.enable = true; - prowlarr.enable = true; - qbittorrent.enable = true; - radarr.enable = true; - sonarr.enable = true; - }; + arr.enable = true; traefik.enable = true; }; virtualisation.guest.enable = true; diff --git a/modules/apps/arr/default.nix b/modules/apps/arr/default.nix index cedd01e..0ce41a3 100644 --- a/modules/apps/arr/default.nix +++ b/modules/apps/arr/default.nix @@ -6,7 +6,7 @@ let networkName = "arrStack"; proxyNet = config.homelab.apps.traefik.sharedNetworkName; - appNames = [ "bazarr" "lidarr" "prowlarr" "qbittorrent" "radarr" "sonarr" ]; + appNames = [ "bazarr" "prowlarr" "qbittorrent" "radarr" "sonarr" ]; inUse = builtins.any (app: cfg.${app}.enable) appNames; PGID = toString config.users.groups.media.gid; @@ -16,7 +16,6 @@ in { enable = lib.mkEnableOption "Arr Stack using Docker"; 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"; @@ -33,7 +32,6 @@ in { # "Master switch": Enable all apps. apps.arr = lib.mkIf cfg.enable { bazarr.enable = true; - lidarr.enable = true; prowlarr.enable = true; qbittorrent.enable = true; radarr.enable = true; @@ -63,20 +61,6 @@ in { ]; }; - "/srv/lidarr-backup" = lib.mkIf cfg.lidarr.enable { - device = "192.168.0.11:/mnt/BIG/BACKUP/LIDARR"; - fsType = "nfs"; - options = [ - "rw" - "auto" - "nfsvers=4.2" - "rsize=1048576" "wsize=1048576" - "hard" - "timeo=600" "retrans=2" - "_netdev" "nosuid" "tcp" - ]; - }; - "/srv/prowlarr-backup" = lib.mkIf cfg.prowlarr.enable { device = "192.168.0.11:/mnt/BIG/BACKUP/PROWLARR"; fsType = "nfs"; @@ -153,7 +137,6 @@ in { description = "Create Docker network for ${networkName}"; requiredBy = [ "docker-bazarr.service" - "docker-lidarr.service" "docker-prowlarr.service" "docker-qbittorrent.service" "docker-radarr.service" @@ -179,13 +162,6 @@ in { 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; @@ -259,39 +235,6 @@ in { }; }; - lidarr = let - port = 8686; - in lib.mkIf cfg.lidarr.enable { - hostname = "lidarr"; - image = "ghcr.io/hotio/lidarr:release-2.5.3.4341"; - autoStart = true; - ports = [ - # Open ports if you don't use Traefik - # "${toString port}:${toString port}/tcp" - ]; - extraOptions = [ - "--network=${networkName}" - "--network=${proxyNet}" - ]; - environment = { - PUID = toString config.users.users.lidarr.uid; - inherit PGID UMASK; - TZ = config.time.timeZone; - }; - volumes = [ - "lidarr-config:/config" - - # TODO Fix path - "/srv/lidarr-backup:/media/Backups" - ]; - labels = { - "traefik.enable" = "true"; - "traefik.docker.network" = proxyNet; - "traefik.http.routers.lidarr.rule" = "Host(`lidarr.depeuter.dev`)"; - "traefik.http.services.lidarr.loadbalancer.server.port" = toString port; - }; - }; - prowlarr = let port = 9696; in lib.mkIf cfg.prowlarr.enable { From 5f027ca12a6f425fd210172caf6fbf279b394f1e Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Fri, 5 Sep 2025 10:35:46 +0200 Subject: [PATCH 15/44] fix(bind9): Update arr records --- modules/apps/bind9/db.depeuter.dev | 53 +++++++++++++++++++++++------- 1 file changed, 41 insertions(+), 12 deletions(-) diff --git a/modules/apps/bind9/db.depeuter.dev b/modules/apps/bind9/db.depeuter.dev index fbd06c3..72f3825 100644 --- a/modules/apps/bind9/db.depeuter.dev +++ b/modules/apps/bind9/db.depeuter.dev @@ -1,16 +1,45 @@ $TTL 604800 -@ IN SOA ns1.depeuter.dev. admin.depeuter.dev. ( - 5 ; Serial - 604800 ; Refresh - 86400 ; Retry - 2419200 ; Expire - 604800 ) ; Negative Cache TTL +@ IN SOA ns1 admin ( + 15 ; Serial + 604800 ; Refresh + 86400 ; Retry + 2419200 ; Expire + 604800 ) ; Negative Cache TTL -; name servers - NS records - IN NS ns1.depeuter.dev. -; IN NS ns2.depeuter.dev. +; Name servers - NS records + IN NS ns1 +; IN NS ns2 -ns1.depeuter.dev. IN A 192.168.0.91 -;ns1.depeuter.dev. IN A 192.158.0.X +ns1 IN A 192.168.0.91 +;ns2 IN A 192.158.0.X -hugo.depeuter.dev. IN A 192.168.0.11 +; 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 From 74e4e8dcd6e76cbbcc77dd542319d2882e5f9661 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Fri, 5 Sep 2025 14:19:34 +0200 Subject: [PATCH 16/44] feat(arr): Add exposePorts option --- modules/apps/arr/default.nix | 80 ++++++++++++++++++++++++++---------- 1 file changed, 58 insertions(+), 22 deletions(-) diff --git a/modules/apps/arr/default.nix b/modules/apps/arr/default.nix index 0ce41a3..5cff39c 100644 --- a/modules/apps/arr/default.nix +++ b/modules/apps/arr/default.nix @@ -14,12 +14,53 @@ let in { options.homelab.apps.arr = { 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"; - 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 = { + enable = lib.mkEnableOption "Bazarr using Docker"; + exposePorts = lib.mkOption { + type = lib.types.bool; + description = "Expose Bazarr port"; + default = cfg.exposePorts; + }; + }; + prowlarr = { + enable = lib.mkEnableOption "Prowlarr using Docker"; + exposePorts = lib.mkOption { + type = lib.types.bool; + description = "Expose Prowlarr port"; + default = cfg.exposePorts; + }; + }; + qbittorrent = { + enable = lib.mkEnableOption "qBittorrent using Docker"; + exposePorts = lib.mkOption { + type = lib.types.bool; + description = "Expose qBittorrent port"; + default = cfg.exposePorts; + }; + }; + radarr = { + enable = lib.mkEnableOption "Radarr using Docker"; + exposePorts = lib.mkOption { + type = lib.types.bool; + description = "Expose Radarr port"; + default = cfg.exposePorts; + }; + }; + sonarr = { + enable = lib.mkEnableOption "Sonarr using Docker"; + exposePorts = lib.mkOption { + type = lib.types.bool; + description = "Expose Sonarr port"; + default = cfg.exposePorts; + }; + }; }; config = { @@ -204,10 +245,9 @@ in { hostname = "bazarr"; image = "ghcr.io/hotio/bazarr:release-1.4.4"; autoStart = true; - ports = [ - # Open ports if you don't use Traefik - # "${toString port}:${toString port}/tcp" - # "${toString port}:${toString port}/udp" + ports = lib.mkIf cfg.bazarr.exposePorts [ + "${toString port}:${toString port}/tcp" + "${toString port}:${toString port}/udp" ]; extraOptions = [ "--network=${networkName}" @@ -241,9 +281,8 @@ in { hostname = "prowlarr"; image = "ghcr.io/hotio/prowlarr:release-1.23.1.4708"; autoStart = true; - ports = [ - # Open ports if you don't use Traefik - # "${toString port}:${toString port}/tcp" + ports = lib.mkIf cfg.prowlarr.exposePorts [ + "${toString port}:${toString port}/tcp" ]; extraOptions = [ "--network=${networkName}" @@ -273,10 +312,9 @@ in { hostname = "qbittorrent"; image = "ghcr.io/hotio/qbittorrent:release-4.6.7"; autoStart = true; - ports = [ - # Open ports if you don't use Traefik - # "${toString port}:${toString port}/tcp" - # "${toString port}:${toString port}/udp" + ports = lib.mkIf cfg.qbittorrent.exposePorts [ + "${toString port}:${toString port}/tcp" + "${toString port}:${toString port}/udp" ]; extraOptions = [ "--network=${networkName}" @@ -307,9 +345,8 @@ in { hostname = "radarr"; image = "ghcr.io/hotio/radarr:release-5.9.1.9070"; autoStart = true; - ports = [ - # Open ports if you don't use Traefik - # "${toString port}:${toString port}/tcp" + ports = lib.mkIf cfg.radarr.exposePorts [ + "${toString port}:${toString port}/tcp" ]; extraOptions = [ "--network=${networkName}" @@ -342,9 +379,8 @@ in { hostname = "sonarr"; image = "ghcr.io/hotio/sonarr:release-4.0.9.2244"; autoStart = true; - ports = [ - # Open ports if you don't use Traefik - # "${toString port}:${toString port}/tcp" + ports = lib.mkIf cfg.sonarr.exposePorts [ + "${toString port}:${toString port}/tcp" ]; extraOptions = [ "--network=${networkName}" From 07a97f360c3177bf518d900bada88fbd1d7f2060 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Thu, 11 Sep 2025 11:27:37 +0200 Subject: [PATCH 17/44] chore(arr): Update images --- modules/apps/arr/default.nix | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/apps/arr/default.nix b/modules/apps/arr/default.nix index 5cff39c..3b05429 100644 --- a/modules/apps/arr/default.nix +++ b/modules/apps/arr/default.nix @@ -243,7 +243,7 @@ in { 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 = lib.mkIf cfg.bazarr.exposePorts [ "${toString port}:${toString port}/tcp" @@ -279,7 +279,7 @@ in { 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 = lib.mkIf cfg.prowlarr.exposePorts [ "${toString port}:${toString port}/tcp" @@ -310,7 +310,7 @@ in { 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 = lib.mkIf cfg.qbittorrent.exposePorts [ "${toString port}:${toString port}/tcp" @@ -343,7 +343,7 @@ in { 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:release-5.28.0.10205"; autoStart = true; ports = lib.mkIf cfg.radarr.exposePorts [ "${toString port}:${toString port}/tcp" @@ -377,7 +377,7 @@ in { 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 = lib.mkIf cfg.sonarr.exposePorts [ "${toString port}:${toString port}/tcp" From cfee4fd83541457971bbbf93b38c001f725bab01 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Thu, 11 Sep 2025 12:09:26 +0200 Subject: [PATCH 18/44] fix(calibre): Specify proxy network --- hosts/Production/default.nix | 5 ++++- modules/apps/calibre/default.nix | 5 +++++ modules/apps/default.nix | 1 + 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/hosts/Production/default.nix b/hosts/Production/default.nix index cd929ff..9bb565d 100644 --- a/hosts/Production/default.nix +++ b/hosts/Production/default.nix @@ -3,7 +3,10 @@ { config = { homelab = { - apps.changedetection.enable = true; + apps = { + calibre.enable = true; + traefik.enable = true; + }; virtualisation.guest.enable = true; }; diff --git a/modules/apps/calibre/default.nix b/modules/apps/calibre/default.nix index aa00c89..e18ecb9 100644 --- a/modules/apps/calibre/default.nix +++ b/modules/apps/calibre/default.nix @@ -11,6 +11,7 @@ let 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)"; @@ -102,6 +103,7 @@ in { ]; extraOptions = [ "--network=${networkName}" + "--network=${proxyNet}" # syscalls are unkown to Docker #"--security-opt" "seccomp=unconfined" @@ -122,6 +124,7 @@ in { ]; 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; }; @@ -156,6 +159,7 @@ in { ]; extraOptions = [ "--network=${networkName}" + "--network=${proxyNet}" ]; environment = { inherit PUID PGID; @@ -175,6 +179,7 @@ in { ]; 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/default.nix b/modules/apps/default.nix index 81c6a06..7c8b8f8 100644 --- a/modules/apps/default.nix +++ b/modules/apps/default.nix @@ -10,6 +10,7 @@ ./plex ./speedtest ./technitium-dns + ./traefik ./vaultwarden ]; } From 59f721f4d7d3d50fbfe68312ce19b820658930ad Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Thu, 11 Sep 2025 12:18:10 +0200 Subject: [PATCH 19/44] chore(calibre): Update images --- modules/apps/calibre/default.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/apps/calibre/default.nix b/modules/apps/calibre/default.nix index e18ecb9..bddf5c8 100644 --- a/modules/apps/calibre/default.nix +++ b/modules/apps/calibre/default.nix @@ -93,7 +93,7 @@ in { innerPort = 8080; in { hostname = "calibre"; - image = "lscr.io/linuxserver/calibre:8.5.0"; + image = "lscr.io/linuxserver/calibre:v8.10.0-ls354"; autoStart = true; ports = [ # Open ports if you don't use Traefik @@ -151,7 +151,7 @@ in { innerPort = 8083; in { hostname = "calibre-web"; - image = "lscr.io/linuxserver/calibre-web:0.6.24"; + image = "lscr.io/linuxserver/calibre-web:0.6.25-ls346"; autoStart = true; ports = [ # Open ports if you don't use Traefik From bdb4ad8160557fa71321f30c995495216fc17ce0 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Wed, 1 Oct 2025 16:33:55 +0200 Subject: [PATCH 20/44] Updates --- modules/apps/arr/default.nix | 2 +- modules/apps/jellyfin/default.nix | 24 +++++++++--------------- modules/services/actions/default.nix | 2 +- 3 files changed, 11 insertions(+), 17 deletions(-) diff --git a/modules/apps/arr/default.nix b/modules/apps/arr/default.nix index 3b05429..e2c0df5 100644 --- a/modules/apps/arr/default.nix +++ b/modules/apps/arr/default.nix @@ -343,7 +343,7 @@ in { port = 7878; in lib.mkIf cfg.radarr.enable { hostname = "radarr"; - image = "ghcr.io/hotio/radarr:release-5.28.0.10205"; + image = "ghcr.io/hotio/radarr:testing-5.28.0.10205"; autoStart = true; ports = lib.mkIf cfg.radarr.exposePorts [ "${toString port}:${toString port}/tcp" diff --git a/modules/apps/jellyfin/default.nix b/modules/apps/jellyfin/default.nix index 5b4081a..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" ]; @@ -144,7 +138,7 @@ in { feishinPort = "9180"; in { hostname = "feishin"; - image = "ghcr.io/jeffvli/feishin:0.7.1"; + image = "ghcr.io/jeffvli/feishin:0.19.0"; autoStart = true; ports = [ "${feishinPort}:9180/tcp" # Web player (HTTP) 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 { ]; }; }; - }; } + From d2a0da648c5c1e7f4ae1f2be02431bf4116a5bb2 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Wed, 1 Oct 2025 16:36:14 +0200 Subject: [PATCH 21/44] feat(solidtime): Add module --- modules/apps/solidtime/default.nix | 238 +++++++++++++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100644 modules/apps/solidtime/default.nix diff --git a/modules/apps/solidtime/default.nix b/modules/apps/solidtime/default.nix new file mode 100644 index 0000000..9174df4 --- /dev/null +++ b/modules/apps/solidtime/default.nix @@ -0,0 +1,238 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.homelab.apps.solidtime; + + networkName = "solidtime"; + internalNetworkName = "solidtime-internal"; + + version = "0.8.0"; + + port = 8000; + user = "1000:1000"; + + # dbExternalPort = ...; + dbInternalPort = 5432; + + gotenbergPort = 3000; + + inherit (config.virtualisation.oci-containers) containers; + + volumes = [ + "solidtime-storage:/var/www/html/storage" + "solidtime-logs:/var/www/html/storage/logs" + "solidtime-app:/var/www/html/storage/app" + ]; + + # laravel.env + laravelEnv = { + APP_NAME = "Solidtime"; + VITE_APP_NAME = laravelEnv.APP_NAME; + APP_ENV = "production"; + APP_DEBUG = "false"; + APP_URL = "http://localhost:${toString port}"; + APP_FORCE_HTTPS = "false"; + APP_ENABLE_REGISTRATION = "false"; + TRUSTED_PROXIES = "0.0.0.0/0,2000:0:0:0:0:0:0:0/3"; + + # Logging + LOG_CHANNEL = "stderr_daily"; + LOG_LEVEL = "debug"; + + # Database + DB_CONNECTION = "pgsql"; + DB_HOST = containers.solidtimeDb.hostname; + DB_PORT = toString dbInternalPort; + DB_SSL_MODE = "require"; + DB_DATABASE = "solidtime"; + DB_USERNAME = "solidtime"; + DB_PASSWORD = "ChangeMe"; + + # Mail + #MAIL_MAILER = "smtp"; + #MAIL_HOST = "smtp.gmail.com"; + #MAIL_PORT = "465"; + #MAIL_ENCRYPTION = "tls"; + #MAIL_FROM_ADDRESS = "no-reply@time.depeuter.dev"; + MAIL_FROM_NAME = laravelEnv.APP_NAME; + #MAIL_USERNAME = "kmtl.hugo@gmail.com"; + #MAIL_PASSWORD = "fhfxoequhhqidrhd"; + + # Queue + QUEUE_CONNECTION = "database"; + + # File storage + FILESYSTEM_DISK = "local"; + PUBLIC_FILESYSTEM_DISK = "public"; + + # Services + GOTENBERG_URL = "http://${containers.solidtimeGotenberg.hostname}:${toString gotenbergPort}"; + }; + +in { + options.homelab.apps.solidtime.enable = lib.mkEnableOption "Solidtime time tracker using Docker"; + + config = lib.mkIf cfg.enable { + homelab.virtualisation.containers.enable = true; + + # Make sure the Docker network exists. + systemd.services = { + "docker-${networkName}-create-network" = { + description = "Create Docker network for ${networkName}"; + requiredBy = [ + "${containers.solidtime.serviceName}.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 + ''; + }; + "docker-${internalNetworkName}-create-network" = { + description = "Create Docker network for ${internalNetworkName}"; + requiredBy = [ + "${containers.solidtime.serviceName}.service" + "${containers.solidtimeScheduler.serviceName}.service" + "${containers.solidtimeQueue.serviceName}.service" + "${containers.solidtimeDb.serviceName}.service" + "${containers.solidtimeGotenberg.serviceName}.service" + ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = '' + if ! ${pkgs.docker}/bin/docker network ls | grep -q ${internalNetworkName}; then + ${pkgs.docker}/bin/docker network create ${internalNetworkName} + fi + ''; + }; + }; + + virtualisation.oci-containers.containers = { + solidtime = { + hostname = "solidtime"; + image = "solidtime/solidtime:${version}"; + autoStart = true; + inherit user; + ports = [ + # Open ports if you don't use Traefik + "${toString port}:8000" + ]; + extraOptions = [ + "--network=${networkName}" + "--network=${internalNetworkName}" + + # Healthecks + # test: [ "CMD-SHELL", "curl --fail http://localhost:8000/health-check/up || exit 1" ] + ''--health-cmd=curl --fail http://localhost:8000/health-check/up || exit 1'' + ]; + inherit volumes; + dependsOn = [ + "solidtimeDb" + ]; + environmentFiles = [ + "/home/admin/.solidtime.env" + ]; + environment = laravelEnv // { + CONTAINER_MODE = "http"; + }; + labels = { + "traefik.enable" = "true"; + "traefik.http.routers.solidtime.rule" = "Host(`time.${config.networking.hostName}.depeuter.dev`)"; + "traefik.http.services.solidtime.loadbalancer.server.port" = toString port; + }; + }; + solidtimeScheduler = { + hostname = "scheduler"; + image = "solidtime/solidtime:${version}"; + inherit user; + autoStart = true; + extraOptions = [ + "--network=${internalNetworkName}" + + # Healthchecks + # test: [ "CMD-SHELL", "supervisorctl status scheduler:scheduler_00" ] + ''--health-cmd="supervisorctl status scheduler:scheduler_00"'' + ]; + inherit volumes; + dependsOn = [ + "solidtimeDb" + ]; + environmentFiles = [ + "/home/admin/.solidtime.env" + ]; + environment = laravelEnv // { + CONTAINER_MODE = "scheduler"; + }; + }; + solidtimeQueue = { + hostname = "queue"; + image = "solidtime/solidtime:${version}"; + inherit user; + autoStart = true; + extraOptions = [ + "--network=${internalNetworkName}" + + # Healthchecks + # test: [ "CMD-SHELL", "supervisorctl status worker:worker_00" ] + ''--health-cmd="supervisorctl status worker:worker_00"'' + ]; + inherit volumes; + dependsOn = [ + "solidtimeDb" + ]; + environmentFiles = [ + "/home/admin/.solidtime.env" + ]; + environment = laravelEnv // { + CONTAINER_MODE = "worker"; + WORKER_COMMAND = "php /var/www/html/artisan queue:work"; + }; + }; + solidtimeDb = { + hostname = "database"; + image = "postgres:15"; + autoStart = true; + ports = [ + # "${toString dbExternalPort}:${toString dbInternalPort}" + ]; + extraOptions = [ + "--network=${internalNetworkName}" + + # Healthchecks + # test: - CMD - pg_isready - '-q' - '-d' - '${DB_DATABASE}' - '-U' - '${DB_USERNAME}' retries: 3 timeout: 5s + ''--health-cmd="pg_isready -q -d ${laravelEnv.DB_DATABASE} -U ${laravelEnv.DB_USERNAME}"'' + "--health-retries=3" + "--health-timeout=5s" + ]; + volumes = [ + "solidtime-db:/var/lib/postgresql/data" + ]; + environment = { + PGPASSWORD = laravelEnv.DB_PASSWORD; + POSTGRES_DB = laravelEnv.DB_DATABASE; + POSTGRES_USER = laravelEnv.DB_USERNAME; + POSTGRES_PASSWORD = laravelEnv.DB_PASSWORD; + }; + }; + solidtimeGotenberg = { + hostname = "gotenberg"; + image = "gotenberg/gotenberg:8"; + autoStart = true; + extraOptions = [ + "--network=${internalNetworkName}" + + # Healthchecks + # test: [ "CMD", "curl", "--silent", "--fail", "http://localhost:3000/health" ] + ''--health-cmd="curl --silent --fail http://localhost:${toString gotenbergPort}/health"'' + ]; + }; + }; + }; +} + From f1ba0a98e846ff62d03c2f2310e67704dae14800 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Wed, 1 Oct 2025 16:33:55 +0200 Subject: [PATCH 22/44] Updates --- flake.lock | 12 ++--- flake.nix | 8 ++- hosts/Ingress/default.nix | 34 ++++++++++-- hosts/Niko/default.nix | 77 +++------------------------- hosts/ProductionGPU/default.nix | 8 +-- hosts/Testing/default.nix | 5 +- modules/apps/arr/default.nix | 2 +- modules/apps/freshrss/default.nix | 27 ++++++++-- modules/apps/gitea/default.nix | 4 +- modules/apps/jellyfin/default.nix | 24 ++++----- modules/default.nix | 1 + modules/services/actions/default.nix | 2 +- 12 files changed, 95 insertions(+), 109 deletions(-) diff --git a/flake.lock b/flake.lock index e3284fd..78e4127 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1735291276, - "narHash": "sha256-NYVcA06+blsLG6wpAbSPTCyLvxD/92Hy4vlY9WxFI1M=", + "lastModified": 1756787288, + "narHash": "sha256-rw/PHa1cqiePdBxhF66V7R+WAP8WekQ0mCDG4CFqT8Y=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "634fd46801442d760e09493a794c4f15db2d0cbb", + "rev": "d0fc30899600b9b3466ddb260fd83deb486c32f1", "type": "github" }, "original": { @@ -62,11 +62,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 44d63c2..7701f3a 100644 --- a/flake.nix +++ b/flake.nix @@ -32,18 +32,24 @@ }; 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/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/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 2da6563..cc353f6 100644 --- a/hosts/Testing/default.nix +++ b/hosts/Testing/default.nix @@ -3,7 +3,10 @@ { config = { homelab = { - apps.freshrss.enable = true; + apps = { + freshrss.enable = true; + traefik.enable = true; + }; virtualisation.guest.enable = true; }; diff --git a/modules/apps/arr/default.nix b/modules/apps/arr/default.nix index 3b05429..e2c0df5 100644 --- a/modules/apps/arr/default.nix +++ b/modules/apps/arr/default.nix @@ -343,7 +343,7 @@ in { port = 7878; in lib.mkIf cfg.radarr.enable { hostname = "radarr"; - image = "ghcr.io/hotio/radarr:release-5.28.0.10205"; + image = "ghcr.io/hotio/radarr:testing-5.28.0.10205"; autoStart = true; ports = lib.mkIf cfg.radarr.exposePorts [ "${toString port}:${toString port}/tcp" diff --git a/modules/apps/freshrss/default.nix b/modules/apps/freshrss/default.nix index f2ea7ba..4f4456f 100644 --- a/modules/apps/freshrss/default.nix +++ b/modules/apps/freshrss/default.nix @@ -27,7 +27,7 @@ in { "rw" "auto" "nfsvers=4.2" - "sync" "hard" "timeo=600" + "async" "soft" "timeo=600" "retrans=2" "_netdev" "nosuid" @@ -53,24 +53,41 @@ in { virtualisation.oci-containers.containers.freshrss = { hostname = "freshrss"; - image = "freshrss/freshrss:1.24.0"; + image = "freshrss/freshrss:1.25.0"; autoStart = true; user = "0:33"; ports = [ - "${toString port}:${toString port}/tcp" + "${toString port}:80/tcp" ]; extraOptions = [ "--network=${networkName}" ]; environment = { TZ = config.time.timeZone; - CRON_TIME = "3,18,33,48"; # Alternatively, configure cron inside container. - LISTEN = "0.0.0.0:${toString port}"; + 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 02f60cd..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 = [ @@ -576,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 5b4081a..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" ]; @@ -144,7 +138,7 @@ in { feishinPort = "9180"; in { hostname = "feishin"; - image = "ghcr.io/jeffvli/feishin:0.7.1"; + image = "ghcr.io/jeffvli/feishin:0.19.0"; autoStart = true; ports = [ "${feishinPort}:9180/tcp" # Web player (HTTP) 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/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 { ]; }; }; - }; } + From 12015f8589f83882fca484c70814925c02f888b3 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Sat, 4 Oct 2025 19:59:03 +0200 Subject: [PATCH 23/44] feat(sops): Setup sops --- .sops.yaml | 8 ++++++++ flake.nix | 10 ++++++++-- secrets/secrets.yaml | 19 +++++++++++++++++++ users/admin/default.nix | 13 ++++++++++--- 4 files changed, 45 insertions(+), 5 deletions(-) create mode 100644 .sops.yaml create mode 100644 secrets/secrets.yaml diff --git a/.sops.yaml b/.sops.yaml new file mode 100644 index 0000000..8d0d445 --- /dev/null +++ b/.sops.yaml @@ -0,0 +1,8 @@ +keys: + - &tdpeuter_Tibo-NixTop age1qzutny0mqpcccqw6myyfntu6wcskruu9ghzvt6r4te7afkqwnguq05ex37 + +creation_rules: + - path_regex: secrets/[^/]+\.(yaml|json|env|ini)$ + key_groups: + - age: + - *tdpeuter_Tibo-NixTop diff --git a/flake.nix b/flake.nix index 7701f3a..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,6 +32,8 @@ modules = [ ./modules ./users + + sops-nix.nixosModules.sops ]; }; 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..bc3ccc7 100644 --- a/users/admin/default.nix +++ b/users/admin/default.nix @@ -2,6 +2,8 @@ let cfg = config.homelab.users.admin; + + owner = config.users.users.admin.name; in { options.homelab.users.admin.enable = lib.mkEnableOption "user System Administrator"; @@ -10,6 +12,12 @@ in { config.users.users.admin.name ]; + sops.secrets."users/admin/authorized_keys" = { + format = "yaml"; + sopsFile = ../../secrets/secrets.yaml; + inherit owner; + }; + users.users.admin = { description = "System Administrator"; isNormalUser = true; @@ -17,9 +25,8 @@ in { config.users.groups.wheel.name # Enable 'sudo' for the user. ]; initialPassword = "ChangeMe"; - openssh.authorizedKeys.keys = [ - # TODO ChangeMe - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPrG+ldRBdCeHEXrsy/qHXIJYg8xQXVuiUR0DxhFjYNg" + openssh.authorizedKeys.keyFiles = [ + /run/secrets/users/admin/authorized_keys ]; packages = with pkgs; [ curl From a37c5ae83a4beb84e46d04bac7bb0ba9bde9a12d Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Sat, 4 Oct 2025 20:21:59 +0200 Subject: [PATCH 24/44] fix(sops): Add Tibo-NixFat --- .sops.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.sops.yaml b/.sops.yaml index 8d0d445..02cc451 100644 --- a/.sops.yaml +++ b/.sops.yaml @@ -1,8 +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 From 4ab3848c830a2a1254e78189909cd316011f5817 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Sat, 4 Oct 2025 20:25:09 +0200 Subject: [PATCH 25/44] fix(Development): Replace authorized_keys --- users/admin/default.nix | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/users/admin/default.nix b/users/admin/default.nix index bc3ccc7..4038266 100644 --- a/users/admin/default.nix +++ b/users/admin/default.nix @@ -2,8 +2,6 @@ let cfg = config.homelab.users.admin; - - owner = config.users.users.admin.name; in { options.homelab.users.admin.enable = lib.mkEnableOption "user System Administrator"; @@ -12,12 +10,6 @@ in { config.users.users.admin.name ]; - sops.secrets."users/admin/authorized_keys" = { - format = "yaml"; - sopsFile = ../../secrets/secrets.yaml; - inherit owner; - }; - users.users.admin = { description = "System Administrator"; isNormalUser = true; @@ -25,8 +17,9 @@ in { config.users.groups.wheel.name # Enable 'sudo' for the user. ]; initialPassword = "ChangeMe"; - openssh.authorizedKeys.keyFiles = [ - /run/secrets/users/admin/authorized_keys + openssh.authorizedKeys.keys = [ + # HomeLab > NixOS > admin > ssh + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGWIOOEqTy8cWKpENVbzD4p7bsQgQb/Dgpzk8i0dZ00T" ]; packages = with pkgs; [ curl From 81a07af1521169a7ffac5e0850a0e236d9580248 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Sat, 4 Oct 2025 20:26:01 +0200 Subject: [PATCH 26/44] nix flake update --- flake.lock | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 78e4127..ca6e418 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1756787288, - "narHash": "sha256-rw/PHa1cqiePdBxhF66V7R+WAP8WekQ0mCDG4CFqT8Y=", + "lastModified": 1759381078, + "narHash": "sha256-gTrEEp5gEspIcCOx9PD8kMaF1iEmfBcTbO0Jag2QhQs=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "d0fc30899600b9b3466ddb260fd83deb486c32f1", + "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, From 297a6df29e56fd72e4888fad295a4683659c9e6f Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Sat, 11 Oct 2025 15:40:43 +0200 Subject: [PATCH 27/44] feat: Add gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..485dee6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea From b2e904306b299e96fdd316857ead0ac351c5a4c1 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Sat, 11 Oct 2025 15:41:13 +0200 Subject: [PATCH 28/44] feat(traefik): Add external services --- hosts/Binnenpost/default.nix | 9 +++++++++ 1 file changed, 9 insertions(+) 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"; }; } From 2c195bf8077c90b2330ba6d43e3de1b5048bfa92 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Mon, 13 Oct 2025 20:35:46 +0200 Subject: [PATCH 29/44] chore(arr): Use functions --- modules/apps/arr/default.nix | 178 +++++++---------------------------- 1 file changed, 35 insertions(+), 143 deletions(-) diff --git a/modules/apps/arr/default.nix b/modules/apps/arr/default.nix index e2c0df5..7b530c3 100644 --- a/modules/apps/arr/default.nix +++ b/modules/apps/arr/default.nix @@ -12,7 +12,16 @@ let 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; @@ -21,46 +30,11 @@ in { default = ! config.homelab.apps.traefik.enable; }; - bazarr = { - enable = lib.mkEnableOption "Bazarr using Docker"; - exposePorts = lib.mkOption { - type = lib.types.bool; - description = "Expose Bazarr port"; - default = cfg.exposePorts; - }; - }; - prowlarr = { - enable = lib.mkEnableOption "Prowlarr using Docker"; - exposePorts = lib.mkOption { - type = lib.types.bool; - description = "Expose Prowlarr port"; - default = cfg.exposePorts; - }; - }; - qbittorrent = { - enable = lib.mkEnableOption "qBittorrent using Docker"; - exposePorts = lib.mkOption { - type = lib.types.bool; - description = "Expose qBittorrent port"; - default = cfg.exposePorts; - }; - }; - radarr = { - enable = lib.mkEnableOption "Radarr using Docker"; - exposePorts = lib.mkOption { - type = lib.types.bool; - description = "Expose Radarr port"; - default = cfg.exposePorts; - }; - }; - sonarr = { - enable = lib.mkEnableOption "Sonarr using Docker"; - exposePorts = lib.mkOption { - type = lib.types.bool; - description = "Expose Sonarr port"; - default = cfg.exposePorts; - }; - }; + bazarr = mkAppOption "Bazarr"; + prowlarr = mkAppOption "Prowlarr"; + qbittorrent = mkAppOption "qBittorrent"; + radarr = mkAppOption "Radarr"; + sonarr = mkAppOption "Sonarr"; }; config = { @@ -87,9 +61,9 @@ in { virtualisation.containers.enable = lib.mkIf inUse true; }; - fileSystems = lib.mkIf inUse { - "/srv/bazarr-backup" = lib.mkIf cfg.bazarr.enable { - device = "192.168.0.11:/mnt/BIG/BACKUP/BAZARR"; + fileSystems = let + mkFileSystem = device: { + inherit device; fsType = "nfs"; options = [ "rw" @@ -102,75 +76,14 @@ in { ]; }; - "/srv/prowlarr-backup" = lib.mkIf cfg.prowlarr.enable { - device = "192.168.0.11:/mnt/BIG/BACKUP/PROWLARR"; - fsType = "nfs"; - options = [ - "rw" - "auto" - "nfsvers=4.2" - "rsize=1048576" "wsize=1048576" - "hard" - "timeo=600" "retrans=2" - "_netdev" "nosuid" "tcp" - ]; - }; - - "/srv/qbittorrent" = lib.mkIf cfg.qbittorrent.enable { - device = "192.168.0.11:/mnt/SMALL/CONFIG/QBITTORRENT"; - fsType = "nfs"; - options = [ - "rw" - "auto" - "nfsvers=4.2" - "rsize=1048576" "wsize=1048576" - "hard" - "timeo=600" "retrans=2" - "_netdev" "nosuid" "tcp" - ]; - }; - - "/srv/radarr-backup" = lib.mkIf cfg.radarr.enable { - device = "192.168.0.11:/mnt/BIG/BACKUP/RADARR"; - fsType = "nfs"; - options = [ - "rw" - "auto" - "nfsvers=4.2" - "rsize=1048576" "wsize=1048576" - "hard" - "timeo=600" "retrans=2" - "_netdev" "nosuid" "tcp" - ]; - }; - - "/srv/sonarr-backup" = lib.mkIf cfg.sonarr.enable { - device = "192.168.0.11:/mnt/BIG/BACKUP/SONARR"; - fsType = "nfs"; - options = [ - "rw" - "auto" - "nfsvers=4.2" - "rsize=1048576" "wsize=1048576" - "hard" - "timeo=600" "retrans=2" - "_netdev" "nosuid" "tcp" - ]; - }; - - "/srv/torrent" = { - device = "192.168.0.11:/mnt/SMALL/MEDIA/TORRENT"; - fsType = "nfs"; - options = [ - "rw" - "auto" - "nfsvers=4.2" - "rsize=1048576" "wsize=1048576" - "hard" - "timeo=600" "retrans=2" - "_netdev" "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. @@ -195,45 +108,24 @@ 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; }; - 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; + 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 ]; - 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 = let From 652a9da0778b163f401ecc501087d6da45f1ad39 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Sat, 18 Oct 2025 17:09:08 +0200 Subject: [PATCH 30/44] flake.lock: Update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'nixpkgs': 'github:NixOS/nixpkgs/7df7ff7d8e00218376575f0acdcc5d66741351ee?narHash=sha256-gTrEEp5gEspIcCOx9PD8kMaF1iEmfBcTbO0Jag2QhQs%3D' (2025-10-02) → 'github:NixOS/nixpkgs/544961dfcce86422ba200ed9a0b00dd4b1486ec5?narHash=sha256-EVAqOteLBFmd7pKkb0%2BFIUyzTF61VKi7YmvP1tw4nEw%3D' (2025-10-15) • Updated input 'sops-nix': 'github:Mic92/sops-nix/9fcfabe085281dd793589bdc770a2e577a3caa5d?narHash=sha256-f9QC2KKiNReZDG2yyKAtDZh0rSK2Xp1wkPzKbHeQVRU%3D' (2025-09-29) → 'github:Mic92/sops-nix/ab8d56e85b8be14cff9d93735951e30c3e86a437?narHash=sha256-8mN3kqyqa2PKY0wwZ2UmMEYMcxvNTwLaOrrDsw6Qi4E%3D' (2025-10-13) --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index ca6e418..67df8c4 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1759381078, - "narHash": "sha256-gTrEEp5gEspIcCOx9PD8kMaF1iEmfBcTbO0Jag2QhQs=", + "lastModified": 1760524057, + "narHash": "sha256-EVAqOteLBFmd7pKkb0+FIUyzTF61VKi7YmvP1tw4nEw=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "7df7ff7d8e00218376575f0acdcc5d66741351ee", + "rev": "544961dfcce86422ba200ed9a0b00dd4b1486ec5", "type": "github" }, "original": { @@ -48,11 +48,11 @@ ] }, "locked": { - "lastModified": 1759188042, - "narHash": "sha256-f9QC2KKiNReZDG2yyKAtDZh0rSK2Xp1wkPzKbHeQVRU=", + "lastModified": 1760393368, + "narHash": "sha256-8mN3kqyqa2PKY0wwZ2UmMEYMcxvNTwLaOrrDsw6Qi4E=", "owner": "Mic92", "repo": "sops-nix", - "rev": "9fcfabe085281dd793589bdc770a2e577a3caa5d", + "rev": "ab8d56e85b8be14cff9d93735951e30c3e86a437", "type": "github" }, "original": { From 6deb36d92061cbb5c359a9c2c6c84a07f69882d3 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Sat, 18 Oct 2025 18:59:17 +0200 Subject: [PATCH 31/44] chore(ssh): Update keys --- hosts/Gitea/default.nix | 7 +++++++ hosts/Vaultwarden/default.nix | 7 +++++++ users/admin/default.nix | 20 +++++++++++++------- users/backup/default.nix | 7 +------ 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/hosts/Gitea/default.nix b/hosts/Gitea/default.nix index 5b2492f..c6c9b43 100644 --- a/hosts/Gitea/default.nix +++ b/hosts/Gitea/default.nix @@ -5,6 +5,13 @@ homelab = { apps.gitea.enable = true; virtualisation.guest.enable = true; + + users.admin = { + enable = true; + authorizedKeys = [ + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFrp6aM62Bf7bj1YM5AlAWuNrANU3N5e8+LtbbpmZPKS" + ]; + }; }; networking = { diff --git a/hosts/Vaultwarden/default.nix b/hosts/Vaultwarden/default.nix index d8115bc..5ded575 100644 --- a/hosts/Vaultwarden/default.nix +++ b/hosts/Vaultwarden/default.nix @@ -9,6 +9,13 @@ name = "Hugo's Vault"; }; virtualisation.guest.enable = true; + + users.admin = { + enable = true; + authorizedKeys = [ + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJnihoyozOCnm6T9OzL2xoMeMZckBYR2w43us68ABA93" + ]; + }; }; networking = { diff --git a/users/admin/default.nix b/users/admin/default.nix index 4038266..dc01c81 100644 --- a/users/admin/default.nix +++ b/users/admin/default.nix @@ -3,24 +3,30 @@ let cfg = config.homelab.users.admin; in { - options.homelab.users.admin.enable = lib.mkEnableOption "user System Administrator"; + options.homelab.users.admin = { + enable = lib.mkEnableOption "user System Administrator"; + authorizedKeys = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ + # HomeLab > NixOS > admin > ssh + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGWIOOEqTy8cWKpENVbzD4p7bsQgQb/Dgpzk8i0dZ00T" + ]; + }; + }; config = lib.mkIf cfg.enable { nix.settings.trusted-users = [ - config.users.users.admin.name + config.users.users.gh0st.name ]; - users.users.admin = { + users.users.gh0st = { description = "System Administrator"; isNormalUser = true; extraGroups = [ config.users.groups.wheel.name # Enable 'sudo' for the user. ]; initialPassword = "ChangeMe"; - openssh.authorizedKeys.keys = [ - # HomeLab > NixOS > admin > ssh - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGWIOOEqTy8cWKpENVbzD4p7bsQgQb/Dgpzk8i0dZ00T" - ]; + openssh.authorizedKeys.keys = cfg.authorizedKeys; packages = with pkgs; [ curl git diff --git a/users/backup/default.nix b/users/backup/default.nix index 8181d02..acae033 100644 --- a/users/backup/default.nix +++ b/users/backup/default.nix @@ -13,13 +13,8 @@ in { "docker" # Allow access to the docker socket. ]; openssh.authorizedKeys.keys = [ - # TODO ChangeMe - - # Tibo-NixFat - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPrG+ldRBdCeHEXrsy/qHXIJYg8xQXVuiUR0DxhFjYNg" - # Hugo - "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDAxR813vqq5zbu1NHrIybu5Imlu3k0rDCGxHiuGEhPoVV9c5FpnKNGLCi3ctm15ZcVBX4HcponYsKRBsCzM2pI4uXjxhHkLzbss5LttFuSzv5v/QHfLW1bvyJEMBEPxguGqAydAeWrBFdI9uHBEXeb325uKxMKBZHYvvpyAQ115c1wKy1bL8BfR0LTkhsFqexRvI86q59AVrAU/KFf6RXO0T9QA6H/vyWLlIPc7Ta+tSWwQ68bMmS5Pwn8q58tOAOAd6Lpt4TqUDJSppPjLEPKyKC6ShwMdEjwmwpEG0hxfsvaU8XERyQbSbEE9sLHRA2LoEdtMx3J8nzX3AwYUNspsqIv6NQZksnVqJ8OfL45ngUFcSJ6kBsUvCZfzEUGUTJ6Js0v84NOIXxNG/ZfPsk6ArXm3dvj2TYeK8llO6wpJnMMyztmmiODWoj9tepZSij44IgVM5wdWYIK/RZoYTsCQbmvJFfB8jhyJnf/7F19Vo5+LwhmCOsQh/KEK0F1DVc= admin@Hugo" + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICms6vjhE9kOlqV5GBPGInwUHAfCSVHLI2Gtzee0VXPh" ]; }; }; From ca7875dee906334815393f10920a3d7ab8780108 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Wed, 4 Feb 2026 09:25:45 +0100 Subject: [PATCH 32/44] feat: Move URLs to vars --- hosts/Ingress/default.nix | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/hosts/Ingress/default.nix b/hosts/Ingress/default.nix index 68cdcfe..9268714 100644 --- a/hosts/Ingress/default.nix +++ b/hosts/Ingress/default.nix @@ -80,7 +80,7 @@ prefixLength = 24; # Only allow PFS-enabled ciphers with AES256 sslCiphers = "AES256+EECDH:AES256+EDH:!aNULL"; - upstreams.docservice.servers."192.168.0.14:8080" = {}; + upstreams.docservice.servers."${nextcloud.host}:${toString nextcloud.officePort}" = {}; appendHttpConfig = '' map $http_x_forwarded_proto $the_scheme { @@ -107,19 +107,24 @@ prefixLength = 24; default = true; }; - "cloud.depeuter.dev" = { + "cloud.depeuter.dev" = let + nextcloud = { + host = "192.168.0.14"; + officePort = 8080; + }; + in { enableACME = true; forceSSL = true; locations = { "/" = { - proxyPass = "http://192.168.0.14"; + proxyPass = "http://${nextcloud.host}"; extraConfig = '' add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always; fastcgi_request_buffering off; ''; }; "/office/" = { - proxyPass = "http://192.168.0.14:8080/"; + proxyPass = "http://${nextcloud.host}:${toString nextcloud.officePort}/"; priority = 500; recommendedProxySettings = false; extraConfig = '' @@ -137,12 +142,6 @@ prefixLength = 24; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; ''; }; - "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; @@ -158,12 +157,17 @@ prefixLength = 24; }; }; - "jelly.depeuter.dev" = { + "jelly.depeuter.dev" = let + jellyfin = { + host = "192.168.0.94"; + port = 8096; + }; + in { enableACME = true; forceSSL = true; locations = { "/" = { - proxyPass = "http://192.168.0.94:8096"; + proxyPass = "http://${jellyfin.host}:${jellyfin.port}"; extraConfig = '' # Proxy main Jellyfin traffic proxy_set_header Host $host; @@ -178,7 +182,7 @@ prefixLength = 24; ''; }; "/socket" = { - proxyPass = "http://192.168.0.91:8096"; + proxyPass = "http://${jellyfin.host}:${jellyfin.port}"; extraConfig = '' # Proxy Jellyfin Websockets traffic proxy_http_version 1.1; From d38c53762a90e1e7993586661c2a338e0b8ca1e6 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Wed, 4 Feb 2026 20:23:56 +0100 Subject: [PATCH 33/44] chore: Change Nextcloud IP --- hosts/Ingress/default.nix | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/hosts/Ingress/default.nix b/hosts/Ingress/default.nix index 9268714..c0a3ac9 100644 --- a/hosts/Ingress/default.nix +++ b/hosts/Ingress/default.nix @@ -68,7 +68,12 @@ prefixLength = 24; # List services that you want to enable. services = { # Enable Nginx as a reverse proxy - nginx = { + nginx = let + nextcloud = { + host = "192.168.0.23"; + officePort = 8080; + }; + in { enable = true; # Use recommended settings @@ -107,12 +112,7 @@ prefixLength = 24; default = true; }; - "cloud.depeuter.dev" = let - nextcloud = { - host = "192.168.0.14"; - officePort = 8080; - }; - in { + "cloud.depeuter.dev" = { enableACME = true; forceSSL = true; locations = { @@ -167,7 +167,7 @@ prefixLength = 24; forceSSL = true; locations = { "/" = { - proxyPass = "http://${jellyfin.host}:${jellyfin.port}"; + proxyPass = "http://${jellyfin.host}:${toString jellyfin.port}"; extraConfig = '' # Proxy main Jellyfin traffic proxy_set_header Host $host; @@ -182,7 +182,7 @@ prefixLength = 24; ''; }; "/socket" = { - proxyPass = "http://${jellyfin.host}:${jellyfin.port}"; + proxyPass = "http://${jellyfin.host}:${toString jellyfin.port}"; extraConfig = '' # Proxy Jellyfin Websockets traffic proxy_http_version 1.1; @@ -244,7 +244,7 @@ prefixLength = 24; locations = { "/" = { proxyPass = "http://192.168.0.22:10102"; - proxyWebSockets = true; + proxyWebsockets = true; }; "~ ^/admin".return = 403; }; From 5582384f01de990f4476d77c954787f04b0171c1 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Fri, 6 Feb 2026 09:20:43 +0100 Subject: [PATCH 34/44] feat: Add homepage module --- hosts/Development/default.nix | 4 ++ modules/apps/default.nix | 1 + modules/apps/homepage/default.nix | 79 +++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+) create mode 100644 modules/apps/homepage/default.nix diff --git a/hosts/Development/default.nix b/hosts/Development/default.nix index b2237b7..fda8e57 100644 --- a/hosts/Development/default.nix +++ b/hosts/Development/default.nix @@ -5,6 +5,10 @@ homelab = { apps = { bind9.enable = true; + homepage = { + enable = true; + exposePort = true; + }; traefik.enable = true; plex.enable = true; }; diff --git a/modules/apps/default.nix b/modules/apps/default.nix index 7c8b8f8..f62dca7 100644 --- a/modules/apps/default.nix +++ b/modules/apps/default.nix @@ -6,6 +6,7 @@ ./changedetection ./freshrss ./gitea + ./homepage ./jellyfin ./plex ./speedtest diff --git a/modules/apps/homepage/default.nix b/modules/apps/homepage/default.nix new file mode 100644 index 0000000..b34f32f --- /dev/null +++ b/modules/apps/homepage/default.nix @@ -0,0 +1,79 @@ +{ config, lib, ... }: + +let + cfg = config.homelab.apps.homepage; + + PUID = toString config.users.users.homepage.uid; + PGID = toString config.users.groups.apps.gid; + + homepage-config = "/srv/homepage-config"; + + proxyNet = config.homelab.apps.traefik.sharedNetworkName; +in { + options.homelab.apps.homepage = { + enable = lib.mkEnableOption "homepage"; + port = lib.mkOption { + type = lib.types.int; + default = 3000; + description = "homepage WebUI port"; + }; + exposePort = lib.mkEnableOption "expose homepage port"; + }; + + config = lib.mkIf cfg.enable { + homelab = { + users.apps.enable = true; + virtualisation.containers.enable = true; + }; + + users.users.homepage = { + uid = lib.mkForce 3018; + isSystemUser = true; + group = config.users.groups.apps.name; + home = "/var/empty"; + shell = null; + }; + + fileSystems."${homepage-config}" = { + device = "192.168.0.11:/mnt/SMALL/CONFIG/HOMEPAGE"; + fsType = "nfs"; + options = [ + "rw" + "auto" + "nfsvers=4.2" + "async" "soft" "timeo=100" "retry=50" "actimeo=1800" "lookupcache=all" + "nosuid" "tcp" + ]; + }; + + virtualisation.oci-containers.containers.homepage = let + host = "homepage.${config.networking.domain}"; + in { + hostname = "homepage"; + image = "ghcr.io/gethomepage/homepage:v1.10.1"; + autoStart = true; + user = "${toString PUID}:${toString PGID}"; + ports = lib.mkIf cfg.exposePort [ + "${toString cfg.port}:3000/tcp" + ]; + networks = [ + proxyNet + ]; + volumes = [ + "${homepage-config}:/app/config" + # "/var/run/docker.sock:/var/run/docker.sock:ro" # For docker integrations + ]; + labels = { + "traefik.enable" = "true"; + "traefik.docker.network" = proxyNet; + "traefik.http.routers.homepage.rule" = "Host(`${host}`)"; + "traefik.http.services.homepage.loadbalancer.server.port" = toString cfg.port; + }; + environment = { + inherit PUID PGID; + + HOMEPAGE_ALLOWED_HOSTS = "${host},192.168.0.91:3000"; + }; + }; + }; +} From 04b4254a72c7b104a28b23ae40bd107e2d158f97 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Tue, 10 Feb 2026 08:30:11 +0100 Subject: [PATCH 35/44] chore(soldtime): Cleanup options --- hosts/Development/default.nix | 1 + modules/apps/default.nix | 1 + modules/apps/solidtime/default.nix | 67 ++++++++++++++++++------------ 3 files changed, 42 insertions(+), 27 deletions(-) diff --git a/hosts/Development/default.nix b/hosts/Development/default.nix index fda8e57..77f6758 100644 --- a/hosts/Development/default.nix +++ b/hosts/Development/default.nix @@ -11,6 +11,7 @@ }; traefik.enable = true; plex.enable = true; + solidtime.enable = true; }; virtualisation.guest.enable = true; }; diff --git a/modules/apps/default.nix b/modules/apps/default.nix index f62dca7..385f915 100644 --- a/modules/apps/default.nix +++ b/modules/apps/default.nix @@ -9,6 +9,7 @@ ./homepage ./jellyfin ./plex + ./solidtime ./speedtest ./technitium-dns ./traefik diff --git a/modules/apps/solidtime/default.nix b/modules/apps/solidtime/default.nix index 9174df4..17193a3 100644 --- a/modules/apps/solidtime/default.nix +++ b/modules/apps/solidtime/default.nix @@ -5,10 +5,10 @@ let networkName = "solidtime"; internalNetworkName = "solidtime-internal"; + proxyNet = config.homelab.apps.traefiik.sharedNetworkName; version = "0.8.0"; - port = 8000; user = "1000:1000"; # dbExternalPort = ...; @@ -30,7 +30,7 @@ let VITE_APP_NAME = laravelEnv.APP_NAME; APP_ENV = "production"; APP_DEBUG = "false"; - APP_URL = "http://localhost:${toString port}"; + APP_URL = "http://localhost:${toString cfg.port}"; APP_FORCE_HTTPS = "false"; APP_ENABLE_REGISTRATION = "false"; TRUSTED_PROXIES = "0.0.0.0/0,2000:0:0:0:0:0:0:0/3"; @@ -70,7 +70,15 @@ let }; in { - options.homelab.apps.solidtime.enable = lib.mkEnableOption "Solidtime time tracker using Docker"; + options.homelab.apps.solidtime = { + enable = lib.mkEnableOption "Solidtime time tracker using Docker"; + port = lib.mkOption { + type = lib.types.int; + default = 8000; + description = "Solidtime WebUI port"; + }; + exposePort = lib.mkEnableOption "Expose Soldtime port"; + }; config = lib.mkIf cfg.enable { homelab.virtualisation.containers.enable = true; @@ -117,52 +125,54 @@ in { solidtime = { hostname = "solidtime"; image = "solidtime/solidtime:${version}"; - autoStart = true; inherit user; + autoStart = true; + dependsOn = [ + "solidtimeDb" + ]; ports = [ # Open ports if you don't use Traefik - "${toString port}:8000" + "${toString cfg.port}:8000" + ]; + networks = [ + networkName + internalNetworkName ]; extraOptions = [ - "--network=${networkName}" - "--network=${internalNetworkName}" - # Healthecks # test: [ "CMD-SHELL", "curl --fail http://localhost:8000/health-check/up || exit 1" ] ''--health-cmd=curl --fail http://localhost:8000/health-check/up || exit 1'' ]; inherit volumes; - dependsOn = [ - "solidtimeDb" - ]; + labels = { + "traefik.enable" = "true"; + "traefik.http.routers.solidtime.rule" = "Host(`time.${config.networking.hostName}.depeuter.dev`)"; + "traefik.http.services.solidtime.loadbalancer.server.port" = toString cfg.port; + }; environmentFiles = [ "/home/admin/.solidtime.env" ]; environment = laravelEnv // { CONTAINER_MODE = "http"; }; - labels = { - "traefik.enable" = "true"; - "traefik.http.routers.solidtime.rule" = "Host(`time.${config.networking.hostName}.depeuter.dev`)"; - "traefik.http.services.solidtime.loadbalancer.server.port" = toString port; - }; }; solidtimeScheduler = { hostname = "scheduler"; image = "solidtime/solidtime:${version}"; inherit user; autoStart = true; + dependsOn = [ + "solidtimeDb" + ]; + networks = [ + internalNetworkName + ]; extraOptions = [ - "--network=${internalNetworkName}" - # Healthchecks # test: [ "CMD-SHELL", "supervisorctl status scheduler:scheduler_00" ] ''--health-cmd="supervisorctl status scheduler:scheduler_00"'' ]; inherit volumes; - dependsOn = [ - "solidtimeDb" - ]; environmentFiles = [ "/home/admin/.solidtime.env" ]; @@ -175,9 +185,10 @@ in { image = "solidtime/solidtime:${version}"; inherit user; autoStart = true; + networks = [ + internalNetworkName + ]; extraOptions = [ - "--network=${internalNetworkName}" - # Healthchecks # test: [ "CMD-SHELL", "supervisorctl status worker:worker_00" ] ''--health-cmd="supervisorctl status worker:worker_00"'' @@ -201,9 +212,10 @@ in { ports = [ # "${toString dbExternalPort}:${toString dbInternalPort}" ]; + networks = [ + internalNetworkName + ]; extraOptions = [ - "--network=${internalNetworkName}" - # Healthchecks # test: - CMD - pg_isready - '-q' - '-d' - '${DB_DATABASE}' - '-U' - '${DB_USERNAME}' retries: 3 timeout: 5s ''--health-cmd="pg_isready -q -d ${laravelEnv.DB_DATABASE} -U ${laravelEnv.DB_USERNAME}"'' @@ -224,9 +236,10 @@ in { hostname = "gotenberg"; image = "gotenberg/gotenberg:8"; autoStart = true; + networks = [ + internalNetworkName + ]; extraOptions = [ - "--network=${internalNetworkName}" - # Healthchecks # test: [ "CMD", "curl", "--silent", "--fail", "http://localhost:3000/health" ] ''--health-cmd="curl --silent --fail http://localhost:${toString gotenbergPort}/health"'' From 84c94afda46feab5947f5050b0fb9def869ea6a4 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Tue, 10 Feb 2026 08:37:32 +0100 Subject: [PATCH 36/44] chore(solidtime): Update to 0.9.0 --- modules/apps/solidtime/default.nix | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/modules/apps/solidtime/default.nix b/modules/apps/solidtime/default.nix index 17193a3..0a62900 100644 --- a/modules/apps/solidtime/default.nix +++ b/modules/apps/solidtime/default.nix @@ -7,7 +7,7 @@ let internalNetworkName = "solidtime-internal"; proxyNet = config.homelab.apps.traefiik.sharedNetworkName; - version = "0.8.0"; + version = "0.9.0"; user = "1000:1000"; @@ -140,8 +140,8 @@ in { ]; extraOptions = [ # Healthecks - # test: [ "CMD-SHELL", "curl --fail http://localhost:8000/health-check/up || exit 1" ] - ''--health-cmd=curl --fail http://localhost:8000/health-check/up || exit 1'' + # test: [ "CMD", "curl", "--fail", "http://localhost:8000/health-check/up" ] + ''--health-cmd=curl --fail http://localhost:8000/health-check/up'' ]; inherit volumes; labels = { @@ -169,8 +169,8 @@ in { ]; extraOptions = [ # Healthchecks - # test: [ "CMD-SHELL", "supervisorctl status scheduler:scheduler_00" ] - ''--health-cmd="supervisorctl status scheduler:scheduler_00"'' + # test: [ "CMD", "healthcheck" ] + ''--health-cmd="healthcheck"'' ]; inherit volumes; environmentFiles = [ @@ -190,8 +190,8 @@ in { ]; extraOptions = [ # Healthchecks - # test: [ "CMD-SHELL", "supervisorctl status worker:worker_00" ] - ''--health-cmd="supervisorctl status worker:worker_00"'' + # test: [ "CMD", "healthcheck" ] + ''--health-cmd="healthcheck"'' ]; inherit volumes; dependsOn = [ From 687b85eeefe14c553f6cda03c3c57c7ce716229f Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Tue, 10 Feb 2026 09:08:11 +0100 Subject: [PATCH 37/44] feat(solidtime): Preload Docker images --- modules/apps/solidtime/default.nix | 42 +++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/modules/apps/solidtime/default.nix b/modules/apps/solidtime/default.nix index 0a62900..3348e0b 100644 --- a/modules/apps/solidtime/default.nix +++ b/modules/apps/solidtime/default.nix @@ -18,6 +18,15 @@ let inherit (config.virtualisation.oci-containers) containers; + solidtimeImageName = "solidtime/solidtime"; + solidtimeImage = "${solidtimeImageName}:${version}"; + solidtimeImageFile = pkgs.dockerTools.pullImage { + imageName = solidtimeImageName; + finalImageTag = version; + imageDigest = "sha256:5601b81f63ece49cdc4110f90f7624b0cc4ba87448b3e4bddce904457822a4ef"; + sha256 = "sha256-/VwuJGczs41XeItNamLahLt/ypUQHTn9AcwneVw/3YY="; + }; + volumes = [ "solidtime-storage:/var/www/html/storage" "solidtime-logs:/var/www/html/storage/logs" @@ -124,7 +133,8 @@ in { virtualisation.oci-containers.containers = { solidtime = { hostname = "solidtime"; - image = "solidtime/solidtime:${version}"; + image = solidtimeImage; + imageFile = solidtimeImageFile; inherit user; autoStart = true; dependsOn = [ @@ -158,7 +168,8 @@ in { }; solidtimeScheduler = { hostname = "scheduler"; - image = "solidtime/solidtime:${version}"; + image = solidtimeImage; + imageFile = solidtimeImageFile; inherit user; autoStart = true; dependsOn = [ @@ -182,7 +193,8 @@ in { }; solidtimeQueue = { hostname = "queue"; - image = "solidtime/solidtime:${version}"; + image = solidtimeImage; + imageFile = solidtimeImageFile; inherit user; autoStart = true; networks = [ @@ -205,9 +217,17 @@ in { WORKER_COMMAND = "php /var/www/html/artisan queue:work"; }; }; - solidtimeDb = { + solidtimeDb = let + imageName = "postgres"; + finalImageTag = "15"; + in { hostname = "database"; - image = "postgres:15"; + image = "${imageName}:${finalImageTag}"; + imageFile = pkgs.dockerTools.pullImage { + inherit imageName finalImageTag; + imageDigest = "sha256:98fe06b500b5eb29e45bf8c073eb0ca399790ce17b1d586448edc4203627d342"; + sha256 = "sha256-AZ4VkOlROX+nR/MjDjsA4xdHzmtKjiBAtsp2Q6IdOvg="; + }; autoStart = true; ports = [ # "${toString dbExternalPort}:${toString dbInternalPort}" @@ -232,9 +252,17 @@ in { POSTGRES_PASSWORD = laravelEnv.DB_PASSWORD; }; }; - solidtimeGotenberg = { + solidtimeGotenberg = let + imageName = "gotenberg/gotenberg"; + finalImageTag = "8.26.0"; + in { hostname = "gotenberg"; - image = "gotenberg/gotenberg:8"; + image = "${imageName}:${finalImageTag}"; + imageFile = pkgs.dockerTools.pullImage { + inherit imageName finalImageTag; + imageDigest = "sha256:328551506b3dec3ff6381dd47e5cd72a44def97506908269e201a8fbfa1c12c0"; + sha256 = "sha256-1zz4xDAgXxHUnkCVIfjHTgXb82EFEx+5am6Cu9+eZj4="; + }; autoStart = true; networks = [ internalNetworkName From 58f2ea9b74731144f1f375419ad1b7f43980cfec Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Tue, 10 Feb 2026 09:25:16 +0100 Subject: [PATCH 38/44] chore(solidtime): Update to 0.10.0 --- modules/apps/solidtime/default.nix | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/modules/apps/solidtime/default.nix b/modules/apps/solidtime/default.nix index 3348e0b..725d32d 100644 --- a/modules/apps/solidtime/default.nix +++ b/modules/apps/solidtime/default.nix @@ -7,8 +7,6 @@ let internalNetworkName = "solidtime-internal"; proxyNet = config.homelab.apps.traefiik.sharedNetworkName; - version = "0.9.0"; - user = "1000:1000"; # dbExternalPort = ...; @@ -19,12 +17,13 @@ let inherit (config.virtualisation.oci-containers) containers; solidtimeImageName = "solidtime/solidtime"; + version = "0.10.0"; solidtimeImage = "${solidtimeImageName}:${version}"; solidtimeImageFile = pkgs.dockerTools.pullImage { imageName = solidtimeImageName; finalImageTag = version; - imageDigest = "sha256:5601b81f63ece49cdc4110f90f7624b0cc4ba87448b3e4bddce904457822a4ef"; - sha256 = "sha256-/VwuJGczs41XeItNamLahLt/ypUQHTn9AcwneVw/3YY="; + imageDigest = "sha256:817d3a366ecc39f0473d7154372afa82dd4e6e50c66d70be45804892c8421cbb"; + sha256 = "sha256-h5aCKaquUF/EVsOHaLOHrn1HAoXZYPhAbJ+e4cmjSA8="; }; volumes = [ From 647ccfd6e23cb16cd17877ac1d613ec56fb7e58a Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Fri, 17 Jul 2026 22:02:42 +0200 Subject: [PATCH 39/44] chore: initialize v2 SOPS configuration and README --- .sops.yaml | 29 +++++++++++++++++++++++++++++ README.md | 31 +++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 .sops.yaml create mode 100644 README.md diff --git a/.sops.yaml b/.sops.yaml new file mode 100644 index 0000000..5cba236 --- /dev/null +++ b/.sops.yaml @@ -0,0 +1,29 @@ +# SOPS configuration for NixOS GitOps Migration (v2) +# See README.md for key management instructions. + +keys: + # Master Keys (Used for generic/global secrets if needed, but usually we encrypt for specific hosts) + - &prod_master age1... # TODO: Replace with the public key of the prod-master offline USB key + - &staging_master age1... # TODO: Replace with the public key of the staging-master key + + # Host Keys (Used by the specific VM to decrypt its own secrets at boot) + # - &host_control_center age1... + +creation_rules: + # Staging Environment + # All files under secrets/staging/ are encrypted purely with the staging_master key. + # Ephemeral VMs are injected with this private key at boot via Cloud-Init. + - path_regex: secrets/staging/.*\.ya?ml$ + key_groups: + - age: + - *staging_master + + # Production Environment + # All files under secrets/prod/ are encrypted with the prod_master key AND the specific host's key. + # Ephemeral VMs CANNOT decrypt these files. + - path_regex: secrets/prod/.*\.ya?ml$ + key_groups: + - age: + - *prod_master + # Add host keys here as they are provisioned + # - *host_control_center diff --git a/README.md b/README.md new file mode 100644 index 0000000..cdc03ab --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# NixOS GitOps Migration (v2) + +This branch contains the automated, pull-based GitOps architecture using `comin`, Terraform, and Forgejo. + +## Secret Management (SOPS-Nix) + +This repository uses `sops-nix` for secret management, adhering to a strict separation between Production and Staging environments to prevent credential leakage during CI runs. + +### Production Keys +* **`prod-master`**: This is the ultimate master key for production secrets. + * **Best Practice**: The private key is **NEVER** stored on any server (including the Control Center VM). It is kept locally on a securely backed-up USB key and a paper backup. + * You only import the private key to your local machine temporarily when you need to encrypt a new secret or during a disaster recovery scenario. +* **Host Keys**: Production VMs use their individual SSH host keys (generated during provisioning) to decrypt their specific secrets at boot time. + +### Staging Keys +* **`staging-master`**: This key is used exclusively for encrypting secrets inside the `secrets/staging/` directory. + * The public key is in `.sops.yaml`. + * The private key is stored securely in Forgejo Secrets. + * During CI runs, the Forgejo Runner injects this private key into the ephemeral Staging VMs via Cloud-Init. + * Even if this key is compromised, it cannot decrypt production data. + +### Generating the Master Keys +To generate your master keys (run this on your secure local workstation): +```bash +# Generate the prod-master key (save the private key to your USB drive) +age-keygen -o prod-master.txt + +# Generate the staging-master key (upload the private key to Forgejo Secrets) +age-keygen -o staging-master.txt +``` +Update `.sops.yaml` with the public keys outputted by these commands. From 17de32268d61ceef0ddbae937403f4ee21598bec Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Fri, 17 Jul 2026 22:05:43 +0200 Subject: [PATCH 40/44] feat: port Phase 1 foundational modules and setup comin in flake.nix --- flake.nix | 65 ++++++++++++++++++++++++ modules/common/default.nix | 16 ++++++ modules/default.nix | 7 +++ modules/services/default.nix | 5 ++ modules/services/openssh/default.nix | 20 ++++++++ modules/virtualisation/default.nix | 5 ++ modules/virtualisation/guest/default.nix | 34 +++++++++++++ users/admin/default.nix | 39 ++++++++++++++ users/default.nix | 5 ++ 9 files changed, 196 insertions(+) create mode 100644 flake.nix create mode 100644 modules/common/default.nix create mode 100644 modules/default.nix create mode 100644 modules/services/default.nix create mode 100644 modules/services/openssh/default.nix create mode 100644 modules/virtualisation/default.nix create mode 100644 modules/virtualisation/guest/default.nix create mode 100644 users/admin/default.nix create mode 100644 users/default.nix diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..5589e5a --- /dev/null +++ b/flake.nix @@ -0,0 +1,65 @@ +{ + description = "Homelab configuration using flakes (v2 GitOps)"; + + inputs = { + 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"; + }; + + comin = { + url = "github:nlewo/comin"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = inputs@{ + self, nixpkgs, + flake-utils, sops-nix, utils, comin, + ... + }: + let + system = utils.lib.system.x86_64-linux; + in + utils.lib.mkFlake { + inherit self inputs; + + hostDefaults = { + inherit system; + + modules = [ + ./modules + ./users + + sops-nix.nixosModules.sops + comin.nixosModules.comin + + # Base comin configuration for all nodes + ({ config, pkgs, ... }: { + services.comin = { + enable = true; + remotes = [{ + name = "origin"; + # Replace with actual internal forgejo URL once available + url = "https://github.com/example/nix-config.git"; + branches.main.name = "v2"; + }]; + }; + }) + ]; + }; + + hosts = { + # Hosts will be populated here as they are migrated to the v2 branch. + }; + }; +} diff --git a/modules/common/default.nix b/modules/common/default.nix new file mode 100644 index 0000000..44309f5 --- /dev/null +++ b/modules/common/default.nix @@ -0,0 +1,16 @@ +{ + config = { + homelab = { + services.openssh.enable = true; + users.admin.enable = true; + }; + + nix.settings.experimental-features = [ + "flakes" + "nix-command" + ]; + + # Set your time zone. + time.timeZone = "Europe/Brussels"; + }; +} diff --git a/modules/default.nix b/modules/default.nix new file mode 100644 index 0000000..6d76ee2 --- /dev/null +++ b/modules/default.nix @@ -0,0 +1,7 @@ +{ + imports = [ + ./common + ./services + ./virtualisation + ]; +} diff --git a/modules/services/default.nix b/modules/services/default.nix new file mode 100644 index 0000000..f7295a5 --- /dev/null +++ b/modules/services/default.nix @@ -0,0 +1,5 @@ +{ + imports = [ + ./openssh + ]; +} diff --git a/modules/services/openssh/default.nix b/modules/services/openssh/default.nix new file mode 100644 index 0000000..4b9cb5e --- /dev/null +++ b/modules/services/openssh/default.nix @@ -0,0 +1,20 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.homelab.services.openssh; +in { + options.homelab.services.openssh.enable = lib.mkEnableOption "OpenSSH daemon"; + + config = lib.mkIf cfg.enable { + services.openssh = { + # Enable the OpenSSH daemon. + enable = true; + settings = { + PasswordAuthentication = false; + PermitRootLogin = "no"; + # Disable keyboard-interactive authentication. + KbdInteractiveAuthentication = false; + }; + }; + }; +} diff --git a/modules/virtualisation/default.nix b/modules/virtualisation/default.nix new file mode 100644 index 0000000..50418a4 --- /dev/null +++ b/modules/virtualisation/default.nix @@ -0,0 +1,5 @@ +{ + imports = [ + ./guest + ]; +} diff --git a/modules/virtualisation/guest/default.nix b/modules/virtualisation/guest/default.nix new file mode 100644 index 0000000..bbb9a50 --- /dev/null +++ b/modules/virtualisation/guest/default.nix @@ -0,0 +1,34 @@ +{ config, lib, modulesPath, ... }: + +let + cfg = config.homelab.virtualisation.guest; +in { + options.homelab.virtualisation.guest.enable = lib.mkEnableOption "Settings for devices running on virtualisation, e.g. Proxmox"; + + imports = [ + (modulesPath + "/profiles/qemu-guest.nix") + ]; + + config = lib.mkIf cfg.enable { + boot = { + # Whether to enable growing the root partition on boot. + growPartition = true; + # Use Grub bootloader + loader.grub = { + enable = true; + devices = [ + "nodev" + ]; + }; + }; + + fileSystems."/" = lib.mkDefault { + device = "/dev/disk/by-label/nixos"; + autoResize = true; + fsType = "ext4"; + }; + + # Enable QEMU Guest for Proxmox + services.qemuGuest.enable = true; + }; +} diff --git a/users/admin/default.nix b/users/admin/default.nix new file mode 100644 index 0000000..dc01c81 --- /dev/null +++ b/users/admin/default.nix @@ -0,0 +1,39 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.homelab.users.admin; +in { + options.homelab.users.admin = { + enable = lib.mkEnableOption "user System Administrator"; + authorizedKeys = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ + # HomeLab > NixOS > admin > ssh + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGWIOOEqTy8cWKpENVbzD4p7bsQgQb/Dgpzk8i0dZ00T" + ]; + }; + }; + + config = lib.mkIf cfg.enable { + nix.settings.trusted-users = [ + config.users.users.gh0st.name + ]; + + users.users.gh0st = { + description = "System Administrator"; + isNormalUser = true; + extraGroups = [ + config.users.groups.wheel.name # Enable 'sudo' for the user. + ]; + initialPassword = "ChangeMe"; + openssh.authorizedKeys.keys = cfg.authorizedKeys; + packages = with pkgs; [ + curl + git + tmux + vim + wget + ]; + }; + }; +} diff --git a/users/default.nix b/users/default.nix new file mode 100644 index 0000000..26b5ff9 --- /dev/null +++ b/users/default.nix @@ -0,0 +1,5 @@ +{ + imports = [ + ./admin + ]; +} From 12b1faf029aa1095bc53145cd495575c7ea025fa Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Fri, 17 Jul 2026 22:06:44 +0200 Subject: [PATCH 41/44] feat(terraform): add proxmox-bootstrap state for user and role creation --- terraform/proxmox-bootstrap/main.tf | 71 +++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 terraform/proxmox-bootstrap/main.tf diff --git a/terraform/proxmox-bootstrap/main.tf b/terraform/proxmox-bootstrap/main.tf new file mode 100644 index 0000000..720d183 --- /dev/null +++ b/terraform/proxmox-bootstrap/main.tf @@ -0,0 +1,71 @@ +terraform { + required_providers { + proxmox = { + source = "bpg/proxmox" + version = "~> 0.61.0" + } + } +} + +# This bootstrap state must be run manually ONCE with the root@pam credentials +# to establish the restricted terraform@pve user for the rest of the CI pipeline. +provider "proxmox" { + # Configuration can be passed via environment variables: + # PROXMOX_VE_ENDPOINT=https://your-proxmox-ip:8006/ + # PROXMOX_VE_USERNAME=root@pam + # PROXMOX_VE_PASSWORD=your-root-password + # PROXMOX_VE_INSECURE=true # If using self-signed certs +} + +resource "proxmox_virtual_environment_role" "terraform_prov" { + role_id = "TerraformProv" + + privileges = [ + "VM.Allocate", + "VM.Audit", + "VM.Clone", + "VM.Config.CDROM", + "VM.Config.CPU", + "VM.Config.Disk", + "VM.Config.HWType", + "VM.Config.Memory", + "VM.Config.Network", + "VM.Config.Options", + "VM.Monitor", + "VM.PowerMgmt", + "Datastore.AllocateSpace", + "Datastore.Audit", + "SDN.Use" + ] +} + +resource "proxmox_virtual_environment_user" "terraform_user" { + user_id = "terraform@pve" + comment = "Managed by Terraform (proxmox-bootstrap) for GitOps CI/CD" +} + +resource "proxmox_virtual_environment_acl" "terraform_vms" { + user_id = proxmox_virtual_environment_user.terraform_user.user_id + role_id = proxmox_virtual_environment_role.terraform_prov.role_id + path = "/vms" +} + +resource "proxmox_virtual_environment_acl" "terraform_storage" { + user_id = proxmox_virtual_environment_user.terraform_user.user_id + role_id = proxmox_virtual_environment_role.terraform_prov.role_id + # Update this path to match your actual local-zfs or TrueNAS mounted storage + path = "/storage/local-zfs" +} + +resource "proxmox_virtual_environment_user_token" "terraform_token" { + user_id = proxmox_virtual_environment_user.terraform_user.user_id + token_id = "tf-automation" + privsep = false + comment = "Token for Forgejo CI/CD to provision VMs" +} + +output "terraform_api_token" { + value = proxmox_virtual_environment_user_token.terraform_token.value + sensitive = true + description = "The secret API token for terraform@pve. Save this to Forgejo Secrets as PROXMOX_VE_API_TOKEN." +} From dd2065cf602a8f89be235676af676ba949eb3ea6 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Fri, 17 Jul 2026 22:07:49 +0200 Subject: [PATCH 42/44] ci: add golden image build action and document disaster recovery --- .forgejo/workflows/build-golden-image.yml | 42 +++++++++++++++++++++++ DISASTER_RECOVERY.md | 32 +++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 .forgejo/workflows/build-golden-image.yml create mode 100644 DISASTER_RECOVERY.md diff --git a/.forgejo/workflows/build-golden-image.yml b/.forgejo/workflows/build-golden-image.yml new file mode 100644 index 0000000..fe71f64 --- /dev/null +++ b/.forgejo/workflows/build-golden-image.yml @@ -0,0 +1,42 @@ +name: Build Golden Image + +on: + push: + paths: + - 'hosts/Template/**' + - 'flake.nix' + - 'flake.lock' + workflow_dispatch: + +jobs: + build: + runs-on: self-hosted + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Install Nix + uses: cachix/install-nix-action@v27 + with: + extra_nix_config: | + experimental-features = nix-command flakes + + - name: Build Proxmox Image + run: | + nix run github:nix-community/nixos-generators -- --format proxmox -c hosts/Template/default.nix -o result + + - name: Upload to Proxmox + env: + PROXMOX_URL: "https://proxmox.local:8006/api2/json" + PROXMOX_TOKEN_ID: "terraform@pve!tf-automation" + PROXMOX_TOKEN_SECRET: ${{ secrets.PROXMOX_TOKEN_SECRET }} + NODE_NAME: "pve" + STORAGE_NAME: "local-zfs" + VMID: 9000 + run: | + IMAGE_PATH=$(find result -name "*.qcow2" | head -n 1) + echo "Uploading $IMAGE_PATH to Proxmox as Template $VMID" + # In a real scenario, this would use a script or API client to upload the image + # e.g., using qm importdisk via ssh or the Proxmox API directly. + # For example, using a custom script: ./scripts/upload-to-proxmox.sh $IMAGE_PATH $VMID + echo "TODO: Implement Proxmox upload API call using PROXMOX_TOKEN_SECRET" diff --git a/DISASTER_RECOVERY.md b/DISASTER_RECOVERY.md new file mode 100644 index 0000000..2d5464b --- /dev/null +++ b/DISASTER_RECOVERY.md @@ -0,0 +1,32 @@ +# Disaster Recovery Plan (Zero-Login Strategy) + +In the event of a catastrophic failure, this document outlines how to restore services without causing configuration drift (i.e., without SSHing directly into target nodes and making manual undocumented changes). + +## 1. Full Proxmox Host Freeze (Hardware/GPU Bug) +If the entire Proxmox hypervisor freezes (often caused by the AMD GPU reset bug when a VM reboots): +* **DO NOT** attempt to SSH into the host or the VMs. +* **Recovery Action**: Use your Out-of-Band Management (OOBM) solution. Access your PiKVM, IPMI interface, or remotely controlled Smart Plug to perform a **hard power cycle** of the host. +* Upon reboot, Proxmox will automatically start the VMs, and `comin` will pull the latest working configuration. + +## 2. Production Service Failure (Code Bug) +If a bad PR was merged and a stateless service is failing to start: +* **DO NOT** SSH into the VM to fix the config manually. +* **Recovery Action**: Open the Forgejo Web UI and click **Revert** on the merged PR. +* Within 60 seconds, the `comin` agent running on the target VM will detect the new commit on the `v2` branch, pull the reverted configuration, and restart the service automatically. + +## 3. Production Data Corruption (Database/Stateful Bug) +If a bad deployment corrupted persistent data on TrueNAS (e.g., a failed one-way database schema migration): +* **Recovery Action**: + 1. Revert the code PR in Forgejo as described in section 2. + 2. Execute the `scripts/dr-rollback-dataset.sh` script from the secure **Control Center VM**. This script uses the TrueNAS REST API to rollback the specific ZFS dataset to the automated snapshot taken immediately prior to the deployment. + +## 4. Total Git Repository Loss +If the `v2` branch is completely destroyed or the Gitea server is unrecoverable: +* Your `prod-master` private age key is stored safely offline on your USB drive. +* A recent backup of the repository should be available via your external backup mechanism. +* Clone the backup to your local workstation, run the `proxmox-bootstrap` Terraform state locally to ensure the hypervisor is reachable, and use Terraform to redeploy the Control Center VM and Forgejo instances. + +## 5. Manually Obtaining the Golden Image +If the CI pipeline is down and you urgently need to provision a new VM: +1. Run `nix run github:nix-community/nixos-generators -- --format proxmox -c hosts/Template/default.nix -o result` on your local machine. +2. The output `.qcow2` image can be uploaded to the Proxmox Web UI manually under `local` -> `ISO Images` (or directly via `qm importdisk` if you must use SSH as a last resort). From 960f8f63fe3f0cd8c953de494b9f0aefd8895cc9 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Fri, 17 Jul 2026 22:08:37 +0200 Subject: [PATCH 43/44] ci: add Control Center bootstrap script and RenovateBot workflow --- .forgejo/workflows/renovate.yml | 25 +++++++++++++++++++++ scripts/control-center-bootstrap.sh | 35 +++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 .forgejo/workflows/renovate.yml create mode 100755 scripts/control-center-bootstrap.sh diff --git a/.forgejo/workflows/renovate.yml b/.forgejo/workflows/renovate.yml new file mode 100644 index 0000000..eb11f07 --- /dev/null +++ b/.forgejo/workflows/renovate.yml @@ -0,0 +1,25 @@ +name: RenovateBot + +on: + schedule: + # Run at 2:00 AM every day + - cron: '0 2 * * *' + workflow_dispatch: + +jobs: + renovate: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Self-hosted Renovate + uses: renovatebot/github-action@v40.1.11 + env: + # Token needs permission to read/write repository contents and pull requests + RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }} + # The platform needs to be explicitly defined for Forgejo/Gitea + RENOVATE_PLATFORM: "gitea" + RENOVATE_ENDPOINT: "https://your-forgejo-instance.local/api/v1" + # The repository format: owner/repo + RENOVATE_REPOSITORIES: "your-username/nix-config" diff --git a/scripts/control-center-bootstrap.sh b/scripts/control-center-bootstrap.sh new file mode 100755 index 0000000..1c12a73 --- /dev/null +++ b/scripts/control-center-bootstrap.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo "==========================================" +echo " Bootstrapping Control Center VM " +echo "==========================================" + +# Ensure script is run with appropriate permissions (this script might run as part of Cloud-Init or manually) + +if ! command -v terraform &> /dev/null; then + echo "Terraform not found. Please ensure it is installed via Nix." + exit 1 +fi + +echo "1. Initializing proxmox-bootstrap state..." +cd terraform/proxmox-bootstrap +terraform init + +echo "2. Applying proxmox-bootstrap (Requires manual approval if not passing -auto-approve)" +echo " This will configure the restricted terraform@pve user on the Proxmox host." +# Uncomment the following line to auto-approve in a fully automated environment +# terraform apply -auto-approve +terraform apply + +echo "3. Fetching the generated API token..." +TF_TOKEN=$(terraform output -raw terraform_api_token) + +echo "==========================================" +echo " IMPORTANT: Save the following token into " +echo " Forgejo Secrets as PROXMOX_TOKEN_SECRET " +echo "==========================================" +echo "$TF_TOKEN" +echo "==========================================" + +echo "Control Center Bootstrap Complete." From 2386e1e9429ca9686f42d4f84cbe1417c5e1cd81 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Fri, 17 Jul 2026 22:11:41 +0200 Subject: [PATCH 44/44] docs: add comprehensive BOOTSTRAP.md guide for manual initialization --- BOOTSTRAP.md | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 3 +++ 2 files changed, 55 insertions(+) create mode 100644 BOOTSTRAP.md diff --git a/BOOTSTRAP.md b/BOOTSTRAP.md new file mode 100644 index 0000000..836bafc --- /dev/null +++ b/BOOTSTRAP.md @@ -0,0 +1,52 @@ +# Bootstrap Guide + +This document outlines the manual, real-world steps required to initialize the NixOS GitOps environment for the first time. You must perform these steps before the CI/CD pipeline or any automated staging environments can function. + +## 1. Secret Management Initialization (SOPS-Nix) + +We use `sops-nix` to manage secrets, adhering to a strict separation between Production and Staging. You must generate these keys locally on a secure workstation. + +**Prerequisites:** Install `age` ([age documentation](https://github.com/FiloSottile/age)). + +1. **Generate the Production Master Key:** + ```bash + age-keygen -o prod-master.txt + ``` + > [!CAUTION] + > Move `prod-master.txt` to a secure offline USB drive and/or print it on paper. **Do not** store this private key on any server. + +2. **Generate the Staging Master Key:** + ```bash + age-keygen -o staging-master.txt + ``` + +3. **Update Configuration:** + Open both `.txt` files and copy their **Public Keys** (the strings starting with `age1...`). Open `.sops.yaml` in the root of this repository and replace the `# TODO` placeholders with your newly generated public keys. Commit and push this change. + +## 2. Proxmox Hypervisor Authentication + +The CI/CD pipeline needs restricted API access to Proxmox to provision Virtual Machines. + +**Prerequisites:** Install `terraform` ([Terraform installation](https://developer.hashicorp.com/terraform/downloads)). + +1. Ensure you have network access to your Proxmox host (e.g., via Tailscale). +2. Execute the bootstrap script from the root of the repository: + ```bash + ./scripts/control-center-bootstrap.sh + ``` +3. Terraform will prompt you. You will need to provide your Proxmox `root@pam` credentials via environment variables or prompt. +4. Upon successful completion, the script will output a secure **API Token**. Copy this token securely. + +## 3. Forgejo Secrets Configuration + +The CI/CD actions require access to the Proxmox token and the staging secret key. + +1. Navigate to your Forgejo Web UI. +2. Go to **Settings > Actions > Secrets** for this repository. +3. Add the following repository secrets: + * `PROXMOX_TOKEN_SECRET`: Paste the token generated from Step 2. + * `STAGING_AGE_KEY`: Paste the *entire contents* of your `staging-master.txt` file (the private key). + * `RENOVATE_TOKEN`: Create a Personal Access Token (PAT) for your user in Forgejo with read/write access to code and pull requests, and paste it here. + +## Next Steps +Once these bootstrap steps are complete, the foundational authentication is in place. The Forgejo CI actions will now have the necessary permissions to build images, provision VMs, and test staging environments autonomously. diff --git a/README.md b/README.md index cdc03ab..cfdb027 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,9 @@ This branch contains the automated, pull-based GitOps architecture using `comin`, Terraform, and Forgejo. +> [!IMPORTANT] +> **Getting Started:** If you are setting up this repository from scratch, you **must** follow the steps in [BOOTSTRAP.md](file:///c:/Users/tibod/Documents/projects/Bos55/nix-config/BOOTSTRAP.md) before the automated pipelines can function. + ## Secret Management (SOPS-Nix) This repository uses `sops-nix` for secret management, adhering to a strict separation between Production and Staging environments to prevent credential leakage during CI runs.