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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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. From ac224b72949c184b3fe8cfd31b6b76be66251b1b Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Fri, 17 Jul 2026 22:15:28 +0200 Subject: [PATCH 45/76] feat(staging): implement ephemeral staging VM provisioning and TrueNAS snapshot clone workflow --- .forgejo/workflows/staging.yml | 62 +++++++++++++++++++ scripts/truenas-staging-clone.sh | 35 +++++++++++ scripts/truenas-staging-teardown.sh | 39 ++++++++++++ terraform/staging-env/main.tf | 96 +++++++++++++++++++++++++++++ 4 files changed, 232 insertions(+) create mode 100644 .forgejo/workflows/staging.yml create mode 100755 scripts/truenas-staging-clone.sh create mode 100755 scripts/truenas-staging-teardown.sh create mode 100644 terraform/staging-env/main.tf diff --git a/.forgejo/workflows/staging.yml b/.forgejo/workflows/staging.yml new file mode 100644 index 0000000..95c5e8b --- /dev/null +++ b/.forgejo/workflows/staging.yml @@ -0,0 +1,62 @@ +name: Dynamic Staging Environment + +on: + pull_request: + types: [opened, synchronize, closed] + +jobs: + manage-staging: + runs-on: self-hosted + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Setup Terraform + uses: hashicorp/setup-terraform@v3 + + - name: Provision Staging Environment (Opened/Sync) + if: github.event.action == 'opened' || github.event.action == 'synchronize' + env: + PROXMOX_VE_ENDPOINT: "https://proxmox.local:8006/" + PROXMOX_VE_API_TOKEN: ${{ secrets.PROXMOX_TOKEN_SECRET }} + TF_VAR_pr_number: ${{ github.event.pull_request.number }} + # VM_ID could be dynamically generated or based on PR number (e.g., 8000 + PR_NUMBER) + TF_VAR_vm_id: ${{ format('8{0:03}', github.event.pull_request.number) }} + TF_VAR_staging_age_key: ${{ secrets.STAGING_AGE_KEY }} + + # TrueNAS integration (Secrets would need to be added to Forgejo) + TRUENAS_IP: "truenas.local" + TRUENAS_API_KEY: ${{ secrets.TRUENAS_API_KEY }} + POOL_NAME: "tank" + SOURCE_DATASET: "apps/production_data" + run: | + echo "Cloning datasets..." + # ./scripts/truenas-staging-clone.sh + + echo "Applying Terraform for PR ${{ github.event.pull_request.number }}..." + cd terraform/staging-env + terraform init + terraform apply -auto-approve + + - name: Teardown Staging Environment (Closed) + if: github.event.action == 'closed' + env: + PROXMOX_VE_ENDPOINT: "https://proxmox.local:8006/" + PROXMOX_VE_API_TOKEN: ${{ secrets.PROXMOX_TOKEN_SECRET }} + TF_VAR_pr_number: ${{ github.event.pull_request.number }} + TF_VAR_vm_id: ${{ format('8{0:03}', github.event.pull_request.number) }} + TF_VAR_staging_age_key: "" + + # TrueNAS integration + TRUENAS_IP: "truenas.local" + TRUENAS_API_KEY: ${{ secrets.TRUENAS_API_KEY }} + POOL_NAME: "tank" + SOURCE_DATASET: "apps/production_data" + run: | + echo "Destroying Terraform environment for PR ${{ github.event.pull_request.number }}..." + cd terraform/staging-env + terraform init + terraform destroy -auto-approve + + echo "Tearing down datasets..." + # ./scripts/truenas-staging-teardown.sh diff --git a/scripts/truenas-staging-clone.sh b/scripts/truenas-staging-clone.sh new file mode 100755 index 0000000..1659303 --- /dev/null +++ b/scripts/truenas-staging-clone.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +set -euo pipefail + +# This script creates a ZFS snapshot of a production dataset and clones it for a staging environment. +# Required environment variables: +# TRUENAS_IP: The IP address of the TrueNAS scale instance +# TRUENAS_API_KEY: The API token for TrueNAS +# POOL_NAME: The name of the ZFS pool (e.g., "tank") +# SOURCE_DATASET: The name of the production dataset (e.g., "apps/jellyfin") +# PR_NUMBER: The Pull Request number + +if [[ -z "${TRUENAS_IP:-}" || -z "${TRUENAS_API_KEY:-}" || -z "${POOL_NAME:-}" || -z "${SOURCE_DATASET:-}" || -z "${PR_NUMBER:-}" ]]; then + echo "Error: Missing required environment variables." + exit 1 +fi + +BASE_URL="http://${TRUENAS_IP}/api/v2.0" +HEADERS=( + "-H" "Authorization: Bearer ${TRUENAS_API_KEY}" + "-H" "Content-Type: application/json" +) + +DATASET_ID="${POOL_NAME}/${SOURCE_DATASET}" +SNAPSHOT_NAME="pr-${PR_NUMBER}-base" +CLONE_DATASET_NAME="${SOURCE_DATASET}-pr-${PR_NUMBER}" + +echo "1. Creating snapshot of ${DATASET_ID} @ ${SNAPSHOT_NAME}..." +curl -s -X POST "${BASE_URL}/zfs/snapshot" "${HEADERS[@]}" \ + -d "{\"dataset\": \"${DATASET_ID}\", \"name\": \"${SNAPSHOT_NAME}\"}" > /dev/null + +echo "2. Cloning snapshot to ${POOL_NAME}/${CLONE_DATASET_NAME}..." +curl -s -X POST "${BASE_URL}/zfs/snapshot/clone" "${HEADERS[@]}" \ + -d "{\"snapshot\": \"${DATASET_ID}@${SNAPSHOT_NAME}\", \"dataset_dst\": \"${CLONE_DATASET_NAME}\"}" > /dev/null + +echo "Staging dataset cloned successfully." diff --git a/scripts/truenas-staging-teardown.sh b/scripts/truenas-staging-teardown.sh new file mode 100755 index 0000000..9b68f26 --- /dev/null +++ b/scripts/truenas-staging-teardown.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +set -euo pipefail + +# This script destroys the ephemeral staging ZFS clone and the base snapshot. +# Required environment variables: +# TRUENAS_IP: The IP address of the TrueNAS scale instance +# TRUENAS_API_KEY: The API token for TrueNAS +# POOL_NAME: The name of the ZFS pool (e.g., "tank") +# SOURCE_DATASET: The name of the production dataset (e.g., "apps/jellyfin") +# PR_NUMBER: The Pull Request number + +if [[ -z "${TRUENAS_IP:-}" || -z "${TRUENAS_API_KEY:-}" || -z "${POOL_NAME:-}" || -z "${SOURCE_DATASET:-}" || -z "${PR_NUMBER:-}" ]]; then + echo "Error: Missing required environment variables." + exit 1 +fi + +BASE_URL="http://${TRUENAS_IP}/api/v2.0" +HEADERS=( + "-H" "Authorization: Bearer ${TRUENAS_API_KEY}" + "-H" "Content-Type: application/json" +) + +DATASET_ID="${POOL_NAME}/${SOURCE_DATASET}" +SNAPSHOT_NAME="pr-${PR_NUMBER}-base" +CLONE_DATASET_ID="${POOL_NAME}/${SOURCE_DATASET}-pr-${PR_NUMBER}" + +# TrueNAS API requires the ID to be URL-encoded for dataset deletion +# URL encoding helper (replace / with %2F) +ENCODED_CLONE_ID="${CLONE_DATASET_ID//\//%2F}" +ENCODED_SNAPSHOT_ID="${DATASET_ID}@${SNAPSHOT_NAME}" +ENCODED_SNAPSHOT_ID="${ENCODED_SNAPSHOT_ID//\//%2F}" + +echo "1. Destroying staging clone ${CLONE_DATASET_ID}..." +curl -s -X DELETE "${BASE_URL}/zfs/dataset/id/${ENCODED_CLONE_ID}" "${HEADERS[@]}" > /dev/null + +echo "2. Destroying base snapshot ${DATASET_ID}@${SNAPSHOT_NAME}..." +curl -s -X DELETE "${BASE_URL}/zfs/snapshot/id/${ENCODED_SNAPSHOT_ID}" "${HEADERS[@]}" > /dev/null + +echo "Staging dataset and snapshot cleaned up successfully." diff --git a/terraform/staging-env/main.tf b/terraform/staging-env/main.tf new file mode 100644 index 0000000..03faf1a --- /dev/null +++ b/terraform/staging-env/main.tf @@ -0,0 +1,96 @@ +terraform { + required_providers { + proxmox = { + source = "bpg/proxmox" + version = "~> 0.61.0" + } + } +} + +variable "vm_id" { + description = "The ID of the VM to create (should be unique per PR)" + type = number +} + +variable "pr_number" { + description = "The Pull Request number for this staging environment" + type = string +} + +variable "staging_age_key" { + description = "The private age key for decrypting staging secrets. Injected via Cloud-Init." + type = string + sensitive = true +} + +provider "proxmox" { + # Relies on PROXMOX_VE_ENDPOINT and PROXMOX_VE_API_TOKEN environment variables +} + +resource "proxmox_virtual_environment_vm" "staging_vm" { + name = "staging-pr-${var.pr_number}" + description = "Ephemeral staging environment for PR #${var.pr_number}" + node_name = "pve" + vm_id = var.vm_id + + # Clone from the latest golden image template + clone { + vm_id = 9000 + full = true + } + + agent { + enabled = true + } + + cpu { + cores = 2 + } + + memory { + dedicated = 2048 + } + + network_device { + bridge = "vmbr0" + # Assign a specific VLAN tag for staging isolation if configured on your switch + # vlan_id = 50 + } + + # Cloud-Init configuration to inject the staging age key and set up networking + initialization { + ip_config { + ipv4 { + address = "dhcp" + } + } + + user_data_file_id = proxmox_virtual_environment_file.cloud_config.id + } +} + +resource "proxmox_virtual_environment_file" "cloud_config" { + content_type = "snippets" + datastore_id = "local-zfs" + node_name = "pve" + + source_raw { + data = <<-EOF + #cloud-config + write_files: + - path: /var/lib/sops-nix/key.txt + permissions: '0600' + content: | + ${indent(10, var.staging_age_key)} + runcmd: + - echo "Staging age key injected successfully." + EOF + + file_name = "staging-pr-${var.pr_number}-cloud-init.yaml" + } +} + +output "staging_vm_ip" { + value = proxmox_virtual_environment_vm.staging_vm.ipv4_addresses[1][0] # Adjust index based on actual returned interfaces + description = "The IP address of the newly spun up staging VM." +} From 3a8f6e6451e9d2f017bd40b8e2bad4e10c5d0fb2 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Fri, 17 Jul 2026 22:17:09 +0200 Subject: [PATCH 46/76] feat(apps): migrate DNS configuration (bind9, technitium-dns) --- modules/apps/bind9/db.depeuter.dev | 45 +++++++++++++++ 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 | 6 ++ modules/apps/technitium-dns/default.nix | 73 +++++++++++++++++++++++++ modules/default.nix | 1 + 8 files changed, 220 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 create mode 100644 modules/apps/default.nix create mode 100644 modules/apps/technitium-dns/default.nix diff --git a/modules/apps/bind9/db.depeuter.dev b/modules/apps/bind9/db.depeuter.dev new file mode 100644 index 0000000..72f3825 --- /dev/null +++ b/modules/apps/bind9/db.depeuter.dev @@ -0,0 +1,45 @@ +$TTL 604800 +@ IN SOA ns1 admin ( + 15 ; Serial + 604800 ; Refresh + 86400 ; Retry + 2419200 ; Expire + 604800 ) ; Negative Cache TTL + +; Name servers - NS records + IN NS ns1 +; IN NS ns2 + +ns1 IN A 192.168.0.91 +;ns2 IN A 192.158.0.X + +; Hostnames +hugo.kmtl IN A 192.168.0.11 + +ingress.kmtl IN A 192.168.0.10 +ingress.kmtl IN AAAA fe80::be24:11ff:fed6:842a + +; Core services +cloud IN A 192.168.0.10 +git IN A 78.23.37.117 +home IN A 192.168.0.10 +jelly IN CNAME ingress.kmtl +vault IN A 192.168.0.10 + +; Production VM +books IN A 192.168.0.31 +calibre IN A 192.168.0.31 + +; Production VM - Arr +bazarr IN A 192.168.0.33 +prowlarr IN A 192.168.0.33 +qb IN A 192.168.0.33 +radarr IN A 192.168.0.33 +sonarr IN A 192.168.0.33 + +; Development VM +plex IN A 192.168.0.91 + +; Catchalls +*.production IN A 192.168.0.31 +*.development IN A 192.168.0.91 diff --git a/modules/apps/bind9/default.nix b/modules/apps/bind9/default.nix new file mode 100644 index 0000000..a2346c1 --- /dev/null +++ b/modules/apps/bind9/default.nix @@ -0,0 +1,54 @@ +{ config, lib, ... }: + +let + cfg = config.homelab.apps.bind9; +in { + options.homelab.apps.bind9.enable = lib.mkEnableOption "ISC BIND 9 (Docker)"; + + config = lib.mkIf cfg.enable { + homelab.virtualisation.containers.enable = true; + + environment.etc = { + "bind/named.conf" = { + source = ./named.conf; + mode = "0555"; + }; + "bind/named.conf.options" = { + source = ./named.conf.options; + mode = "0555"; + }; + "bind/named.conf.local" = { + source = ./named.conf.local; + mode = "0555"; + }; + "bind/zones/db.depeuter.dev" = { + source = ./db.depeuter.dev; + mode = "0555"; + }; + }; + + virtualisation.oci-containers.containers.bind9 = { + hostname = "bind9"; + #image = "internetsystemsconsortium/bind9:9.20"; # Current stable + image = "ubuntu/bind9"; # Current stable + autoStart = true; + ports = [ + "53:53/udp" + "53:53/tcp" + "953:953/tcp" + ]; + extraOptions = [ + ]; + environment = { + }; + volumes = [ + "/etc/bind:/etc/bind" # For configuration, your `named.conf` lives here + "bind9-cache:/var/cache/bind" + #"...:/var/lib/bind" # Secondary zones + "bind9-logs:/var/log" # Logfiles + ]; + labels = { + }; + }; + }; +} diff --git a/modules/apps/bind9/named.conf b/modules/apps/bind9/named.conf new file mode 100644 index 0000000..d301bd7 --- /dev/null +++ b/modules/apps/bind9/named.conf @@ -0,0 +1,2 @@ +include "/etc/bind/named.conf.options"; +include "/etc/bind/named.conf.local"; diff --git a/modules/apps/bind9/named.conf.local b/modules/apps/bind9/named.conf.local new file mode 100644 index 0000000..442eca9 --- /dev/null +++ b/modules/apps/bind9/named.conf.local @@ -0,0 +1,4 @@ +zone "depeuter.dev" { + type primary; + file "/etc/bind/zones/db.depeuter.dev"; +}; diff --git a/modules/apps/bind9/named.conf.options b/modules/apps/bind9/named.conf.options new file mode 100644 index 0000000..b05f4bf --- /dev/null +++ b/modules/apps/bind9/named.conf.options @@ -0,0 +1,35 @@ +http local { + endpoints { "/dns-query"; }; +}; + +acl bogusnets { +}; + +acl trusted { + 192.168.0.0/16; +}; + +options { + directory "/var/cache/bind"; + + version "not currently available"; + + listen-on { any; }; + listen-on-v6 { any; }; + listen-on tls ephemeral { any; }; + listen-on-v6 tls ephemeral { any; }; + listen-on tls ephemeral http local { any; }; + listen-on-v6 tls ephemeral http local { any; }; + + recursion yes; + forwarders { + 9.9.9.9; + 149.112.112.112; + }; + forward only; + + allow-query { any; }; + allow-recursion { any; }; + allow-transfer { none; }; + blackhole { bogusnets; }; +}; diff --git a/modules/apps/default.nix b/modules/apps/default.nix new file mode 100644 index 0000000..8ee2e12 --- /dev/null +++ b/modules/apps/default.nix @@ -0,0 +1,6 @@ +{ + imports = [ + ./bind9 + ./technitium-dns + ]; +} 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/default.nix b/modules/default.nix index 6d76ee2..e944ebe 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -1,5 +1,6 @@ { imports = [ + ./apps ./common ./services ./virtualisation From 29604805579f3bff8bb2be68c13011dff8fb7aa5 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Fri, 17 Jul 2026 22:18:19 +0200 Subject: [PATCH 47/76] feat: migrate remaining applications, services, and user configurations --- modules/apps/arr/default.nix | 303 ++++++++ modules/apps/calibre/default.nix | 189 +++++ modules/apps/changedetection/default.nix | 28 + modules/apps/default.nix | 12 + modules/apps/freshrss/default.nix | 93 +++ modules/apps/gitea/default.nix | 673 ++++++++++++++++++ modules/apps/homepage/default.nix | 79 ++ modules/apps/jellyfin/default.nix | 169 +++++ modules/apps/plex/default.nix | 69 ++ modules/apps/solidtime/default.nix | 278 ++++++++ modules/apps/speedtest/default.nix | 27 + modules/apps/traefik/default.nix | 90 +++ modules/apps/vaultwarden/default.nix | 656 +++++++++++++++++ modules/default.nix | 1 + modules/fileSystems/default.nix | 5 + modules/fileSystems/media/default.nix | 5 + modules/fileSystems/media/video/default.nix | 42 ++ modules/services/actions/default.nix | 49 ++ modules/services/default.nix | 1 + modules/virtualisation/containers/default.nix | 23 + modules/virtualisation/default.nix | 1 + users/apps/default.nix | 18 + users/backup/default.nix | 21 + users/default.nix | 4 + users/deploy/default.nix | 49 ++ users/media/default.nix | 18 + 26 files changed, 2903 insertions(+) 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/freshrss/default.nix create mode 100644 modules/apps/gitea/default.nix create mode 100644 modules/apps/homepage/default.nix create mode 100644 modules/apps/jellyfin/default.nix create mode 100644 modules/apps/plex/default.nix create mode 100644 modules/apps/solidtime/default.nix create mode 100644 modules/apps/speedtest/default.nix create mode 100644 modules/apps/traefik/default.nix create mode 100644 modules/apps/vaultwarden/default.nix create mode 100644 modules/fileSystems/default.nix create mode 100644 modules/fileSystems/media/default.nix create mode 100644 modules/fileSystems/media/video/default.nix create mode 100644 modules/services/actions/default.nix create mode 100644 modules/virtualisation/containers/default.nix create mode 100644 users/apps/default.nix create mode 100644 users/backup/default.nix create mode 100644 users/deploy/default.nix create mode 100644 users/media/default.nix diff --git a/modules/apps/arr/default.nix b/modules/apps/arr/default.nix new file mode 100644 index 0000000..7b530c3 --- /dev/null +++ b/modules/apps/arr/default.nix @@ -0,0 +1,303 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.homelab.apps.arr; + + networkName = "arrStack"; + proxyNet = config.homelab.apps.traefik.sharedNetworkName; + + appNames = [ "bazarr" "prowlarr" "qbittorrent" "radarr" "sonarr" ]; + inUse = builtins.any (app: cfg.${app}.enable) appNames; + + PGID = toString config.users.groups.media.gid; + UMASK = "002"; +in { + options.homelab.apps.arr = let + mkAppOption = appName: { + enable = lib.mkEnableOption "${appName} using Docker"; + exposePorts = lib.mkOption { + type = lib.types.bool; + description = "Expose ${appName} port"; + default = cfg.exposePorts; + }; + }; + in { + enable = lib.mkEnableOption "Arr Stack using Docker"; + exposePorts = lib.mkOption { + type = lib.types.bool; + description = "Expose all app ports"; + # Only expose ports by default if Traefik is not in use. + default = ! config.homelab.apps.traefik.enable; + }; + + bazarr = mkAppOption "Bazarr"; + prowlarr = mkAppOption "Prowlarr"; + qbittorrent = mkAppOption "qBittorrent"; + radarr = mkAppOption "Radarr"; + sonarr = mkAppOption "Sonarr"; + }; + + 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; + prowlarr.enable = true; + qbittorrent.enable = true; + radarr.enable = true; + sonarr.enable = true; + }; + + fileSystems.media.video = { + enable = true; + permissions = [ "read" "write" ]; + }; + + virtualisation.containers.enable = lib.mkIf inUse true; + }; + + fileSystems = let + mkFileSystem = device: { + inherit device; + 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. + systemd.services."docker-${networkName}-create-network" = lib.mkIf inUse { + description = "Create Docker network for ${networkName}"; + requiredBy = [ + "docker-bazarr.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 = let + mkUser = uid: { + uid = lib.mkForce uid; + isSystemUser = true; + group = config.users.groups.media.name; + home = "/var/empty"; + shell = null; + }; + in { + bazarr = lib.mkIf cfg.bazarr.enable (mkUser 3003); + prowlarr = lib.mkIf cfg.prowlarr.enable (mkUser 3004); + qbittorrent = lib.mkIf cfg.qbittorrent.enable (mkUser 3005) // { + extraGroups = [ + config.users.groups.apps.name + ]; + }; + radarr = lib.mkIf cfg.radarr.enable (mkUser 3006); + sonarr = lib.mkIf cfg.sonarr.enable (mkUser 3007); + }; + + virtualisation.oci-containers.containers = let + videoHostPath = config.homelab.fileSystems.media.video.hostPath; + in { + bazarr = let + port = 6767; + in lib.mkIf cfg.bazarr.enable { + hostname = "bazarr"; + image = "ghcr.io/hotio/bazarr:release-1.5.2"; + autoStart = true; + ports = lib.mkIf cfg.bazarr.exposePorts [ + "${toString port}:${toString port}/tcp" + "${toString port}:${toString port}/udp" + ]; + extraOptions = [ + "--network=${networkName}" + "--network=${proxyNet}" + ]; + environment = { + PUID = toString config.users.users.bazarr.uid; + inherit PGID UMASK; + TZ = config.time.timeZone; + WEBUI_PORTS = "${toString port}/tcp,${toString port}/udp"; + }; + volumes = [ + "bazarr-config:/config" + + "/srv/bazarr-backup:/config/backup" + + "${videoHostPath}/Films:/media/movies" + "${videoHostPath}/Series:/media/series" + ]; + labels = { + "traefik.enable" = "true"; + "traefik.docker.network" = proxyNet; + "traefik.http.routers.bazarr.rule" = "Host(`bazarr.depeuter.dev`)"; + "traefik.http.services.bazarr.loadbalancer.server.port" = toString port; + }; + }; + + prowlarr = let + port = 9696; + in lib.mkIf cfg.prowlarr.enable { + hostname = "prowlarr"; + image = "ghcr.io/hotio/prowlarr:release-2.0.5.5160"; + autoStart = true; + ports = lib.mkIf cfg.prowlarr.exposePorts [ + "${toString port}:${toString port}/tcp" + ]; + extraOptions = [ + "--network=${networkName}" + "--network=${proxyNet}" + ]; + environment = { + PUID = toString config.users.users.prowlarr.uid; + inherit PGID UMASK; + TZ = config.time.timeZone; + }; + volumes = [ + "prowlarr-config:/config" + + "/srv/prowlarr-backup:/config/Backups" + ]; + labels = { + "traefik.enable" = "true"; + "traefik.docker.network" = proxyNet; + "traefik.http.routers.prowlarr.rule" = "Host(`prowlarr.depeuter.dev`)"; + "traefik.http.services.prowlarr.loadbalancer.server.port" = toString port; + }; + }; + + qbittorrent = let + port = 10095; + in lib.mkIf cfg.qbittorrent.enable { + hostname = "qbittorrent"; + image = "ghcr.io/hotio/qbittorrent:release-5.1.2"; + autoStart = true; + ports = lib.mkIf cfg.qbittorrent.exposePorts [ + "${toString port}:${toString port}/tcp" + "${toString port}:${toString port}/udp" + ]; + extraOptions = [ + "--network=${networkName}" + "--network=${proxyNet}" + ]; + environment = { + PUID = toString config.users.users.qbittorrent.uid; + inherit PGID UMASK; + TZ = config.time.timeZone; + WEBUI_PORTS = "${toString port}/tcp,${toString port}/udp"; + }; + volumes = [ + "/srv/qbittorrent:/config" + + "/srv/torrent:/media/cache" + ]; + labels = { + "traefik.enable" = "true"; + "traefik.docker.network" = proxyNet; + "traefik.http.routers.qbittorrent.rule" = "Host(`qb.depeuter.dev`)"; + "traefik.http.services.qbittorrent.loadbalancer.server.port" = toString port; + }; + }; + + radarr = let + port = 7878; + in lib.mkIf cfg.radarr.enable { + hostname = "radarr"; + image = "ghcr.io/hotio/radarr:testing-5.28.0.10205"; + autoStart = true; + ports = lib.mkIf cfg.radarr.exposePorts [ + "${toString port}:${toString port}/tcp" + ]; + extraOptions = [ + "--network=${networkName}" + "--network=${proxyNet}" + ]; + environment = { + PUID = toString config.users.users.radarr.uid; + inherit PGID UMASK; + TZ = config.time.timeZone; + }; + volumes = [ + "radarr-config:/config" + + "/srv/radarr-backup:/config/Backups" + + "/srv/torrent:/media/cache" + "${videoHostPath}/Films:/media/movies" + ]; + labels = { + "traefik.enable" = "true"; + "traefik.docker.network" = proxyNet; + "traefik.http.routers.radarr.rule" = "Host(`radarr.depeuter.dev`)"; + "traefik.http.services.radarr.loadbalancer.server.port" = toString port; + }; + }; + + sonarr = let + port = 8989; + in lib.mkIf cfg.sonarr.enable { + hostname = "sonarr"; + image = "ghcr.io/hotio/sonarr:release-4.0.15.2941"; + autoStart = true; + ports = lib.mkIf cfg.sonarr.exposePorts [ + "${toString port}:${toString port}/tcp" + ]; + extraOptions = [ + "--network=${networkName}" + "--network=${proxyNet}" + ]; + environment = { + PUID = toString config.users.users.sonarr.uid; + inherit PGID UMASK; + TZ = config.time.timeZone; + }; + volumes = [ + "sonarr-config:/config" + + "/srv/sonarr-backup:/config/Backups" + + "/srv/torrent:/media/cache" + "${videoHostPath}/Series:/media/series" + ]; + labels = { + "traefik.enable" = "true"; + "traefik.docker.network" = proxyNet; + "traefik.http.routers.sonarr.rule" = "Host(`sonarr.depeuter.dev`)"; + "traefik.http.services.sonarr.loadbalancer.server.port" = toString port; + }; + }; + }; + }; +} diff --git a/modules/apps/calibre/default.nix b/modules/apps/calibre/default.nix new file mode 100644 index 0000000..bddf5c8 --- /dev/null +++ b/modules/apps/calibre/default.nix @@ -0,0 +1,189 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.homelab.apps.calibre; + + PUID = toString config.users.users.calibre.uid; + PGID = toString config.users.groups.media.gid; + + books = "/srv/books"; + calibre-config = "/srv/calibre-config"; + calibre-web-config = "/srv/calibre-web-config"; + + networkName = "calibre"; + proxyNet = config.homelab.apps.traefik.sharedNetworkName; +in { + options.homelab.apps.calibre = { + enable = lib.mkEnableOption "Calibre (Desktop + Web)"; + desktop.enable = lib.mkEnableOption "Calibre Desktop (KasmVNC)"; + web.enable = lib.mkEnableOption "Calibre Web"; + }; + + config = lib.mkMerge [ + { + homelab.apps.calibre = lib.mkIf cfg.enable { + desktop.enable = true; + web.enable = true; + }; + } + + # Common + (lib.mkIf (cfg.desktop.enable || cfg.web.enable) { + homelab = { + users.media.enable = true; + virtualisation.containers.enable = true; + }; + + users.users.calibre = { + uid = lib.mkForce 3010; + isSystemUser = true; + group = config.users.groups.media.name; + home = "/var/empty"; + shell = null; + }; + + fileSystems."${books}" = { + device = "192.168.0.11:/mnt/SMALL/MEDIA/BOOKS"; + fsType = "nfs"; + options = [ + "rw" + "auto" + "nfsvers=4.2" + "rsize=1048576" "wsize=1048576" + "soft" + "timeo=600" "retrans=2" + "_netdev" "nosuid" "tcp" + ]; + }; + + # Make sure the Docker network exists. + systemd.services."docker-${networkName}-create-network" = { + requiredBy = [ + "docker-calibre.service" + ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = '' + if ! ${pkgs.docker}/bin/docker network ls | grep -q ${networkName}; then + ${pkgs.docker}/bin/docker network create ${networkName} + fi + ''; + }; + }) + + # Calibre desktop + (lib.mkIf cfg.desktop.enable { + fileSystems."${calibre-config}" = { + device = "192.168.0.11:/mnt/SMALL/CONFIG/CALIBRE"; + fsType = "nfs"; + options = [ + "rw" + "auto" + "nfsvers=4.2" + "rsize=1048576" "wsize=1048576" + "soft" + "timeo=600" "retrans=2" + "_netdev" "nosuid" "tcp" + ]; + }; + + virtualisation.oci-containers.containers.calibre = let + innerPort = 8080; + in { + hostname = "calibre"; + image = "lscr.io/linuxserver/calibre:v8.10.0-ls354"; + autoStart = true; + ports = [ + # Open ports if you don't use Traefik + "9480:${toString innerPort}" # Calibre desktop GUI + #"9481:8181" # Calibre desktop GUI HTTPS + #"9581:8081" # Calibre webserver gui + ]; + extraOptions = [ + "--network=${networkName}" + "--network=${proxyNet}" + + # syscalls are unkown to Docker + #"--security-opt" "seccomp=unconfined" + ]; + environment = { + inherit PUID PGID; + #UMASK = "022"; + + TZ = config.time.timeZone; + + #PASSWORD = ""; + #CLI_ARGS = ""; + }; + volumes = [ + "${calibre-config}:/config" + + "${books}:/media/books" + ]; + labels = { + "traefik.enable" = "true"; + "traefik.docker.network" = proxyNet; + "traefik.http.routers.calibre.rule" = "Host(`calibre.depeuter.dev`)"; + "traefik.http.services.calibre.loadbalancer.server.port" = toString innerPort; + }; + }; + }) + + # Calibre Web + (lib.mkIf cfg.web.enable { + fileSystems."${calibre-web-config}" = { + device = "192.168.0.11:/mnt/SMALL/CONFIG/CALIBRE-WEB"; + fsType = "nfs"; + options = [ + "rw" + "auto" + "nfsvers=4.2" + "rsize=1048576" "wsize=1048576" + "soft" + "timeo=600" "retrans=2" + "_netdev" "nosuid" "tcp" + ]; + }; + + virtualisation.oci-containers.containers.calibre-web = let + innerPort = 8083; + in { + hostname = "calibre-web"; + image = "lscr.io/linuxserver/calibre-web:0.6.25-ls346"; + autoStart = true; + ports = [ + # Open ports if you don't use Traefik + "8083:${toString innerPort}" # Web UI + ]; + extraOptions = [ + "--network=${networkName}" + "--network=${proxyNet}" + ]; + environment = { + inherit PUID PGID; + #UMASK = "022"; + + TZ = config.time.timeZone; + + # (x86-64 only) Adds the ability to perform ebook conversion + DOCKER_MODS = "linuxserver/mods:universal-calibre"; + # Allow Google Oauth + #OAUTHLIB_RELAX_TOKEN_SCOPE = "1"; + }; + volumes = [ + "${calibre-web-config}:/config" + + "${books}:/media/books" + ]; + labels = { + "traefik.enable" = "true"; + "traefik.docker.network" = proxyNet; + "traefik.http.routers.calibre-web.rule" = "Host(`books.depeuter.dev`)"; + "traefik.http.services.calibre-web.loadbalancer.server.port" = toString innerPort; + }; + }; + }) + ]; +} diff --git a/modules/apps/changedetection/default.nix b/modules/apps/changedetection/default.nix new file mode 100644 index 0000000..ee88751 --- /dev/null +++ b/modules/apps/changedetection/default.nix @@ -0,0 +1,28 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.homelab.apps.changedetection; +in { + options.homelab.apps.changedetection.enable = lib.mkEnableOption "Changedetection.io"; + + config = lib.mkIf cfg.enable { + homelab.virtualisation.containers.enable = true; + + virtualisation.oci-containers.containers.changedetection = { + hostname = "changedetection"; + image = "ghcr.io/dgtlmoon/changedetection.io"; + autoStart = true; + ports = [ + "5000:5000/tcp" + ]; + extraOptions = [ + ]; + volumes = [ + "changedetection:/datastore" + ]; + environment = { + LOGGER_LEVEL = "WARNING"; + }; + }; + }; +} diff --git a/modules/apps/default.nix b/modules/apps/default.nix index 8ee2e12..385f915 100644 --- a/modules/apps/default.nix +++ b/modules/apps/default.nix @@ -1,6 +1,18 @@ { imports = [ + ./arr ./bind9 + ./calibre + ./changedetection + ./freshrss + ./gitea + ./homepage + ./jellyfin + ./plex + ./solidtime + ./speedtest ./technitium-dns + ./traefik + ./vaultwarden ]; } diff --git a/modules/apps/freshrss/default.nix b/modules/apps/freshrss/default.nix new file mode 100644 index 0000000..4f4456f --- /dev/null +++ b/modules/apps/freshrss/default.nix @@ -0,0 +1,93 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.homelab.apps.freshrss; + + networkName = "freshrss"; +in { + options.homelab.apps.freshrss = { + enable = lib.mkEnableOption "FreshRSS"; + port = lib.mkOption { + type = lib.types.int; + default = 9080; + description = "FreshRSS WebUI port"; + }; + }; + + config = let + inherit (config.homelab.apps.freshrss) port; + in + lib.mkIf cfg.enable { + homelab.virtualisation.containers.enable = true; + + fileSystems."/srv/freshrss" = { + device = "192.168.0.11:/mnt/SMALL/CONFIG/FRESHRSS"; + fsType = "nfs"; + options = [ + "rw" + "auto" + "nfsvers=4.2" + "async" "soft" "timeo=600" + "retrans=2" + "_netdev" + "nosuid" + "tcp" + ]; + }; + + systemd.services."docker-${networkName}-create-network" = { + description = "Create Docker network for ${networkName}"; + requiredBy = [ + "docker-freshrss.service" + ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = '' + if ! ${pkgs.docker}/bin/docker network ls | grep -q ${networkName}; then + ${pkgs.docker}/bin/docker network create ${networkName} + fi + ''; + }; + + virtualisation.oci-containers.containers.freshrss = { + hostname = "freshrss"; + image = "freshrss/freshrss:1.25.0"; + autoStart = true; + user = "0:33"; + ports = [ + "${toString port}:80/tcp" + ]; + extraOptions = [ + "--network=${networkName}" + ]; + environment = { + TZ = config.time.timeZone; + CRON_MIN = "3,18,33,48"; # Alternatively, configure cron inside container. + SERVER_DNS = "rss.depeuter.dev"; + TRUSTED_PROXY = "172.16.0.1/12 192.168.0.1/16"; + }; + volumes = [ + "/srv/freshrss/www/freshrss/data:/var/www/FreshRSS/data" + "/srv/freshrss/www/freshrss/extensions:/var/www/FreshRSS/extensions" + ]; + labels = { + "traefik.enable" = "true"; + + "traefik.http.middlewares.freshrssM1.compress" = "true"; + "traefik.http.middlewares.freshrssM2.headers.browserXssFilter" = "true"; + "traefik.http.middlewares.freshrssM2.headers.forceSTSHeader" = "true"; + "traefik.http.middlewares.freshrssM2.headers.frameDeny" = "true"; + "traefik.http.middlewares.freshrssM2.headers.referrerPolicy" = "no-referrer-when-downgrade"; + "traefik.http.middlewares.freshrssM2.headers.stsSeconds" = "31536000"; + "traefik.http.routers.freshrss.entryPoints" = "websecure"; + "traefik.http.routers.freshrss.tls" = "true"; + + "traefik.http.services.freshrss.loadbalancer.server.port" = "80"; + "traefik.http.routers.freshrss.middlewares" = "freshrssM1,freshrssM2"; + "traefik.http.routers.freshrss.rule" = "Host(`rss.depeuter.dev`)"; + }; + }; + }; +} diff --git a/modules/apps/gitea/default.nix b/modules/apps/gitea/default.nix new file mode 100644 index 0000000..0361bd5 --- /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:11.0.1-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/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"; + }; + }; + }; +} diff --git a/modules/apps/jellyfin/default.nix b/modules/apps/jellyfin/default.nix new file mode 100644 index 0000000..011f56b --- /dev/null +++ b/modules/apps/jellyfin/default.nix @@ -0,0 +1,169 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.homelab.apps.jellyfin; + + networkName = "jellyfin"; + inherit (config.homelab.fileSystems) media; + + 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 = { + fileSystems.media.video = { + enable = true; + permissions = [ "read" ]; + }; + + 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/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.7"; + 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" + "${media.video.hostPath}:/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.19.0"; + 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..b307b86 --- /dev/null +++ b/modules/apps/plex/default.nix @@ -0,0 +1,69 @@ +{ config, lib, ... }: + +let + cfg = config.homelab.apps.plex; +in { + options.homelab.apps.plex.enable = lib.mkEnableOption "Plex"; + + config = lib.mkIf cfg.enable { + homelab = { + users = { + apps.enable = true; + media.enable = true; + }; + fileSystems.media.video.enable = true; + virtualisation.containers.enable = true; + }; + + users.users.plex = { + uid = lib.mkForce 3009; + isSystemUser = true; + group = config.users.groups.apps.name; + extraGroups = [ + config.users.groups.media.name + ]; + home = "/var/empty"; + shell = null; + }; + + virtualisation.oci-containers.containers.plex = let + videoHostPath = config.homelab.fileSystems.media.video.hostPath; + in { + hostname = "plex"; + image = "plexinc/pms-docker:1.41.6.9685-d301f511a"; + autoStart = true; + ports = [ + "32400:32400/tcp" # Plex Media Server + "1900:1900/udp" # Plex DLNA Server + "32469:32469/tcp" # Plex DLNA Server + "32410:32410/udp" # GDM network discovery + "32412:32412/udp" # GDM network discovery + "32413:32413/udp" # GDM network discovery + "32414:32414/udp" # GDM network discovery + # "8324:8324/tcp" # Controlling Plex for Roku via Plex Companion + ]; + environment = { + #ADVERTISE_AP = "..."; # TODO Configure ip + ALLOWED_NETWORKS = "192.168.0.0/24,172.16.0.0/16"; + CHANGE_CONFIG_DIR_OWNERSHIP = "false"; + HOSTNAME = "Hugo-Plex"; + PLEX_CLAIM = "claim-d5MqsjMeCZrUF6oUvssr"; + PLEX_UID = toString config.users.users.plex.uid; + PLEX_GID = toString config.users.groups.media.gid; + TZ = config.time.timeZone; + }; + volumes = [ + # TODO Backup over NFS + "plex-config:/config" + "plex-transcode:/transcode" + + "${videoHostPath}:/data/video:ro" + ]; + labels = { + "traefik.enable" = "true"; + "traefik.http.routers.plex.rule" = "Host(`plex.depeuter.dev`)"; + "traefik.http.services.plex.loadbalancer.server.port" = "32400"; + }; + }; + }; +} diff --git a/modules/apps/solidtime/default.nix b/modules/apps/solidtime/default.nix new file mode 100644 index 0000000..725d32d --- /dev/null +++ b/modules/apps/solidtime/default.nix @@ -0,0 +1,278 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.homelab.apps.solidtime; + + networkName = "solidtime"; + internalNetworkName = "solidtime-internal"; + proxyNet = config.homelab.apps.traefiik.sharedNetworkName; + + user = "1000:1000"; + + # dbExternalPort = ...; + dbInternalPort = 5432; + + gotenbergPort = 3000; + + 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:817d3a366ecc39f0473d7154372afa82dd4e6e50c66d70be45804892c8421cbb"; + sha256 = "sha256-h5aCKaquUF/EVsOHaLOHrn1HAoXZYPhAbJ+e4cmjSA8="; + }; + + 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 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"; + + # 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"; + 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; + + # 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 = solidtimeImage; + imageFile = solidtimeImageFile; + inherit user; + autoStart = true; + dependsOn = [ + "solidtimeDb" + ]; + ports = [ + # Open ports if you don't use Traefik + "${toString cfg.port}:8000" + ]; + networks = [ + networkName + internalNetworkName + ]; + extraOptions = [ + # Healthecks + # test: [ "CMD", "curl", "--fail", "http://localhost:8000/health-check/up" ] + ''--health-cmd=curl --fail http://localhost:8000/health-check/up'' + ]; + inherit volumes; + 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"; + }; + }; + solidtimeScheduler = { + hostname = "scheduler"; + image = solidtimeImage; + imageFile = solidtimeImageFile; + inherit user; + autoStart = true; + dependsOn = [ + "solidtimeDb" + ]; + networks = [ + internalNetworkName + ]; + extraOptions = [ + # Healthchecks + # test: [ "CMD", "healthcheck" ] + ''--health-cmd="healthcheck"'' + ]; + inherit volumes; + environmentFiles = [ + "/home/admin/.solidtime.env" + ]; + environment = laravelEnv // { + CONTAINER_MODE = "scheduler"; + }; + }; + solidtimeQueue = { + hostname = "queue"; + image = solidtimeImage; + imageFile = solidtimeImageFile; + inherit user; + autoStart = true; + networks = [ + internalNetworkName + ]; + extraOptions = [ + # Healthchecks + # test: [ "CMD", "healthcheck" ] + ''--health-cmd="healthcheck"'' + ]; + inherit volumes; + dependsOn = [ + "solidtimeDb" + ]; + environmentFiles = [ + "/home/admin/.solidtime.env" + ]; + environment = laravelEnv // { + CONTAINER_MODE = "worker"; + WORKER_COMMAND = "php /var/www/html/artisan queue:work"; + }; + }; + solidtimeDb = let + imageName = "postgres"; + finalImageTag = "15"; + in { + hostname = "database"; + 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}" + ]; + networks = [ + internalNetworkName + ]; + extraOptions = [ + # 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 = let + imageName = "gotenberg/gotenberg"; + finalImageTag = "8.26.0"; + in { + hostname = "gotenberg"; + image = "${imageName}:${finalImageTag}"; + imageFile = pkgs.dockerTools.pullImage { + inherit imageName finalImageTag; + imageDigest = "sha256:328551506b3dec3ff6381dd47e5cd72a44def97506908269e201a8fbfa1c12c0"; + sha256 = "sha256-1zz4xDAgXxHUnkCVIfjHTgXb82EFEx+5am6Cu9+eZj4="; + }; + autoStart = true; + networks = [ + internalNetworkName + ]; + extraOptions = [ + # Healthchecks + # test: [ "CMD", "curl", "--silent", "--fail", "http://localhost:3000/health" ] + ''--health-cmd="curl --silent --fail http://localhost:${toString gotenbergPort}/health"'' + ]; + }; + }; + }; +} + 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/traefik/default.nix b/modules/apps/traefik/default.nix new file mode 100644 index 0000000..7f6ce38 --- /dev/null +++ b/modules/apps/traefik/default.nix @@ -0,0 +1,90 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.homelab.apps.traefik; + + port = 8080; +in { + options.homelab.apps.traefik = { + enable = lib.mkEnableOption "Traefik Reverse Proxy"; + sharedNetworkName = lib.mkOption { + type = lib.types.str; + default = "traefik"; + description = "The name of the shared network to connect the container to."; + }; + }; + + config = lib.mkIf cfg.enable { + homelab.virtualisation.containers.enable = true; + + # Make sure the Docker network exists. + systemd.services."docker-${cfg.sharedNetworkName}-create-network" = { + description = "Create Docker network for ${cfg.sharedNetworkName}"; + requiredBy = [ + "docker-traefik.service" + ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = '' + if ! ${pkgs.docker}/bin/docker network ls | grep -q ${cfg.sharedNetworkName}; then + ${pkgs.docker}/bin/docker network create ${cfg.sharedNetworkName} + fi + ''; + }; + + virtualisation.oci-containers.containers.traefik = { + hostname = "traefik"; + image = "traefik:v3.4.3"; + autoStart = true; + ports = [ + "80:80/tcp" + "443:443/tcp" + "${toString port}:${toString port}/tcp" # Web UI (enabled by --api.insecure=true) + ]; + extraOptions = [ + "--network=${cfg.sharedNetworkName}" + ]; + environmentFiles = [ + /home/admin/.cloudflare.secret + ]; + cmd = [ + "--api.insecure=true" + + # Add Docker provider + "--providers.docker=true" + "--providers.docker.exposedByDefault=false" + + # Add web entrypoint + "--entrypoints.web.address=:80/tcp" + "--entrypoints.web.http.redirections.entrypoint.to=websecure" + "--entrypoints.web.http.redirections.entrypoint.scheme=https" + + # Add websecure entrypoint + "--entrypoints.websecure.address=:443/tcp" + "--entrypoints.websecure.http.tls=true" + "--entrypoints.websecure.http.tls.certResolver=letsencrypt" + "--entrypoints.websecure.http.tls.domains[0].main=depeuter.dev" + "--entrypoints.websecure.http.tls.domains[0].sans=*.depeuter.dev" + "--entrypoints.websecure.http.tls.domains[1].sans=*.${config.networking.hostName}.depeuter.dev" + + # Certificates + "--certificatesresolvers.letsencrypt.acme.dnschallenge=true" + "--certificatesresolvers.letsencrypt.acme.dnschallenge.provider=cloudflare" + "--certificatesresolvers.letsencrypt.acme.email=tibo.depeuter@telenet.be" + "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json" + ]; + volumes = [ + "letsencryp:/letsencrypt" + + "/var/run/docker.sock:/var/run/docker.sock:ro" + ]; + labels = { + "traefik.enable" = "true"; + "traefik.http.routers.traefik.rule" = "Host(`traefik.${config.networking.hostName}.depeuter.dev`)"; + "traefik.http.services.traefik.loadbalancer.server.port" = toString port; + }; + }; + }; +} diff --git a/modules/apps/vaultwarden/default.nix b/modules/apps/vaultwarden/default.nix new file mode 100644 index 0000000..4510299 --- /dev/null +++ b/modules/apps/vaultwarden/default.nix @@ -0,0 +1,656 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.homelab.apps.vaultwarden; + + networkName = "vaultwarden"; +in { + 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 = { + # 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 = let + dbHostname = "vaultwarden-db"; + dbPort = 5432; + in { + vaultwardenDb = { + hostname = dbHostname; + image = "postgres:15.8-alpine"; + autoStart = true; + ports = [ + "${toString dbPort}: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.34.3-alpine"; + autoStart = true; + ports = [ + "${toString cfg.port}:80/tcp" + ]; + extraOptions = [ + "--network=${networkName}" + ]; + dependsOn = [ + "vaultwardenDb" + ]; + 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@${dbHostname}:${toString dbPort}/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 = cfg.domain; + ## 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 = 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) + # 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 = cfg.name; + # 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/default.nix b/modules/default.nix index e944ebe..08bcacd 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -2,6 +2,7 @@ imports = [ ./apps ./common + ./fileSystems ./services ./virtualisation ]; diff --git a/modules/fileSystems/default.nix b/modules/fileSystems/default.nix new file mode 100644 index 0000000..7c25689 --- /dev/null +++ b/modules/fileSystems/default.nix @@ -0,0 +1,5 @@ +{ + imports = [ + ./media + ]; +} diff --git a/modules/fileSystems/media/default.nix b/modules/fileSystems/media/default.nix new file mode 100644 index 0000000..41cb81f --- /dev/null +++ b/modules/fileSystems/media/default.nix @@ -0,0 +1,5 @@ +{ + imports = [ + ./video + ]; +} diff --git a/modules/fileSystems/media/video/default.nix b/modules/fileSystems/media/video/default.nix new file mode 100644 index 0000000..e46193c --- /dev/null +++ b/modules/fileSystems/media/video/default.nix @@ -0,0 +1,42 @@ +{ config, lib, ... }: + +let + cfg = config.homelab.fileSystems.media.video; + + remotePath = "/mnt/SMALL/MEDIA/VIDEO"; + + maxPermissions = permissions: + if builtins.elem "write" permissions then "rw" + else "ro"; + permissionsOption = maxPermissions cfg.permissions; +in { + options.homelab.fileSystems.media.video = { + enable = lib.mkEnableOption "MEDIA/VIDEO dataset"; + hostPath = lib.mkOption { + type = lib.types.path; + default = "/srv/video"; + description = "Mountpath on host"; + }; + permissions = lib.mkOption { + type = lib.types.listOf (lib.types.enum [ "read" "write" ]); + default = [ "read" ]; + description = "Mount options permissions"; + }; + }; + + config = lib.mkIf cfg.enable { + fileSystems."${cfg.hostPath}" = { + device = "192.168.0.11:${remotePath}"; + fsType = "nfs"; + options = [ + permissionsOption + "auto" + "nfsvers=4.2" + "async" "soft" + "rsize=1048576" "wsize=1048576" + "timeo=600" "retry=50" "retrans=2" "actimeo=1800" "lookupcache=all" + "_netdev" "nosuid" "tcp" + ]; + }; + }; +} diff --git a/modules/services/actions/default.nix b/modules/services/actions/default.nix new file mode 100644 index 0000000..ea6b025 --- /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 index f7295a5..f70bc54 100644 --- a/modules/services/default.nix +++ b/modules/services/default.nix @@ -1,5 +1,6 @@ { imports = [ + ./actions ./openssh ]; } 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 index 50418a4..2290be2 100644 --- a/modules/virtualisation/default.nix +++ b/modules/virtualisation/default.nix @@ -1,5 +1,6 @@ { imports = [ + ./containers ./guest ]; } 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..acae033 --- /dev/null +++ b/users/backup/default.nix @@ -0,0 +1,21 @@ +{ 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 = [ + # Hugo + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICms6vjhE9kOlqV5GBPGInwUHAfCSVHLI2Gtzee0VXPh" + ]; + }; + }; +} diff --git a/users/default.nix b/users/default.nix index 26b5ff9..fe82019 100644 --- a/users/default.nix +++ b/users/default.nix @@ -1,5 +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 7b59f88c489b610d03d634b062db3fe3f2abf32c Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Fri, 17 Jul 2026 22:22:39 +0200 Subject: [PATCH 48/76] ci: add flake validation job before staging VM provisioning --- .forgejo/workflows/staging.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.forgejo/workflows/staging.yml b/.forgejo/workflows/staging.yml index 95c5e8b..691866f 100644 --- a/.forgejo/workflows/staging.yml +++ b/.forgejo/workflows/staging.yml @@ -5,8 +5,24 @@ on: types: [opened, synchronize, closed] jobs: + validate-flake: + runs-on: self-hosted + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Install Nix + uses: cachix/install-nix-action@v27 + with: + extra_nix_config: | + experimental-features = nix-command flakes + + - name: Validate Flake + run: nix flake check + manage-staging: runs-on: self-hosted + needs: validate-flake steps: - name: Checkout Repository uses: actions/checkout@v4 From 29de646d06d417cc0be3adb9bf701cd7842ba3c6 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Fri, 17 Jul 2026 22:30:56 +0200 Subject: [PATCH 49/76] feat(staging): implement micro-segmentation with proxmox firewall rules --- terraform/staging-env/main.tf | 47 ++++++++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/terraform/staging-env/main.tf b/terraform/staging-env/main.tf index 03faf1a..08ff226 100644 --- a/terraform/staging-env/main.tf +++ b/terraform/staging-env/main.tf @@ -52,12 +52,12 @@ resource "proxmox_virtual_environment_vm" "staging_vm" { } network_device { - bridge = "vmbr0" - # Assign a specific VLAN tag for staging isolation if configured on your switch - # vlan_id = 50 + bridge = "vmbr0" + firewall = true } # Cloud-Init configuration to inject the staging age key and set up networking + initialization { ip_config { ipv4 { @@ -90,6 +90,47 @@ resource "proxmox_virtual_environment_file" "cloud_config" { } } +resource "proxmox_virtual_environment_firewall_options" "staging_vm_fw_options" { + vm_id = proxmox_virtual_environment_vm.staging_vm.vm_id + node_name = proxmox_virtual_environment_vm.staging_vm.node_name + enable = true + policy_in = "ACCEPT" + policy_out = "DROP" +} + +resource "proxmox_virtual_environment_firewall_rules" "staging_vm_rules" { + vm_id = proxmox_virtual_environment_vm.staging_vm.vm_id + node_name = proxmox_virtual_environment_vm.staging_vm.node_name + + rule { + action = "ACCEPT" + type = "out" + dest = "192.168.0.11" + comment = "Allow traffic to TrueNAS" + } + + rule { + action = "ACCEPT" + type = "out" + dest = "192.168.0.1" + comment = "Allow traffic to Gateway/DNS" + } + + rule { + action = "DROP" + type = "out" + dest = "192.168.0.0/24" + comment = "Drop traffic to local homelab" + } + + rule { + action = "ACCEPT" + type = "out" + dest = "0.0.0.0/0" + comment = "Allow outbound internet traffic" + } +} + output "staging_vm_ip" { value = proxmox_virtual_environment_vm.staging_vm.ipv4_addresses[1][0] # Adjust index based on actual returned interfaces description = "The IP address of the newly spun up staging VM." From fb3758dbbb4577a67863df0136a4394c89440426 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Fri, 17 Jul 2026 22:34:06 +0200 Subject: [PATCH 50/76] feat: add pre-commit-hooks devShell and checks --- flake.nix | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index 5589e5a..031df52 100644 --- a/flake.nix +++ b/flake.nix @@ -20,11 +20,16 @@ url = "github:nlewo/comin"; inputs.nixpkgs.follows = "nixpkgs"; }; + + pre-commit-hooks = { + url = "github:cachix/pre-commit-hooks.nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; }; outputs = inputs@{ self, nixpkgs, - flake-utils, sops-nix, utils, comin, + flake-utils, sops-nix, utils, comin, pre-commit-hooks, ... }: let @@ -33,6 +38,27 @@ utils.lib.mkFlake { inherit self inputs; + outputsBuilder = channels: let + pkgs = channels.nixpkgs; + system = pkgs.system; + pre-commit-check = pre-commit-hooks.lib.${system}.run { + src = ./.; + hooks = { + nixfmt-rfc-style.enable = true; + statix.enable = true; + terraform-format.enable = true; + }; + }; + in { + checks = { + inherit pre-commit-check; + }; + devShells.default = pkgs.mkShell { + inherit (pre-commit-check) shellHook; + buildInputs = pre-commit-check.enabledPackages; + }; + }; + hostDefaults = { inherit system; From 53a539dd918c9253651ce436072d1c8bdbbcad99 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Fri, 17 Jul 2026 22:51:04 +0200 Subject: [PATCH 51/76] feat(observability): implement plg stack and ntfy alerting --- modules/apps/default.nix | 1 + modules/apps/monitoring/default.nix | 158 ++++++++++++++++++++++++++++ modules/common/default.nix | 4 + modules/common/monitoring.nix | 47 +++++++++ 4 files changed, 210 insertions(+) create mode 100644 modules/apps/monitoring/default.nix create mode 100644 modules/common/monitoring.nix diff --git a/modules/apps/default.nix b/modules/apps/default.nix index 385f915..88f35d5 100644 --- a/modules/apps/default.nix +++ b/modules/apps/default.nix @@ -8,6 +8,7 @@ ./gitea ./homepage ./jellyfin + ./monitoring ./plex ./solidtime ./speedtest diff --git a/modules/apps/monitoring/default.nix b/modules/apps/monitoring/default.nix new file mode 100644 index 0000000..035ddec --- /dev/null +++ b/modules/apps/monitoring/default.nix @@ -0,0 +1,158 @@ +{ config, lib, pkgs, ... }: + +{ + sops.secrets."grafana/admin_password" = { + sopsFile = ../../../secrets/prod/monitoring.yaml; + }; + sops.secrets."alertmanager/smtp_password" = { + sopsFile = ../../../secrets/prod/monitoring.yaml; + }; + + # 1. Loki Log Storage + services.loki = { + enable = true; + configuration = { + auth_enabled = false; + server.http_listen_port = 3100; + common.ring.instance_addr = "127.0.0.1"; + common.ring.kvstore.store = "inmemory"; + schema_config = { + configs = [{ + from = "2020-10-24"; + store = "boltdb-shipper"; + object_store = "filesystem"; + schema = "v11"; + index = { + prefix = "index_"; + period = "24h"; + }; + }]; + }; + storage_config = { + boltdb_shipper = { + active_index_directory = "/var/lib/loki/boltdb-shipper-active"; + cache_location = "/var/lib/loki/boltdb-shipper-cache"; + }; + filesystem.directory = "/var/lib/loki/chunks"; + }; + }; + }; + + # 2. Prometheus Time-Series DB + services.prometheus = { + enable = true; + port = 9090; + scrapeConfigs = [ + { + job_name = "node"; + scrape_interval = "15s"; + # In a real setup, we would use Prometheus service discovery (e.g., file_sd_configs) + # or list all homelab IPs here. For now, we scrape localhost. + static_configs = [{ + targets = [ "127.0.0.1:9100" ]; + }]; + } + ]; + # Connect Prometheus to Alertmanager + alertmanagers = [{ + static_configs = [{ + targets = [ "127.0.0.1:9093" ]; + }]; + }]; + }; + + # 3. Alertmanager (Routing alerts to NTFY and Email) + services.prometheus.alertmanager = { + enable = true; + port = 9093; + configuration = { + global = { + smtp_smarthost = "smtp.example.com:587"; + smtp_from = "alerts@depeuter.dev"; + smtp_auth_username = "alerts@depeuter.dev"; + smtp_auth_password_file = config.sops.secrets."alertmanager/smtp_password".path; + }; + route = { + receiver = "ntfy-and-email"; + group_wait = "30s"; + group_interval = "5m"; + repeat_interval = "4h"; + group_by = [ "alertname" "instance" ]; + }; + receivers = [{ + name = "ntfy-and-email"; + email_configs = [{ + to = "your-email@example.com"; + # Use smarthost settings defined in global + }]; + webhook_configs = [{ + # Alertmanager natively supports webhooks. We send the JSON to NTFY's Prometheus endpoint. + url = "http://127.0.0.1:2586/alerts"; + }]; + }]; + }; + }; + + # 4. NTFY Push Notification Server + services.ntfy-sh = { + enable = true; + settings = { + base-url = "https://ntfy.lab.depeuter.dev"; + listen-http = ":2586"; + # You can configure auth via the CLI once the service is running, or via auth-file. + # For now, it is open locally. + }; + }; + + # 5. Grafana Visualization + services.grafana = { + enable = true; + settings.server = { + http_port = 3000; + http_addr = "127.0.0.1"; + domain = "grafana.lab.depeuter.dev"; + }; + settings.security.admin_password = "$__file{${config.sops.secrets."grafana/admin_password".path}}"; + + # Declarative Data Sources + provision = { + enable = true; + datasources.settings.datasources = [ + { + name = "Prometheus"; + type = "prometheus"; + access = "proxy"; + url = "http://127.0.0.1:9090"; + isDefault = true; + } + { + name = "Loki"; + type = "loki"; + access = "proxy"; + url = "http://127.0.0.1:3100"; + } + ]; + }; + }; + + # 6. Traefik Reverse Proxy for Grafana and NTFY + services.traefik.dynamicConfigOptions.http = { + routers = { + grafana = { + rule = "Host(`grafana.lab.depeuter.dev`)"; + service = "grafana"; + }; + ntfy = { + rule = "Host(`ntfy.lab.depeuter.dev`)"; + service = "ntfy"; + }; + }; + services = { + grafana.loadBalancer.servers = [{ url = "http://127.0.0.1:3000"; }]; + ntfy.loadBalancer.servers = [{ url = "http://127.0.0.1:2586"; }]; + }; + }; + + # Open firewall for Loki so agents can push logs + networking.firewall.allowedTCPPorts = [ 3100 ]; +} diff --git a/modules/common/default.nix b/modules/common/default.nix index 44309f5..be4b3d3 100644 --- a/modules/common/default.nix +++ b/modules/common/default.nix @@ -1,4 +1,8 @@ { + imports = [ + ./monitoring.nix + ]; + config = { homelab = { services.openssh.enable = true; diff --git a/modules/common/monitoring.nix b/modules/common/monitoring.nix new file mode 100644 index 0000000..6aeac60 --- /dev/null +++ b/modules/common/monitoring.nix @@ -0,0 +1,47 @@ +{ config, lib, pkgs, ... }: + +{ + # Prometheus Node Exporter for hardware metrics + services.prometheus.exporters = { + node = { + enable = true; + enabledCollectors = [ "systemd" ]; + port = 9100; + }; + }; + + # Promtail to ship logs to Loki + services.promtail = { + enable = true; + configuration = { + server = { + http_listen_port = 28183; + grpc_listen_port = 0; + }; + positions = { + filename = "/tmp/positions.yaml"; + }; + clients = [{ + # Use the internal DNS name for the Loki ingress + url = "http://loki.lab.depeuter.dev/loki/api/v1/push"; + }]; + scrape_configs = [{ + job_name = "journal"; + journal = { + max_age = "12h"; + labels = { + job = "systemd-journal"; + host = config.networking.hostName; + }; + }; + relabel_configs = [{ + source_labels = [ "__journal__systemd_unit" ]; + target_label = "unit"; + }]; + }]; + }; + }; + + # Open firewall ports for node-exporter so Prometheus can scrape it + networking.firewall.allowedTCPPorts = [ 9100 ]; +} From 8fbecaa8643d9087d3f9561c02f9afb6efa921be Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Fri, 17 Jul 2026 23:01:44 +0200 Subject: [PATCH 52/76] refactor(security): move staging age key from forgejo secrets to proxmox snippet --- .forgejo/workflows/staging.yml | 2 -- BOOTSTRAP.md | 21 ++++++++++++++++++++- terraform/staging-env/main.tf | 29 ++--------------------------- 3 files changed, 22 insertions(+), 30 deletions(-) diff --git a/.forgejo/workflows/staging.yml b/.forgejo/workflows/staging.yml index 691866f..9ab3470 100644 --- a/.forgejo/workflows/staging.yml +++ b/.forgejo/workflows/staging.yml @@ -38,7 +38,6 @@ jobs: TF_VAR_pr_number: ${{ github.event.pull_request.number }} # VM_ID could be dynamically generated or based on PR number (e.g., 8000 + PR_NUMBER) TF_VAR_vm_id: ${{ format('8{0:03}', github.event.pull_request.number) }} - TF_VAR_staging_age_key: ${{ secrets.STAGING_AGE_KEY }} # TrueNAS integration (Secrets would need to be added to Forgejo) TRUENAS_IP: "truenas.local" @@ -61,7 +60,6 @@ jobs: PROXMOX_VE_API_TOKEN: ${{ secrets.PROXMOX_TOKEN_SECRET }} TF_VAR_pr_number: ${{ github.event.pull_request.number }} TF_VAR_vm_id: ${{ format('8{0:03}', github.event.pull_request.number) }} - TF_VAR_staging_age_key: "" # TrueNAS integration TRUENAS_IP: "truenas.local" diff --git a/BOOTSTRAP.md b/BOOTSTRAP.md index 836bafc..2cd99aa 100644 --- a/BOOTSTRAP.md +++ b/BOOTSTRAP.md @@ -45,8 +45,27 @@ The CI/CD actions require access to the Proxmox token and the staging secret key 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. +## 4. Staging Golden Key Provisioning (Proxmox Snippet) + +Instead of relying on Forgejo CI/CD to store the staging private key, we use a secure hypervisor-level Cloud-Init snippet. + +1. SSH into your Proxmox server (`pve`). +2. Create the Cloud-Init snippet file: + ```bash + cat << 'EOF' > /var/lib/vz/snippets/staging-key.yaml + #cloud-config + write_files: + - path: /var/lib/sops-nix/key.txt + permissions: '0600' + content: | + AGE-SECRET-KEY-1... (paste your staging-master private key here) + runcmd: + - echo "Staging age key injected successfully." + EOF + ``` +3. This completely removes the secret from Forgejo. When Terraform spins up a staging VM, it simply tells Proxmox to attach this local snippet! + ## 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/terraform/staging-env/main.tf b/terraform/staging-env/main.tf index 08ff226..b77ee4a 100644 --- a/terraform/staging-env/main.tf +++ b/terraform/staging-env/main.tf @@ -17,12 +17,6 @@ variable "pr_number" { type = string } -variable "staging_age_key" { - description = "The private age key for decrypting staging secrets. Injected via Cloud-Init." - type = string - sensitive = true -} - provider "proxmox" { # Relies on PROXMOX_VE_ENDPOINT and PROXMOX_VE_API_TOKEN environment variables } @@ -58,37 +52,18 @@ resource "proxmox_virtual_environment_vm" "staging_vm" { # Cloud-Init configuration to inject the staging age key and set up networking - initialization { + initialization { ip_config { ipv4 { address = "dhcp" } } - user_data_file_id = proxmox_virtual_environment_file.cloud_config.id + user_data_file_id = "local:snippets/staging-key.yaml" } } -resource "proxmox_virtual_environment_file" "cloud_config" { - content_type = "snippets" - datastore_id = "local-zfs" - node_name = "pve" - source_raw { - data = <<-EOF - #cloud-config - write_files: - - path: /var/lib/sops-nix/key.txt - permissions: '0600' - content: | - ${indent(10, var.staging_age_key)} - runcmd: - - echo "Staging age key injected successfully." - EOF - - file_name = "staging-pr-${var.pr_number}-cloud-init.yaml" - } -} resource "proxmox_virtual_environment_firewall_options" "staging_vm_fw_options" { vm_id = proxmox_virtual_environment_vm.staging_vm.vm_id From 67c7cb6ec65d1bd552df1e49b419ddb361eea1c9 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Fri, 17 Jul 2026 23:08:19 +0200 Subject: [PATCH 53/76] feat(security): add truenas rbac automation script --- BOOTSTRAP.md | 16 ++++++- scripts/truenas-rbac-setup.sh | 88 +++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 1 deletion(-) create mode 100755 scripts/truenas-rbac-setup.sh diff --git a/BOOTSTRAP.md b/BOOTSTRAP.md index 2cd99aa..e9df6c2 100644 --- a/BOOTSTRAP.md +++ b/BOOTSTRAP.md @@ -67,5 +67,19 @@ Instead of relying on Forgejo CI/CD to store the staging private key, we use a s ``` 3. This completely removes the secret from Forgejo. When Terraform spins up a staging VM, it simply tells Proxmox to attach this local snippet! +## 5. TrueNAS API Security (RBAC) + +To prevent the CI/CD pipeline from having `root` access to your TrueNAS server, you must run the RBAC bootstrap script to create a restricted user (`forgejo-ci`) that can *only* clone datasets for staging, not destroy production data. + +1. Ensure you have network access to your TrueNAS host. +2. Execute the RBAC setup script: + ```bash + ./scripts/truenas-rbac-setup.sh + ``` +3. Provide your TrueNAS IP and the `root` Admin API Token when prompted. +4. The script will automatically create the custom `ci-runner-role` and the `forgejo-ci` user. +5. Follow the terminal output instructions to log into the TrueNAS Web UI as the new user and generate the restricted API token. +6. Use this restricted token for the `TRUENAS_API_KEY` secret in Forgejo. + ## 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. +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 and securely. diff --git a/scripts/truenas-rbac-setup.sh b/scripts/truenas-rbac-setup.sh new file mode 100755 index 0000000..bca71bf --- /dev/null +++ b/scripts/truenas-rbac-setup.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash +set -euo pipefail + +# This script automates the creation of a restricted TrueNAS user (forgejo-ci) +# and assigns it a custom privilege role strictly limited to ZFS cloning/snapshots. + +echo "==========================================" +echo " TrueNAS RBAC Setup for CI/CD" +echo "==========================================" +echo "This script will create a custom Privilege Role and a Restricted User." +echo "" + +read -p "Enter your TrueNAS IP (e.g., 192.168.0.11): " TRUENAS_IP +read -s -p "Enter your current TrueNAS Admin Token (root): " ADMIN_TOKEN +echo "" +read -p "Enter a password for the new 'forgejo-ci' user: " CI_PASSWORD + +BASE_URL="http://${TRUENAS_IP}/api/v2.0" +HEADERS=( + "-H" "Authorization: Bearer ${ADMIN_TOKEN}" + "-H" "Content-Type: application/json" +) + +echo "" +echo "1. Creating Custom Privilege (ci-runner-role)..." +# In TrueNAS SCALE, we create a privilege that allows specific methods +PRIV_PAYLOAD=$(cat < Privilege created successfully." +elif [ "$HTTP_STATUS" -eq 409 ] || [ "$HTTP_STATUS" -eq 422 ]; then + echo " -> Privilege already exists or validation failed (code ${HTTP_STATUS}). Skipping." +else + echo " -> Warning: Privilege creation returned HTTP ${HTTP_STATUS}. (Your TrueNAS version might handle RBAC differently)." +fi + +echo "2. Creating Restricted User (forgejo-ci)..." +USER_PAYLOAD=$(cat < User created successfully." +elif [ "$HTTP_STATUS" -eq 409 ] || [ "$HTTP_STATUS" -eq 422 ]; then + echo " -> User already exists. Skipping." +else + echo " -> Warning: User creation returned HTTP ${HTTP_STATUS}." + cat /tmp/truenas_user.json +fi + +echo "" +echo "==========================================" +echo " Setup Complete (or mostly complete)!" +echo "==========================================" +echo "Because TrueNAS prevents root from generating API tokens for other users," +echo "you must complete the final step manually:" +echo "" +echo "1. Log into the TrueNAS Web UI at http://${TRUENAS_IP}" +echo "2. If the script failed to attach the privilege automatically, go to Credentials > Local Users," +echo " edit 'forgejo-ci', and assign it the ZFS roles." +echo "3. Log in as 'forgejo-ci' (or use the API Keys menu as Admin to generate a key for that user)." +echo "4. Copy the newly generated token." +echo "5. Update the TRUENAS_API_KEY secret in your Forgejo repository." +echo "==========================================" From d4d655e7350e580dfea1d5b537895d539c4bd037 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Fri, 17 Jul 2026 23:14:45 +0200 Subject: [PATCH 54/76] refactor: abstract docker network creation via custom options --- modules/apps/arr/default.nix | 12 +-------- modules/apps/calibre/default.nix | 11 +------- modules/apps/freshrss/default.nix | 12 +-------- modules/apps/gitea/default.nix | 12 +-------- modules/apps/jellyfin/default.nix | 12 +-------- modules/apps/solidtime/default.nix | 26 +++---------------- modules/apps/traefik/default.nix | 12 +-------- modules/apps/vaultwarden/default.nix | 12 +-------- modules/common/default.nix | 1 + modules/common/docker.nix | 38 ++++++++++++++++++++++++++++ 10 files changed, 49 insertions(+), 99 deletions(-) create mode 100644 modules/common/docker.nix diff --git a/modules/apps/arr/default.nix b/modules/apps/arr/default.nix index 7b530c3..5f69663 100644 --- a/modules/apps/arr/default.nix +++ b/modules/apps/arr/default.nix @@ -87,8 +87,7 @@ in { }; # Make sure the Docker network exists. - systemd.services."docker-${networkName}-create-network" = lib.mkIf inUse { - description = "Create Docker network for ${networkName}"; + homelab.dockerNetworks."${networkName}" = lib.mkIf inUse { requiredBy = [ "docker-bazarr.service" "docker-prowlarr.service" @@ -96,15 +95,6 @@ in { "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. diff --git a/modules/apps/calibre/default.nix b/modules/apps/calibre/default.nix index bddf5c8..9018f1e 100644 --- a/modules/apps/calibre/default.nix +++ b/modules/apps/calibre/default.nix @@ -57,19 +57,10 @@ in { }; # Make sure the Docker network exists. - systemd.services."docker-${networkName}-create-network" = { + homelab.dockerNetworks."${networkName}" = { 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 - ''; }; }) diff --git a/modules/apps/freshrss/default.nix b/modules/apps/freshrss/default.nix index 4f4456f..bf6af85 100644 --- a/modules/apps/freshrss/default.nix +++ b/modules/apps/freshrss/default.nix @@ -35,20 +35,10 @@ in { ]; }; - systemd.services."docker-${networkName}-create-network" = { - description = "Create Docker network for ${networkName}"; + homelab.dockerNetworks."${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 = { diff --git a/modules/apps/gitea/default.nix b/modules/apps/gitea/default.nix index 0361bd5..95feea5 100644 --- a/modules/apps/gitea/default.nix +++ b/modules/apps/gitea/default.nix @@ -70,21 +70,11 @@ in { }; # Make sure the Docker network exists. - systemd.services."docker-${networkName}-create-network" = { - description = "Create Docker network for ${networkName}"; + homelab.dockerNetworks."${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 = { diff --git a/modules/apps/jellyfin/default.nix b/modules/apps/jellyfin/default.nix index 011f56b..6e422b2 100644 --- a/modules/apps/jellyfin/default.nix +++ b/modules/apps/jellyfin/default.nix @@ -75,21 +75,11 @@ in { }; # Make sure the Docker network exists. - systemd.services."docker-${networkName}-create-network" = { - description = "Create Docker network for ${networkName}"; + homelab.dockerNetworks."${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 = { diff --git a/modules/apps/solidtime/default.nix b/modules/apps/solidtime/default.nix index 725d32d..80202b0 100644 --- a/modules/apps/solidtime/default.nix +++ b/modules/apps/solidtime/default.nix @@ -92,24 +92,13 @@ in { homelab.virtualisation.containers.enable = true; # Make sure the Docker network exists. - systemd.services = { - "docker-${networkName}-create-network" = { - description = "Create Docker network for ${networkName}"; + homelab.dockerNetworks = { + "${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}"; + "${internalNetworkName}" = { requiredBy = [ "${containers.solidtime.serviceName}.service" "${containers.solidtimeScheduler.serviceName}.service" @@ -117,15 +106,6 @@ in { "${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 - ''; }; }; diff --git a/modules/apps/traefik/default.nix b/modules/apps/traefik/default.nix index 7f6ce38..6623955 100644 --- a/modules/apps/traefik/default.nix +++ b/modules/apps/traefik/default.nix @@ -18,20 +18,10 @@ in { 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}"; + homelab.dockerNetworks."${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 = { diff --git a/modules/apps/vaultwarden/default.nix b/modules/apps/vaultwarden/default.nix index 4510299..5e9b891 100644 --- a/modules/apps/vaultwarden/default.nix +++ b/modules/apps/vaultwarden/default.nix @@ -33,21 +33,11 @@ in { }; # Make sure the Docker network exists. - systemd.services."docker-${networkName}-create-network" = { - description = "Create Docker network for ${networkName}"; + homelab.dockerNetworks."${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 = let diff --git a/modules/common/default.nix b/modules/common/default.nix index be4b3d3..5d4b801 100644 --- a/modules/common/default.nix +++ b/modules/common/default.nix @@ -1,5 +1,6 @@ { imports = [ + ./docker.nix ./monitoring.nix ]; diff --git a/modules/common/docker.nix b/modules/common/docker.nix new file mode 100644 index 0000000..293da32 --- /dev/null +++ b/modules/common/docker.nix @@ -0,0 +1,38 @@ +{ config, lib, pkgs, ... }: + +{ + options.homelab.dockerNetworks = lib.mkOption { + description = "Declarative Docker networks to create before containers start."; + default = {}; + type = lib.types.attrsOf (lib.types.submodule { + options = { + requiredBy = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = []; + description = "List of systemd services that require this network (e.g., docker-containerName.service)."; + }; + }; + }); + }; + + config = { + systemd.services = lib.mapAttrs' (networkName: cfg: + lib.nameValuePair "docker-${networkName}-create-network" { + description = "Create Docker network for ${networkName}"; + requiredBy = cfg.requiredBy; + after = [ "network.target" "docker.service" ]; + requires = [ "docker.service" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + ExecStart = pkgs.writeShellScript "create-${networkName}-docker-network" '' + if ! ${pkgs.docker}/bin/docker network ls | grep -q ${networkName}; then + ${pkgs.docker}/bin/docker network create ${networkName} + fi + ''; + }; + } + ) config.homelab.dockerNetworks; + }; +} From 1403b1f9c0b2b8f7f1ad971b698e68ec79541857 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Fri, 17 Jul 2026 23:24:17 +0200 Subject: [PATCH 55/76] refactor: abstract NFS mount creation via custom options --- modules/apps/arr/default.nix | 39 ++++++++--------- modules/apps/calibre/default.nix | 30 ++++---------- modules/apps/freshrss/default.nix | 11 ++--- modules/apps/gitea/default.nix | 21 +--------- modules/apps/homepage/default.nix | 9 +--- modules/apps/jellyfin/default.nix | 43 ++++--------------- modules/common/default.nix | 1 + modules/common/nfs.nix | 46 +++++++++++++++++++++ modules/fileSystems/media/video/default.nix | 13 +++--- 9 files changed, 93 insertions(+), 120 deletions(-) create mode 100644 modules/common/nfs.nix diff --git a/modules/apps/arr/default.nix b/modules/apps/arr/default.nix index 5f69663..1b1ce53 100644 --- a/modules/apps/arr/default.nix +++ b/modules/apps/arr/default.nix @@ -61,29 +61,26 @@ in { virtualisation.containers.enable = lib.mkIf inUse true; }; - fileSystems = let - mkFileSystem = device: { - inherit device; - fsType = "nfs"; - options = [ - "rw" - "auto" - "nfsvers=4.2" - "rsize=1048576" "wsize=1048576" - "hard" - "timeo=600" "retrans=2" - "_netdev" "nosuid" "tcp" - ]; - }; - + homelab.nfsMounts = let hugoBackup = "192.168.0.11:/mnt/BIG/BACKUP"; + arrOptions = [ + "auto" + "rsize=1048576" "wsize=1048576" + "hard" + "timeo=600" "retrans=2" + "_netdev" + ]; + mkMount = device: { + inherit device; + extraOptions = arrOptions; + }; 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"; + "/srv/bazarr-backup" = lib.mkIf cfg.bazarr.enable (mkMount "${hugoBackup}/BAZARR"); + "/srv/prowlarr-backup" = lib.mkIf cfg.bazarr.enable (mkMount "${hugoBackup}/PROWLARR"); + "/srv/qbittorrent" = lib.mkIf cfg.qbittorrent.enable (mkMount "192.168.0.11:/mnt/SMALL/CONFIG/QBITTORRENT"); + "/srv/radarr-backup" = lib.mkIf cfg.radarr.enable (mkMount "${hugoBackup}/RADARR"); + "/srv/sonarr-backup" = lib.mkIf cfg.sonarr.enable (mkMount "${hugoBackup}/SONARR"); + "/srv/torrent" = mkMount "192.168.0.11:/mnt/SMALL/MEDIA/TORRENT"; }; # Make sure the Docker network exists. diff --git a/modules/apps/calibre/default.nix b/modules/apps/calibre/default.nix index 9018f1e..ea722bc 100644 --- a/modules/apps/calibre/default.nix +++ b/modules/apps/calibre/default.nix @@ -42,17 +42,13 @@ in { shell = null; }; - fileSystems."${books}" = { + homelab.nfsMounts."${books}" = { device = "192.168.0.11:/mnt/SMALL/MEDIA/BOOKS"; - fsType = "nfs"; - options = [ - "rw" + extraOptions = [ "auto" - "nfsvers=4.2" "rsize=1048576" "wsize=1048576" - "soft" "timeo=600" "retrans=2" - "_netdev" "nosuid" "tcp" + "_netdev" ]; }; @@ -66,17 +62,13 @@ in { # Calibre desktop (lib.mkIf cfg.desktop.enable { - fileSystems."${calibre-config}" = { + homelab.nfsMounts."${calibre-config}" = { device = "192.168.0.11:/mnt/SMALL/CONFIG/CALIBRE"; - fsType = "nfs"; - options = [ - "rw" + extraOptions = [ "auto" - "nfsvers=4.2" "rsize=1048576" "wsize=1048576" - "soft" "timeo=600" "retrans=2" - "_netdev" "nosuid" "tcp" + "_netdev" ]; }; @@ -124,17 +116,13 @@ in { # Calibre Web (lib.mkIf cfg.web.enable { - fileSystems."${calibre-web-config}" = { + homelab.nfsMounts."${calibre-web-config}" = { device = "192.168.0.11:/mnt/SMALL/CONFIG/CALIBRE-WEB"; - fsType = "nfs"; - options = [ - "rw" + extraOptions = [ "auto" - "nfsvers=4.2" "rsize=1048576" "wsize=1048576" - "soft" "timeo=600" "retrans=2" - "_netdev" "nosuid" "tcp" + "_netdev" ]; }; diff --git a/modules/apps/freshrss/default.nix b/modules/apps/freshrss/default.nix index bf6af85..c0729a1 100644 --- a/modules/apps/freshrss/default.nix +++ b/modules/apps/freshrss/default.nix @@ -20,18 +20,13 @@ in { lib.mkIf cfg.enable { homelab.virtualisation.containers.enable = true; - fileSystems."/srv/freshrss" = { + homelab.nfsMounts."/srv/freshrss" = { device = "192.168.0.11:/mnt/SMALL/CONFIG/FRESHRSS"; - fsType = "nfs"; - options = [ - "rw" + extraOptions = [ "auto" - "nfsvers=4.2" - "async" "soft" "timeo=600" + "timeo=600" "retrans=2" "_netdev" - "nosuid" - "tcp" ]; }; diff --git a/modules/apps/gitea/default.nix b/modules/apps/gitea/default.nix index 95feea5..9073c38 100644 --- a/modules/apps/gitea/default.nix +++ b/modules/apps/gitea/default.nix @@ -40,32 +40,13 @@ in { # 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 + homelab.nfsMounts = { "/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" - ]; }; }; diff --git a/modules/apps/homepage/default.nix b/modules/apps/homepage/default.nix index b34f32f..cc1b43b 100644 --- a/modules/apps/homepage/default.nix +++ b/modules/apps/homepage/default.nix @@ -34,15 +34,10 @@ in { shell = null; }; - fileSystems."${homepage-config}" = { + homelab.nfsMounts."${homepage-config}" = { device = "192.168.0.11:/mnt/SMALL/CONFIG/HOMEPAGE"; - fsType = "nfs"; - options = [ - "rw" + extraOptions = [ "auto" - "nfsvers=4.2" - "async" "soft" "timeo=100" "retry=50" "actimeo=1800" "lookupcache=all" - "nosuid" "tcp" ]; }; diff --git a/modules/apps/jellyfin/default.nix b/modules/apps/jellyfin/default.nix index 6e422b2..560f527 100644 --- a/modules/apps/jellyfin/default.nix +++ b/modules/apps/jellyfin/default.nix @@ -25,42 +25,15 @@ in { 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/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" - ]; + homelab.nfsMounts = let + mkMount = device: { + inherit device; + readOnly = true; }; + in { + "/srv/audio" = mkMount "192.168.0.11:/mnt/SMALL/MEDIA/AUDIO"; + "/srv/homevideo" = mkMount "192.168.0.11:/mnt/BIG/MEDIA/HOMEVIDEO/ARCHIVE"; + "/srv/photo" = mkMount "192.168.0.11:/mnt/BIG/MEDIA/PHOTO/ARCHIVE"; }; users.users.jellyfin = { diff --git a/modules/common/default.nix b/modules/common/default.nix index 5d4b801..0a6ac0e 100644 --- a/modules/common/default.nix +++ b/modules/common/default.nix @@ -2,6 +2,7 @@ imports = [ ./docker.nix ./monitoring.nix + ./nfs.nix ]; config = { diff --git a/modules/common/nfs.nix b/modules/common/nfs.nix new file mode 100644 index 0000000..251676f --- /dev/null +++ b/modules/common/nfs.nix @@ -0,0 +1,46 @@ +{ config, lib, ... }: + +{ + options.homelab.nfsMounts = lib.mkOption { + type = lib.types.attrsOf (lib.types.submodule { + options = { + device = lib.mkOption { + type = lib.types.str; + description = "The NFS device, e.g. 192.168.0.11:/mnt/POOL/DATA"; + }; + readOnly = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Whether to mount the NFS share read-only"; + }; + extraOptions = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ + "async" + "soft" + "timeo=100" + "retry=50" + "actimeo=1800" + "lookupcache=all" + ]; + description = "Extra NFS mount options to append"; + }; + }; + }); + default = {}; + description = "NFS mounts to automatically configure with standard homelab options"; + }; + + config = { + fileSystems = lib.mapAttrs (path: cfg: { + device = cfg.device; + fsType = "nfs"; + options = [ + "nfsvers=4.2" + "nosuid" + "tcp" + ] ++ (if cfg.readOnly then [ "ro" ] else [ "rw" ]) + ++ cfg.extraOptions; + }) config.homelab.nfsMounts; + }; +} diff --git a/modules/fileSystems/media/video/default.nix b/modules/fileSystems/media/video/default.nix index e46193c..55b6068 100644 --- a/modules/fileSystems/media/video/default.nix +++ b/modules/fileSystems/media/video/default.nix @@ -25,17 +25,14 @@ in { }; config = lib.mkIf cfg.enable { - fileSystems."${cfg.hostPath}" = { + homelab.nfsMounts."${cfg.hostPath}" = { device = "192.168.0.11:${remotePath}"; - fsType = "nfs"; - options = [ - permissionsOption + readOnly = permissionsOption == "ro"; + extraOptions = [ "auto" - "nfsvers=4.2" - "async" "soft" "rsize=1048576" "wsize=1048576" - "timeo=600" "retry=50" "retrans=2" "actimeo=1800" "lookupcache=all" - "_netdev" "nosuid" "tcp" + "timeo=600" "retrans=2" + "_netdev" ]; }; }; From d74267146060bc29ba69a730c47bddf59dc5c042 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Fri, 17 Jul 2026 23:26:03 +0200 Subject: [PATCH 56/76] refactor: abstract app user creation via custom options --- modules/apps/arr/default.nix | 15 +++++-------- modules/apps/calibre/default.nix | 7 ++---- modules/apps/gitea/default.nix | 8 ++----- modules/apps/homepage/default.nix | 8 ++----- modules/apps/jellyfin/default.nix | 12 +++------- modules/common/default.nix | 1 + modules/common/users.nix | 37 +++++++++++++++++++++++++++++++ 7 files changed, 52 insertions(+), 36 deletions(-) create mode 100644 modules/common/users.nix diff --git a/modules/apps/arr/default.nix b/modules/apps/arr/default.nix index 1b1ce53..befd61b 100644 --- a/modules/apps/arr/default.nix +++ b/modules/apps/arr/default.nix @@ -95,22 +95,17 @@ in { }; # Create a user for each app. - users.users = let + homelab.appUsers = let mkUser = uid: { - uid = lib.mkForce uid; - isSystemUser = true; + inherit uid; group = config.users.groups.media.name; - home = "/var/empty"; - shell = null; }; in { bazarr = lib.mkIf cfg.bazarr.enable (mkUser 3003); prowlarr = lib.mkIf cfg.prowlarr.enable (mkUser 3004); - qbittorrent = lib.mkIf cfg.qbittorrent.enable (mkUser 3005) // { - extraGroups = [ - config.users.groups.apps.name - ]; - }; + qbittorrent = lib.mkIf cfg.qbittorrent.enable ((mkUser 3005) // { + extraGroups = [ config.users.groups.apps.name ]; + }); radarr = lib.mkIf cfg.radarr.enable (mkUser 3006); sonarr = lib.mkIf cfg.sonarr.enable (mkUser 3007); }; diff --git a/modules/apps/calibre/default.nix b/modules/apps/calibre/default.nix index ea722bc..f342679 100644 --- a/modules/apps/calibre/default.nix +++ b/modules/apps/calibre/default.nix @@ -34,12 +34,9 @@ in { virtualisation.containers.enable = true; }; - users.users.calibre = { - uid = lib.mkForce 3010; - isSystemUser = true; + homelab.appUsers.calibre = { + uid = 3010; group = config.users.groups.media.name; - home = "/var/empty"; - shell = null; }; homelab.nfsMounts."${books}" = { diff --git a/modules/apps/gitea/default.nix b/modules/apps/gitea/default.nix index 9073c38..eeec8b6 100644 --- a/modules/apps/gitea/default.nix +++ b/modules/apps/gitea/default.nix @@ -30,12 +30,8 @@ in { virtualisation.containers.enable = true; }; - users.users.gitea = { - uid = lib.mkForce UID; - isSystemUser = true; - group = config.users.groups.apps.name; - home = "/var/empty"; - shell = null; + homelab.appUsers.gitea = { + uid = UID; }; # Use filesystem mounts because rootless containers otherwise don't have access to the mount path (nested in docker directories). diff --git a/modules/apps/homepage/default.nix b/modules/apps/homepage/default.nix index cc1b43b..5901ee7 100644 --- a/modules/apps/homepage/default.nix +++ b/modules/apps/homepage/default.nix @@ -26,12 +26,8 @@ in { virtualisation.containers.enable = true; }; - users.users.homepage = { - uid = lib.mkForce 3018; - isSystemUser = true; - group = config.users.groups.apps.name; - home = "/var/empty"; - shell = null; + homelab.appUsers.homepage = { + uid = 3018; }; homelab.nfsMounts."${homepage-config}" = { diff --git a/modules/apps/jellyfin/default.nix b/modules/apps/jellyfin/default.nix index 560f527..56fba09 100644 --- a/modules/apps/jellyfin/default.nix +++ b/modules/apps/jellyfin/default.nix @@ -36,15 +36,9 @@ in { "/srv/photo" = mkMount "192.168.0.11:/mnt/BIG/MEDIA/PHOTO/ARCHIVE"; }; - 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; + homelab.appUsers.jellyfin = { + uid = UID; + extraGroups = [ config.users.groups.media.name ]; }; # Make sure the Docker network exists. diff --git a/modules/common/default.nix b/modules/common/default.nix index 0a6ac0e..98b415e 100644 --- a/modules/common/default.nix +++ b/modules/common/default.nix @@ -3,6 +3,7 @@ ./docker.nix ./monitoring.nix ./nfs.nix + ./users.nix ]; config = { diff --git a/modules/common/users.nix b/modules/common/users.nix new file mode 100644 index 0000000..59aaea0 --- /dev/null +++ b/modules/common/users.nix @@ -0,0 +1,37 @@ +{ config, lib, ... }: + +{ + options.homelab.appUsers = lib.mkOption { + type = lib.types.attrsOf (lib.types.submodule { + options = { + uid = lib.mkOption { + type = lib.types.int; + description = "The user ID for the app user"; + }; + group = lib.mkOption { + type = lib.types.str; + default = config.users.groups.apps.name; + description = "The primary group for the app user"; + }; + extraGroups = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = []; + description = "Extra groups for the app user"; + }; + }; + }); + default = {}; + description = "App users to automatically create with standard homelab options"; + }; + + config = { + users.users = lib.mapAttrs (name: cfg: { + uid = lib.mkForce cfg.uid; + isSystemUser = true; + group = cfg.group; + extraGroups = cfg.extraGroups; + home = "/var/empty"; + shell = null; + }) config.homelab.appUsers; + }; +} From 95e67dd1518a199291d0af661cda96904790abb4 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Fri, 17 Jul 2026 23:29:17 +0200 Subject: [PATCH 57/76] refactor: abstract traefik router labels and dynamic config --- modules/apps/arr/default.nix | 53 ++++++++++------------- modules/apps/calibre/default.nix | 22 +++++----- modules/apps/freshrss/default.nix | 34 ++++++++------- modules/apps/homepage/default.nix | 13 +++--- modules/apps/jellyfin/default.nix | 16 ++++--- modules/apps/monitoring/default.nix | 6 +-- modules/apps/traefik/default.nix | 16 ++++++- modules/common/default.nix | 1 + modules/common/traefik.nix | 65 +++++++++++++++++++++++++++++ 9 files changed, 152 insertions(+), 74 deletions(-) create mode 100644 modules/common/traefik.nix diff --git a/modules/apps/arr/default.nix b/modules/apps/arr/default.nix index befd61b..8fcc56d 100644 --- a/modules/apps/arr/default.nix +++ b/modules/apps/arr/default.nix @@ -110,6 +110,29 @@ in { sonarr = lib.mkIf cfg.sonarr.enable (mkUser 3007); }; + homelab.traefikRouters = { + bazarr = lib.mkIf cfg.bazarr.enable { + rule = "Host(`bazarr.depeuter.dev`)"; + port = 6767; + }; + prowlarr = lib.mkIf cfg.prowlarr.enable { + rule = "Host(`prowlarr.depeuter.dev`)"; + port = 9696; + }; + qbittorrent = lib.mkIf cfg.qbittorrent.enable { + rule = "Host(`qb.depeuter.dev`)"; + port = 10095; + }; + radarr = lib.mkIf cfg.radarr.enable { + rule = "Host(`radarr.depeuter.dev`)"; + port = 7878; + }; + sonarr = lib.mkIf cfg.sonarr.enable { + rule = "Host(`sonarr.depeuter.dev`)"; + port = 8989; + }; + }; + virtualisation.oci-containers.containers = let videoHostPath = config.homelab.fileSystems.media.video.hostPath; in { @@ -141,12 +164,6 @@ in { "${videoHostPath}/Films:/media/movies" "${videoHostPath}/Series:/media/series" ]; - labels = { - "traefik.enable" = "true"; - "traefik.docker.network" = proxyNet; - "traefik.http.routers.bazarr.rule" = "Host(`bazarr.depeuter.dev`)"; - "traefik.http.services.bazarr.loadbalancer.server.port" = toString port; - }; }; prowlarr = let @@ -172,12 +189,6 @@ in { "/srv/prowlarr-backup:/config/Backups" ]; - labels = { - "traefik.enable" = "true"; - "traefik.docker.network" = proxyNet; - "traefik.http.routers.prowlarr.rule" = "Host(`prowlarr.depeuter.dev`)"; - "traefik.http.services.prowlarr.loadbalancer.server.port" = toString port; - }; }; qbittorrent = let @@ -205,12 +216,6 @@ in { "/srv/torrent:/media/cache" ]; - labels = { - "traefik.enable" = "true"; - "traefik.docker.network" = proxyNet; - "traefik.http.routers.qbittorrent.rule" = "Host(`qb.depeuter.dev`)"; - "traefik.http.services.qbittorrent.loadbalancer.server.port" = toString port; - }; }; radarr = let @@ -239,12 +244,6 @@ in { "/srv/torrent:/media/cache" "${videoHostPath}/Films:/media/movies" ]; - labels = { - "traefik.enable" = "true"; - "traefik.docker.network" = proxyNet; - "traefik.http.routers.radarr.rule" = "Host(`radarr.depeuter.dev`)"; - "traefik.http.services.radarr.loadbalancer.server.port" = toString port; - }; }; sonarr = let @@ -273,12 +272,6 @@ in { "/srv/torrent:/media/cache" "${videoHostPath}/Series:/media/series" ]; - labels = { - "traefik.enable" = "true"; - "traefik.docker.network" = proxyNet; - "traefik.http.routers.sonarr.rule" = "Host(`sonarr.depeuter.dev`)"; - "traefik.http.services.sonarr.loadbalancer.server.port" = toString port; - }; }; }; }; diff --git a/modules/apps/calibre/default.nix b/modules/apps/calibre/default.nix index f342679..e7847da 100644 --- a/modules/apps/calibre/default.nix +++ b/modules/apps/calibre/default.nix @@ -55,6 +55,16 @@ in { "docker-calibre.service" ]; }; + homelab.traefikRouters = { + calibre = lib.mkIf cfg.desktop.enable { + rule = "Host(`calibre.depeuter.dev`)"; + port = 8080; + }; + calibre-web = lib.mkIf cfg.web.enable { + rule = "Host(`books.depeuter.dev`)"; + port = 8083; + }; + }; }) # Calibre desktop @@ -102,12 +112,6 @@ in { "${books}:/media/books" ]; - labels = { - "traefik.enable" = "true"; - "traefik.docker.network" = proxyNet; - "traefik.http.routers.calibre.rule" = "Host(`calibre.depeuter.dev`)"; - "traefik.http.services.calibre.loadbalancer.server.port" = toString innerPort; - }; }; }) @@ -153,12 +157,6 @@ in { "${books}:/media/books" ]; - labels = { - "traefik.enable" = "true"; - "traefik.docker.network" = proxyNet; - "traefik.http.routers.calibre-web.rule" = "Host(`books.depeuter.dev`)"; - "traefik.http.services.calibre-web.loadbalancer.server.port" = toString innerPort; - }; }; }) ]; diff --git a/modules/apps/freshrss/default.nix b/modules/apps/freshrss/default.nix index c0729a1..e997fc5 100644 --- a/modules/apps/freshrss/default.nix +++ b/modules/apps/freshrss/default.nix @@ -4,6 +4,7 @@ let cfg = config.homelab.apps.freshrss; networkName = "freshrss"; + proxyNet = config.homelab.apps.traefik.sharedNetworkName; in { options.homelab.apps.freshrss = { enable = lib.mkEnableOption "FreshRSS"; @@ -36,6 +37,22 @@ in { ]; }; + homelab.traefikRouters.freshrss = { + rule = "Host(`rss.depeuter.dev`)"; + port = 80; + tls = true; + entryPoints = [ "websecure" ]; + middlewares = [ "freshrssM1" "freshrssM2" ]; + extraLabels = { + "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"; + }; + }; + virtualisation.oci-containers.containers.freshrss = { hostname = "freshrss"; image = "freshrss/freshrss:1.25.0"; @@ -46,6 +63,7 @@ in { ]; extraOptions = [ "--network=${networkName}" + "--network=${proxyNet}" ]; environment = { TZ = config.time.timeZone; @@ -57,22 +75,6 @@ in { "/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/homepage/default.nix b/modules/apps/homepage/default.nix index 5901ee7..b820e2b 100644 --- a/modules/apps/homepage/default.nix +++ b/modules/apps/homepage/default.nix @@ -37,6 +37,13 @@ in { ]; }; + homelab.traefikRouters.homepage = let + host = "homepage.${config.networking.domain}"; + in { + rule = "Host(`${host}`)"; + port = cfg.port; + }; + virtualisation.oci-containers.containers.homepage = let host = "homepage.${config.networking.domain}"; in { @@ -54,12 +61,6 @@ in { "${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; diff --git a/modules/apps/jellyfin/default.nix b/modules/apps/jellyfin/default.nix index 56fba09..09bbbb5 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"; + proxyNet = config.homelab.apps.traefik.sharedNetworkName; inherit (config.homelab.fileSystems) media; UID = 3008; @@ -49,6 +50,14 @@ in { ]; }; + homelab.traefikRouters.feishin = { + rule = "Host(`play.jelly.depeuter.dev`)"; + port = 9180; + extraLabels = { + "traefik.tls.options.default.minVersion" = "VersionTLS13"; + }; + }; + virtualisation.oci-containers.containers = { jellyfin = { hostname = "jellyfin"; @@ -102,6 +111,7 @@ in { ]; extraOptions = [ "--network=${networkName}" + "--network=${proxyNet}" ]; environment = { # pre defined server name @@ -114,12 +124,6 @@ in { 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/monitoring/default.nix b/modules/apps/monitoring/default.nix index 035ddec..a1bd265 100644 --- a/modules/apps/monitoring/default.nix +++ b/modules/apps/monitoring/default.nix @@ -136,7 +136,7 @@ }; # 6. Traefik Reverse Proxy for Grafana and NTFY - services.traefik.dynamicConfigOptions.http = { + homelab.apps.traefik.dynamicConfigOptions.http = { routers = { grafana = { rule = "Host(`grafana.lab.depeuter.dev`)"; @@ -148,8 +148,8 @@ }; }; services = { - grafana.loadBalancer.servers = [{ url = "http://127.0.0.1:3000"; }]; - ntfy.loadBalancer.servers = [{ url = "http://127.0.0.1:2586"; }]; + grafana.loadBalancer.servers = [{ url = "http://host.docker.internal:3000"; }]; + ntfy.loadBalancer.servers = [{ url = "http://host.docker.internal:2586"; }]; }; }; diff --git a/modules/apps/traefik/default.nix b/modules/apps/traefik/default.nix index 6623955..452df43 100644 --- a/modules/apps/traefik/default.nix +++ b/modules/apps/traefik/default.nix @@ -12,6 +12,11 @@ in { default = "traefik"; description = "The name of the shared network to connect the container to."; }; + dynamicConfigOptions = lib.mkOption { + type = lib.types.attrs; + default = {}; + description = "Dynamic configuration options to write to file and mount into Traefik."; + }; }; config = lib.mkIf cfg.enable { @@ -35,6 +40,7 @@ in { ]; extraOptions = [ "--network=${cfg.sharedNetworkName}" + "--add-host=host.docker.internal:host-gateway" ]; environmentFiles = [ /home/admin/.cloudflare.secret @@ -46,6 +52,10 @@ in { "--providers.docker=true" "--providers.docker.exposedByDefault=false" + # Add File provider + "--providers.file.filename=/etc/traefik/dynamic_conf.yml" + "--providers.file.watch=true" + # Add web entrypoint "--entrypoints.web.address=:80/tcp" "--entrypoints.web.http.redirections.entrypoint.to=websecure" @@ -65,10 +75,14 @@ in { "--certificatesresolvers.letsencrypt.acme.email=tibo.depeuter@telenet.be" "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json" ]; - volumes = [ + volumes = let + dynamicConfFormat = pkgs.formats.yaml { }; + dynamicConfFile = dynamicConfFormat.generate "traefik-dynamic-conf.yml" cfg.dynamicConfigOptions; + in [ "letsencryp:/letsencrypt" "/var/run/docker.sock:/var/run/docker.sock:ro" + "${dynamicConfFile}:/etc/traefik/dynamic_conf.yml:ro" ]; labels = { "traefik.enable" = "true"; diff --git a/modules/common/default.nix b/modules/common/default.nix index 98b415e..60e4048 100644 --- a/modules/common/default.nix +++ b/modules/common/default.nix @@ -3,6 +3,7 @@ ./docker.nix ./monitoring.nix ./nfs.nix + ./traefik.nix ./users.nix ]; diff --git a/modules/common/traefik.nix b/modules/common/traefik.nix new file mode 100644 index 0000000..81d1c10 --- /dev/null +++ b/modules/common/traefik.nix @@ -0,0 +1,65 @@ +{ config, lib, ... }: + +let + proxyNet = config.homelab.apps.traefik.sharedNetworkName; +in { + options.homelab.traefikRouters = lib.mkOption { + type = lib.types.attrsOf (lib.types.submodule { + options = { + rule = lib.mkOption { + type = lib.types.str; + description = "The Traefik router rule, e.g. Host(`example.com`)"; + }; + port = lib.mkOption { + type = lib.types.int; + description = "The port the service listens on"; + }; + middlewares = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = []; + description = "Middlewares to apply"; + }; + tls = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Whether to enable TLS"; + }; + entryPoints = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = []; + description = "Entrypoints to use"; + }; + extraLabels = lib.mkOption { + type = lib.types.attrsOf lib.types.str; + default = {}; + description = "Extra labels to apply"; + }; + }; + }); + default = {}; + description = "Declarative Traefik router configuration"; + }; + + config = { + # Generate labels for containers based on homelab.traefikRouters + # This assumes that the name in homelab.traefikRouters matches the container name. + virtualisation.oci-containers.containers = lib.mapAttrs (name: router: { + labels = { + "traefik.enable" = "true"; + "traefik.docker.network" = proxyNet; + "traefik.http.routers.${name}.rule" = router.rule; + "traefik.http.services.${name}.loadbalancer.server.port" = toString router.port; + } + // lib.optionalAttrs (router.middlewares != []) { + "traefik.http.routers.${name}.middlewares" = builtins.concatStringsSep "," router.middlewares; + } + // lib.optionalAttrs router.tls { + "traefik.http.routers.${name}.tls" = "true"; + } + // lib.optionalAttrs (router.entryPoints != []) { + "traefik.http.routers.${name}.entryPoints" = builtins.concatStringsSep "," router.entryPoints; + } + // router.extraLabels; + }) config.homelab.traefikRouters; + }; +} From 501cda640262e08757a89e646c0fb6da90e86ca6 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Wed, 22 Jul 2026 20:52:28 +0200 Subject: [PATCH 58/76] docs: clarify proxmox fresh install state and potential breaking changes --- BOOTSTRAP.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/BOOTSTRAP.md b/BOOTSTRAP.md index e9df6c2..0db25b7 100644 --- a/BOOTSTRAP.md +++ b/BOOTSTRAP.md @@ -23,9 +23,18 @@ We use `sops-nix` to manage secrets, adhering to a strict separation between Pro 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 +## 2. Proxmox Hypervisor Authentication & State -The CI/CD pipeline needs restricted API access to Proxmox to provision Virtual Machines. +The CI/CD pipeline needs restricted API access to Proxmox to provision Virtual Machines. + +### Fresh Install vs. Existing Install +> [!WARNING] +> This repository assumes a standard, fresh installation of Proxmox VE. +> +> **If you are NOT starting from a fresh install, be aware of these potential breaking changes:** +> * **VM ID Conflicts:** Terraform automatically assigns VM IDs. If you have existing VMs, Terraform might fail to provision or (if misconfigured) attempt to overwrite them. Check your Terraform variables to ensure the ID range (e.g., 8000+) does not conflict. +> * **Storage Pools:** The automation assumes the default Proxmox storage pools (`local` for snippets/ISOs, and `local-lvm` or `local-zfs` for VM disks). If you renamed your pools, you must update the Terraform configuration. +> * **Network Bridges:** It assumes `vmbr0` is available for VM networking. **Prerequisites:** Install `terraform` ([Terraform installation](https://developer.hashicorp.com/terraform/downloads)). From 78a20fbe003bed59cfed366321ac85bb9c883e23 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Wed, 22 Jul 2026 21:20:16 +0200 Subject: [PATCH 59/76] feat(gitops): implement native custom gitops solution with webhooks --- .gitignore | 1 + NixOS GitOps Migration Specification.md | 138 ++++++++++ flake.lock | 102 +++++++ flake.nix | 21 +- hosts/ACE/default.nix | 41 +++ hosts/Binnenpost/default.nix | 90 +++++++ hosts/Builder/default.nix | 50 ++++ hosts/Development/default.nix | 72 +++++ hosts/Gitea/default.nix | 45 ++++ hosts/Ingress/default.nix | 263 +++++++++++++++++++ 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 | 119 +++++++++ hosts/Niko/hardware-configuration.nix | 53 ++++ hosts/Production/default.nix | 48 ++++ hosts/ProductionArr/default.nix | 48 ++++ hosts/ProductionGPU/default.nix | 98 +++++++ hosts/Template/default.nix | 36 +++ hosts/Testing/default.nix | 49 ++++ hosts/Vaultwarden/default.nix | 49 ++++ modules/common/default.nix | 2 + modules/common/gitops.nix | 141 ++++++++++ 25 files changed, 1804 insertions(+), 18 deletions(-) create mode 100644 .gitignore create mode 100644 NixOS GitOps Migration Specification.md create mode 100644 flake.lock create mode 100644 hosts/ACE/default.nix create mode 100644 hosts/Binnenpost/default.nix create mode 100644 hosts/Builder/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/ProductionArr/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/common/gitops.nix diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..485dee6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea diff --git a/NixOS GitOps Migration Specification.md b/NixOS GitOps Migration Specification.md new file mode 100644 index 0000000..878984d --- /dev/null +++ b/NixOS GitOps Migration Specification.md @@ -0,0 +1,138 @@ +# **Infrastructure Specification: Automated NixOS GitOps Environment** + +## **1\. Architecture Overview** + +This specification outlines the migration from a manually deployed NixOS environment to a fully automated, pull-based GitOps architecture. + +* **Source of Truth:** Forgejo repository (v2 branch). +* **Infrastructure Provisioning:** Terraform (via bpg/proxmox provider) executed by Forgejo Actions. +* **Configuration Management:** NixOS Flakes, pulled autonomously by individual nodes via comin. +* **Secret Management:** sops-nix using Age keys (SSH host keys for VM decryption). +* **Storage:** TrueNAS serving persistent data via NFS/iSCSI, utilizing ZFS snapshot cloning for the staging environment. +* **Dependency Management:** RenovateBot running via Forgejo Actions. + +## **2\. Phase 1: Foundation & State Preparation** + +### **2.1 Git Branching Strategy** + +* Initialize a new orphan branch named v2 (or a fresh repository) to ensure a clean slate for the flake lockfile and SOPS configuration. +* Cherry-pick existing .nix service modules from the legacy main branch as they are migrated. + +### **2.2 Secret Management (Bankruptcy & Reset)** + +* Delete all legacy .sops.yaml configurations. +* Generate a new master age key. This key will be stored securely offline (e.g., Bitwarden) and **never** placed on the Forgejo runner or target VMs. +* Initialize a new .sops.yaml. Target VMs will be added via their public SSH host keys as they are provisioned. + +### **2.3 Storage Strategy (TrueNAS & Staging)** + +To provide the Staging VM with production-accurate data without risking corruption: + +1. **Production State:** TrueNAS maintains the primary ZFS datasets. +2. **Staging State (On-Demand):** When the staging environment spins up, a script (via Proxmox/Forgejo Action or TrueNAS API) takes a temporary ZFS snapshot of the production dataset. +3. **Clone & Mount:** The snapshot is cloned and exported via NFS/SMB specifically for the Staging VM. +4. **Teardown:** When Staging is spun down, the ZFS clone is destroyed. + +## **3\. Phase 2: Proxmox Configuration & Least Privilege** + +Terraform requires API access to Proxmox. To adhere to the principle of least privilege, Proxmox RBAC (Role-Based Access Control) will be utilized. + +### **3.1 Proxmox Static Configuration (RBAC)** + +The root user (root@pam) must **never** be used for automation. Instead, a dedicated API user with a highly restricted role must be created. This can be done via the Proxmox Web UI (Datacenter \-\> Permissions) or via the Proxmox shell (pveum). +**1\. Create the Restricted Role (TerraformProv):** +This role grants only the permissions needed to clone templates, configure hardware, and manage power states. +pveum role add TerraformProv \-privs "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" + +**2\. Create the Terraform User:** +Create a user in the Proxmox VE authentication realm (@pve). +pveum user add terraform@pve + +**3\. Assign Permissions to Paths:** +Bind the role to the user, strictly limiting access to the VMs path and the specific storage path where VMs will reside (e.g., local-zfs). +pveum acl modify /vms \-user terraform@pve \-role TerraformProv +pveum acl modify /storage/local-zfs \-user terraform@pve \-role TerraformProv + +**4\. Generate API Token:** +Generate a token for the user. Proxmox will output a Secret ID. This ID is saved into Forgejo Secrets and passed to the Terraform provider. +pveum user token add terraform@pve tf-automation \--privsep 0 + +*(Note: \--privsep 0 ensures the token inherits the user's permissions, rather than requiring separate ACL definitions).* + +## **4\. Phase 3: Infrastructure as Code (Terraform)** + +### **4.1 The Golden Image (.qcow2)** + +A minimal NixOS image is built locally using nixos-generators and uploaded to Proxmox as a template (e.g., ID 9000). This template contains: + +* QEMU Guest Agent. +* Cloud-Init support. +* comin installed. + +### **4.2 Terraform Provisioning** + +Terraform maintains the hardware state. Its responsibilities are strictly limited to: + +* Cloning Template 9000\. +* Allocating vCPU, RAM, and Virtual Disks. +* Using Cloud-Init to inject network configurations (IP, Gateway) and the Forgejo runner's public SSH key for health check access. + +## **5\. Phase 4: CI/CD & Automation** + +### **5.1 Forgejo Runner & Terraform Pipeline** + +* The newly provisioned Forgejo Runner VM registers with Forgejo. +* A workflow (terraform.yml) runs terraform plan on Pull Requests and terraform apply on merges to the main branch. + +### **5.2 GitOps Pull Agent (comin and Alternatives)** + +Once provisioned by Terraform, VMs boot and pull their OS configuration autonomously. + +* **Chosen Tool (comin):** Polling agent designed specifically for NixOS GitOps. Runs as a systemd service, supports multiple git remotes, branch tracking, and local cryptographic verification. +* **Alternative 1 (system.autoUpgrade):** The native NixOS approach utilizing systemd timers. *Rejected* because it lacks advanced Git authentication and webhooks for immediate triggering. +* **Alternative 2 (deploy-rs / colmena):** Excellent push-based deployment tools. *Rejected* because they require the CI runner to maintain SSH root access to the entire fleet, violating our zero-trust/pull-only goals. + +### **5.3 RenovateBot Integration** + +* Renovate is deployed via a Forgejo Action running on a cron schedule. +* Nix flake support is explicitly enabled in renovate.json. +* Renovate parses flake.lock and Docker tags, automatically opening PRs for updates. + +## **6\. Phase 5: Staging Lifecycle & Healthchecks** + +### **6.1 Lifecycle Automation** + +1. **Trigger:** A PR is opened by Renovate or a developer. +2. **Wake-up:** A Forgejo Action calls the Proxmox API to start the nixos-staging VM and triggers the TrueNAS ZFS snapshot clone. +3. **Pull:** The Staging VM boots, connects to the network, and comin pulls the PR's commit hash. + +### **6.2 Automated Testing** + +The Forgejo Action will verify the deployment was successful: + +* **Systemd Checks:** systemctl is-system-running \--wait and systemctl is-active \. +* **Docker Healthchecks:** docker inspect \--format='{{json .State.Health.Status}}' \. +* **HTTP Probes:** Execute curl commands against the Staging VM to verify HTTP 200 OK. + +### **6.3 Spin Down** + +Upon PR merge or closure, the Forgejo Action shuts down the VM via the Proxmox API and destroys the temporary TrueNAS ZFS clone. + +## **7\. Phase 6: Rollbacks & Disaster Recovery** + +Because infrastructure involves both *stateless* configurations (NixOS) and *stateful* data (TrueNAS), a unified rollback strategy is critical. + +### **7.1 Differentiating Failures (Nix vs. TrueNAS)** + +If a deployment fails, the root cause must be identified to determine the rollback path: + +* **Stateless Failure (NixOS/Code):** The service fails to start immediately. System logs (journalctl \-u my-service) show syntax errors, missing binaries, or bad systemd unit definitions. +* **Stateful Failure (TrueNAS/Data):** The service starts but crashes with a database schema version mismatch, "permission denied" on the NFS mount, or missing user data. This means a service (like a Docker container) attempted a one-way database migration on the persistent TrueNAS dataset and failed halfway. + +### **7.2 The Unified Rollback Procedure** + +When a production rollout fails, **do not** attempt to fix it live. + +1. **Revert the State (TrueNAS):** If the failure was stateful (e.g., a bad database migration), immediately log into TrueNAS and rollback the primary dataset to the automated ZFS snapshot taken right before the deployment. +2. **Revert the Code (Forgejo):** Use the Forgejo UI to click "Revert" on the problematic Pull Request. This creates a new commit restoring the previous flake.nix state. +3. **Autonomous Recovery:** Within 60 seconds, the production VM's comin agent will detect the new commit on main, pull the reverted code, apply the old configuration, and reconnect to the restored TrueNAS dataset. \ No newline at end of file diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..67df8c4 --- /dev/null +++ b/flake.lock @@ -0,0 +1,102 @@ +{ + "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": 1760524057, + "narHash": "sha256-EVAqOteLBFmd7pKkb0+FIUyzTF61VKi7YmvP1tw4nEw=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "544961dfcce86422ba200ed9a0b00dd4b1486ec5", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "ref": "nixos-unstable", + "type": "indirect" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "sops-nix": "sops-nix", + "utils": "utils" + } + }, + "sops-nix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1760393368, + "narHash": "sha256-8mN3kqyqa2PKY0wwZ2UmMEYMcxvNTwLaOrrDsw6Qi4E=", + "owner": "Mic92", + "repo": "sops-nix", + "rev": "ab8d56e85b8be14cff9d93735951e30c3e86a437", + "type": "github" + }, + "original": { + "owner": "Mic92", + "repo": "sops-nix", + "type": "github" + } + }, + "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": 1738591040, + "narHash": "sha256-4WNeriUToshQ/L5J+dTSWC5OJIwT39SEP7V7oylndi8=", + "owner": "gytis-ivaskevicius", + "repo": "flake-utils-plus", + "rev": "afcb15b845e74ac5e998358709b2b5fe42a948d1", + "type": "github" + }, + "original": { + "owner": "gytis-ivaskevicius", + "repo": "flake-utils-plus", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix index 031df52..5450b8d 100644 --- a/flake.nix +++ b/flake.nix @@ -16,11 +16,6 @@ inputs.flake-utils.follows = "flake-utils"; }; - comin = { - url = "github:nlewo/comin"; - inputs.nixpkgs.follows = "nixpkgs"; - }; - pre-commit-hooks = { url = "github:cachix/pre-commit-hooks.nix"; inputs.nixpkgs.follows = "nixpkgs"; @@ -29,7 +24,7 @@ outputs = inputs@{ self, nixpkgs, - flake-utils, sops-nix, utils, comin, pre-commit-hooks, + flake-utils, sops-nix, utils, pre-commit-hooks, ... }: let @@ -67,19 +62,9 @@ ./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"; - }]; - }; + # Inject git revision for our custom GitOps module to query + system.configurationRevision = self.rev or self.dirtyRev or "unknown"; }) ]; }; 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..561fbe1 --- /dev/null +++ b/hosts/Binnenpost/default.nix @@ -0,0 +1,90 @@ +{ pkgs, ... }: + +{ + config = { + environment = { + etc."nixos/tailscale-authkey".text = '' + tskey-auth-k1tfJLTnGB11CNTRL-HhnegtfNzQ3G8h71SC2DR38PFXwseQiu + ''; + + systemPackages = with pkgs; [ + ethtool + ]; + }; + + homelab = { + apps = { + speedtest.enable = true; + technitiumDNS.enable = true; + traefik.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" + ]; + }; + }; + + virtualisation.oci-containers.containers.traefik.labels = { + "traefik.http.routers.roxanne.rule" = "Host(`roxanne.depeuter.dev`)"; + "traefik.http.services.roxanne.loadbalancer.server.url" = "https://192.168.0.13:8006"; + + "traefik.http.routers.hugo.rule" = "Host(`hugo.depeuter.dev`)"; + "traefik.http.services.hugo.loadbalancer.server.url" = "https://192.168.0.11:444"; + }; + + system.stateVersion = "24.05"; + }; +} diff --git a/hosts/Builder/default.nix b/hosts/Builder/default.nix new file mode 100644 index 0000000..515bcb9 --- /dev/null +++ b/hosts/Builder/default.nix @@ -0,0 +1,50 @@ +{ pkgs, ... }: + +{ + config = { + networking = { + hostName = "Builder"; + hostId = "aaaa4200"; + domain = "depeuter.dev"; + + useDHCP = false; + enableIPv6 = true; + + defaultGateway = { + address = "192.168.0.1"; + interface = "ens18"; + }; + + interfaces.ens18 = { + ipv4.addresses = [ + { + address = "192.168.0.42"; + prefixLength = 24; + } + ]; + }; + + nameservers = [ + "1.1.1.1" + "1.0.0.1" + ]; + }; + + # Dedicated builder user + users.users.builder = { + isNormalUser = true; + description = "Nix Remote Builder"; + # You must add the public key corresponding to `builder-ssh-key` here + openssh.authorizedKeys.keys = [ + "ssh-ed25519 AAAAC3NzaC1... TODO: replace with build-farm-key.pub" + ]; + }; + + # Optimize nix settings for a build farm + nix.settings.trusted-users = [ "builder" ]; + nix.settings.cores = 0; # Use all cores + nix.settings.max-jobs = "auto"; + + system.stateVersion = "24.05"; + }; +} diff --git a/hosts/Development/default.nix b/hosts/Development/default.nix new file mode 100644 index 0000000..77f6758 --- /dev/null +++ b/hosts/Development/default.nix @@ -0,0 +1,72 @@ +{ config, pkgs, lib, system, ... }: + +{ + config = { + homelab = { + apps = { + bind9.enable = true; + homepage = { + enable = true; + exposePort = true; + }; + traefik.enable = true; + plex.enable = true; + solidtime.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..c6c9b43 --- /dev/null +++ b/hosts/Gitea/default.nix @@ -0,0 +1,45 @@ +{ config, pkgs, lib, system, ... }: + +{ + config = { + homelab = { + apps.gitea.enable = true; + virtualisation.guest.enable = true; + + users.admin = { + enable = true; + authorizedKeys = [ + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFrp6aM62Bf7bj1YM5AlAWuNrANU3N5e8+LtbbpmZPKS" + ]; + }; + }; + + 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..c0a3ac9 --- /dev/null +++ b/hosts/Ingress/default.nix @@ -0,0 +1,263 @@ +{ 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" = { }; + "home.depeuter.dev" = { }; + "jelly.depeuter.dev" = { }; + "vault.depeuter.dev" = { }; + }; + }; + + # List services that you want to enable. + services = { + # Enable Nginx as a reverse proxy + nginx = let + nextcloud = { + host = "192.168.0.23"; + officePort = 8080; + }; + in { + 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."${nextcloud.host}:${toString nextcloud.officePort}" = {}; + + 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://${nextcloud.host}"; + extraConfig = '' + add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always; + fastcgi_request_buffering off; + ''; + }; + "/office/" = { + proxyPass = "http://${nextcloud.host}:${toString nextcloud.officePort}/"; + 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; + ''; + }; + + "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" = let + jellyfin = { + host = "192.168.0.94"; + port = 8096; + }; + in { + enableACME = true; + forceSSL = true; + locations = { + "/" = { + proxyPass = "http://${jellyfin.host}:${toString jellyfin.port}"; + 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://${jellyfin.host}:${toString jellyfin.port}"; + 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 512M; + + # 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 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 + 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"; + proxyWebsockets = true; + }; + "~ ^/admin".return = 403; + }; + }; + "rss.depeuter.dev" = { + enableACME = true; + forceSSL = true; + locations."/".proxyPass = "http://192.168.92:${toString config.homelab.apps.freshrss.port}"; + }; + }; + }; + }; + + 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..910f325 --- /dev/null +++ b/hosts/Niko/default.nix @@ -0,0 +1,119 @@ +{ config, pkgs, ... }: + +{ + imports = [ + # Include the results of the hardware scan. + ./hardware-configuration.nix + ]; + + homelab = { + apps = { + technitiumDNS.enable = true; + traefik.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; + graphics.enable = true; + }; + + # Select internationalisation properties. + i18n.defaultLocale = "en_GB.UTF-8"; + + 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; + }; + + pulseaudio.enable = true; + + 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; + }; + + # 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"; +} 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..9bb565d --- /dev/null +++ b/hosts/Production/default.nix @@ -0,0 +1,48 @@ +{ config, pkgs, lib, system, ... }: + +{ + config = { + homelab = { + apps = { + calibre.enable = true; + traefik.enable = true; + }; + virtualisation.guest.enable = true; + }; + + networking = { + hostId = "aaaa2100"; + domain = "roxanne.depeuter.dev"; + + useDHCP = false; + + enableIPv6 = true; + + defaultGateway = { + address = "192.168.0.1"; + interface = "ens18"; + }; + + # Open ports in the firewall. + firewall = { + enable = true; + }; + + interfaces.ens18 = { + ipv4.addresses = [ + { + address = "192.168.0.31"; + prefixLength = 24; + } + ]; + }; + + nameservers = [ + "1.1.1.1" # Cloudflare + "1.0.0.1" # Cloudflare + ]; + }; + + system.stateVersion = "24.05"; + }; +} diff --git a/hosts/ProductionArr/default.nix b/hosts/ProductionArr/default.nix new file mode 100644 index 0000000..ff4f4c2 --- /dev/null +++ b/hosts/ProductionArr/default.nix @@ -0,0 +1,48 @@ +{ config, pkgs, lib, system, ... }: + +{ + config = { + homelab = { + apps = { + arr.enable = true; + traefik.enable = true; + }; + virtualisation.guest.enable = true; + }; + + networking = { + hostId = "aaaa2300"; + domain = "roxanne.depeuter.dev"; + + useDHCP = false; + + enableIPv6 = true; + + defaultGateway = { + address = "192.168.0.1"; + interface = "ens18"; + }; + + # Open ports in the firewall. + firewall = { + enable = true; + }; + + interfaces.ens18 = { + ipv4.addresses = [ + { + address = "192.168.0.33"; + prefixLength = 24; + } + ]; + }; + + nameservers = [ + "1.1.1.1" # Cloudflare + "1.0.0.1" # Cloudflare + ]; + }; + + system.stateVersion = "24.05"; + }; +} diff --git a/hosts/ProductionGPU/default.nix b/hosts/ProductionGPU/default.nix new file mode 100644 index 0000000..fa9ca8c --- /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 = "ens18"; + }; + + # Open ports in the firewall. + firewall = { + enable = true; + }; + + interfaces.ens18 = { + ipv4.addresses = [ + { + address = "192.168.0.94"; + prefixLength = 24; + } + ]; + }; + + nameservers = [ + "1.1.1.1" # Cloudflare + "1.0.0.1" # Cloudflare + ]; + }; + + system.stateVersion = "24.11"; + + ### 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 = { + graphics = { + 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..cc353f6 --- /dev/null +++ b/hosts/Testing/default.nix @@ -0,0 +1,49 @@ +{ config, pkgs, lib, system, ... }: + +{ + config = { + homelab = { + apps = { + freshrss.enable = true; + traefik.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..5ded575 --- /dev/null +++ b/hosts/Vaultwarden/default.nix @@ -0,0 +1,49 @@ +{ config, pkgs, lib, system, ... }: + +{ + config = { + homelab = { + apps.vaultwarden = { + enable = true; + domain = "https://vault.depeuter.dev"; + name = "Hugo's Vault"; + }; + virtualisation.guest.enable = true; + + users.admin = { + enable = true; + authorizedKeys = [ + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJnihoyozOCnm6T9OzL2xoMeMZckBYR2w43us68ABA93" + ]; + }; + }; + + 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/common/default.nix b/modules/common/default.nix index 60e4048..eab0450 100644 --- a/modules/common/default.nix +++ b/modules/common/default.nix @@ -1,6 +1,7 @@ { imports = [ ./docker.nix + ./gitops.nix ./monitoring.nix ./nfs.nix ./traefik.nix @@ -11,6 +12,7 @@ homelab = { services.openssh.enable = true; users.admin.enable = true; + gitops.enable = true; }; nix.settings.experimental-features = [ diff --git a/modules/common/gitops.nix b/modules/common/gitops.nix new file mode 100644 index 0000000..b2731e0 --- /dev/null +++ b/modules/common/gitops.nix @@ -0,0 +1,141 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.homelab.gitops; + + updateScript = pkgs.writeShellApplication { + name = "homelab-gitops-update"; + runtimeInputs = [ pkgs.git pkgs.nixos-rebuild pkgs.jq pkgs.coreutils ]; + text = '' + set -euo pipefail + + REMOTE_URL="${cfg.repoUrl}" + BRANCH="${cfg.branch}" + + echo "Checking remote hash for $REMOTE_URL branch $BRANCH..." + + # Fetch remote hash, fallback to unknown if it fails + REMOTE_HASH=$(git ls-remote "$REMOTE_URL" "refs/heads/$BRANCH" | awk '{print $1}' || true) + + if [ -z "$REMOTE_HASH" ]; then + echo "WARNING: Could not fetch remote hash. Forcing rebuild to be safe." + REMOTE_HASH="unknown_remote" + fi + + LOCAL_HASH="unknown_local" + if [ -f /run/current-system/configurationRevision ]; then + LOCAL_HASH=$(cat /run/current-system/configurationRevision) + fi + + echo "Remote hash: $REMOTE_HASH" + echo "Local hash: $LOCAL_HASH" + + if [ "$REMOTE_HASH" = "$LOCAL_HASH" ] && [ "$REMOTE_HASH" != "unknown_remote" ] && [ "$LOCAL_HASH" != "unknown" ]; then + echo "Hashes match. No update needed." + exit 0 + fi + + echo "Hashes differ or unknown. Triggering nixos-rebuild..." + + # Trigger the build and switch + nixos-rebuild switch --flake "git+$REMOTE_URL?ref=$BRANCH" + + echo "Update successful." + ''; + }; + +in { + options.homelab.gitops = { + enable = lib.mkEnableOption "Custom GitOps native deployment system"; + + repoUrl = lib.mkOption { + type = lib.types.str; + default = "https://git.depeuter.dev/Bos55/nix-config.git"; + description = "The repository URL to pull configurations from."; + }; + + branch = lib.mkOption { + type = lib.types.str; + default = "v2"; + description = "The branch to deploy."; + }; + + useBuilder = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Whether to use the central Builder host to compile packages."; + }; + }; + + config = lib.mkIf cfg.enable { + # 1. Systemd Service and Timer for polling + systemd.services.homelab-gitops = { + description = "Homelab GitOps Update Service"; + wants = [ "network-online.target" ]; + after = [ "network-online.target" ]; + serviceConfig = { + Type = "oneshot"; + ExecStart = "${updateScript}/bin/homelab-gitops-update"; + # Must run as root to rebuild the system + User = "root"; + }; + }; + + systemd.timers.homelab-gitops = { + description = "Timer for Homelab GitOps Update Service"; + wantedBy = [ "timers.target" ]; + timerConfig = { + OnBootSec = "5m"; + OnUnitActiveSec = "5m"; + RandomizedDelaySec = "30s"; + }; + }; + + # 2. Webhook listener for instant trigger + sops.secrets."webhook-secret" = {}; + + services.webhook = { + enable = true; + port = 9000; + hooks = { + gitops = { + execute-command = "${pkgs.systemd}/bin/systemctl"; + pass-arguments-to-command = [ + { source = "string"; name = "start"; } + { source = "string"; name = "homelab-gitops.service"; } + ]; + trigger-rule = { + match = { + type = "payload-hash-sha256"; + secret = "{{ getenv \"WEBHOOK_SECRET\" }}"; + parameter = { + source = "header"; + name = "X-Forgejo-Signature"; + }; + }; + }; + }; + }; + }; + + # Inject the secret as an environment variable into the webhook service + systemd.services.webhook.serviceConfig.EnvironmentFile = config.sops.secrets."webhook-secret".path; + + # 3. Builder Configuration + sops.secrets."builder-ssh-key" = lib.mkIf cfg.useBuilder {}; + + nix.buildMachines = lib.mkIf cfg.useBuilder [ + { + hostName = "builder.depeuter.dev"; # Must be routable from nodes + system = "x86_64-linux"; + sshUser = "builder"; + sshKey = config.sops.secrets."builder-ssh-key".path; + maxJobs = 4; + speedFactor = 2; + supportedFeatures = [ "nixos-test" "benchmark" "big-parallel" "kvm" ]; + } + ]; + + nix.distributedBuilds = lib.mkIf cfg.useBuilder true; + }; +} From 86b42b7f258b4efe8d320fb5b12ef53485cfe5d8 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Wed, 22 Jul 2026 21:23:39 +0200 Subject: [PATCH 60/76] docs: update README with project goals and quickstart --- README.md | 46 ++++++++++++++++++++-------------------------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index cfdb027..f2b7c8a 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,28 @@ -# NixOS GitOps Migration (v2) +# Bos55 Homelab (NixOS + Proxmox GitOps) -This branch contains the automated, pull-based GitOps architecture using `comin`, Terraform, and Forgejo. +This repository contains the declarative infrastructure-as-code for the Bos55 Homelab. It leverages NixOS, Flakes, Terraform, and a custom native pull-based GitOps architecture to fully automate the provisioning, configuration, and maintenance of a home server environment. -> [!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. +## Goals & Philosophy -## Secret Management (SOPS-Nix) +The primary goal of this project is **Zero-Login**. +Servers should be treated as cattle, not pets. If a configuration needs to change, it must be changed in this repository. -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. +* **Declarative Infrastructure:** Every application, user, reverse proxy rule, and network configuration is defined declaratively using custom NixOS modules. +* **Autonomous Pull-Based Deployments:** Nodes autonomously pull updates from this repository via a custom `homelab.gitops` systemd service, securely offloading compilation to a dedicated Build farm. +* **Instant Webhooks:** Merging a Pull Request instantly triggers deployments across the cluster via cryptographic webhooks. +* **Micro-segmented Security:** Ephemeral staging environments are completely isolated from production datasets using Proxmox hypervisor-level firewalls. +* **Secret Management:** Strict separation between production and staging secrets using `sops-nix` and `age`. -### 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. +## Quickstart -### 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. +If you are setting up this repository from scratch, you **must** perform the initial bootstrap before the automated pipelines can function. -### 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 +1. **Bootstrap Keys:** Follow the [BOOTSTRAP.md](./BOOTSTRAP.md) guide to generate your offline `age` master keys and configure Proxmox API access. +2. **Deploy Builder:** Ensure the dedicated Nix `Builder` host is running so other nodes can securely offload package compilation. +3. **Commit & Push:** Make your configuration changes to `flake.nix` or the `hosts/` directory, and push to the `v2` branch. +4. **Autonomous Deployment:** The nodes will automatically fetch the new hash and apply the configuration. -# 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. +## Documentation + +* [Bootstrap Guide](./BOOTSTRAP.md) - Initial setup instructions. +* [Disaster Recovery](./DISASTER_RECOVERY.md) - Protocols for handling hardware freezes and ZFS corruption. From d125848b82489c7901570113f66f5592df9417c8 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Fri, 24 Jul 2026 18:40:31 +0200 Subject: [PATCH 61/76] chore: add master keys --- .sops.yaml | 4 ++-- BOOTSTRAP.md | 19 ++++++++++--------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/.sops.yaml b/.sops.yaml index 5cba236..3bba812 100644 --- a/.sops.yaml +++ b/.sops.yaml @@ -3,8 +3,8 @@ 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 + - &prod_master age1pq12tgz8e980yvrsvd6c6ct6fa8y8eq0c8hjkdfvhp9k0phsdpjvewrrj42zsj9xhk2h2q6wmzvfnpjcgrm8x2zyzxjwv6urjpe60w89n9c2c0lw4j44qzr5j6ryxnmw8hysvngnf50s7ykrdrvy0aql9ktqqks2zzn48rd0k2mecw2snug23ydc4u4w6vphgusp3nrux9e8s6dsmf3vg7ngyc4ylgsht9fkmt6j4teq5y2sy9v9lye59ugztr29fw566dsdwu6vk3pw8dmelynkh8rg4fe88y3udntgwdcrzhun0j7kvjgxskvutyjlestwdfue27uqkdp94yhq0pgdxz8zs5tnxdnctzcf6v3yg2zlypesuzf8fatxxtt4564rr6uwdgjnepwv77gmw9erqp4hwtu40x4gxcyvz7pqpk724yd5g44ldh80fj53quhga0re7sqckmr2qfscdunqzu6s3am59t0p7dppw9jq0tfjnq4e8y78ywxhnjyct45g4q4jthn2yqh9gsep2cuuxq6pg3meye7qxs4y598t24su8ky8u25hfcfg9km5j5mf47vq9w8zu7kcaw74rqyq95wxdz84synckuut0ejt0yauzmxcyufjfmvgdx4xphvmf6882av97326hjfvr7qs72umjf6jq5jmhgmjz4vxj3j09ew4xxa3v92dcdqmwa3eshhq939c24l0ymv6lcf33wmcrr4ty869jl2yksjg33sc885q0wvrzn5wkd4ewgjmn660hgfgcs03r40wzugl7u7tssrunrs5du0scu077hc5vee2vxxtqrsgmmvg3h5usg8mcjk4yf8gnx6g6pzy076gv3ysxn6vspp3s6eyk2e39dhpj98r9u656kqa3c2tatkw3uucunru92d4z45yqp8q2wy4py3uyp0n9rsu2dv29xqvhnjh8tw324qc6favu59t7mryvz50d96sq2njfmsdqvqw3fvjxfvvrhldzllfasrlr69qt9ujtdeezycx7axwwc3jt5fhydfq7mcgdt4xuf8v4v6kk7y8fe5xxlyfyuhwlqlvpce0yjr2pnf264m5llgtprkpwa940f4n6ju4kgh0vnstw8lnw99pe9ta06pyccuqkdr6p6dkq3kk7v8mw2pxp0vzex0semjau6cewmtdlluqlkwxx9y6cuf0uaj3262qs640arlw99wfsxgzc3ev3r5fkqvy9v028gtr9mfs7s3yuyrug56qtdx0w76azyt3exfeetuqdxx9dm9k70er2ky9dqyjzfrd0ezesz55h8pqp2pltylyaptt7l9pf92qtj5cye7q2ezk2567v6hwus58rdrxefprcgshscg7ttc4hsact5c57zc6kqretwqs6dxk00qyznfz9uxzuuufekt275mxxcvf8ad4su08u3k5d56gptxcpjsg60d3qm7e4ryyqajxlq6kf4659jhvqv52fvs0ztv9zmrkz3y08hd9snurakf3fth9202gwur8gmrs3g4pdqunmrugk8kttsdxmhyegzwg46vcytuas4q0nsrjxanng6jp896szgtqm6xf5l3v86kyy5lxxtn2ht6j5wy5mkygmwus9fpzkrwqdzht3up28mesr6rfzj3pvepgdyq5nhsqzdrcu2u3xcwnx6ujvght6hgr4qg555dky50uvqxete2xcf4e6udh2d76ulakfe4uv3uxly93p4tpx4fdxgezyq9yxpmk2730akezna9fswmakxzg9446nft0ry2zy2ftnra9lr6m3vx96xszwd44rnrewt0x5fhjj248psmyztt899y28y4h2g5j3veq294np5wqd7vnn27zmkqxzadn5ylj0m3465t4fzzvrmmftdurlcxpxzj0zxmk2nhjw3h7ggtgvgzxe4wasjcfd4xflrzxs7ejp73 + - &staging_master age1w8tg8mpwj2ujxw0p9k36cpgecq285luwl4wf7a5tjtej4t2wffcq0gnzdw # Host Keys (Used by the specific VM to decrypt its own secrets at boot) # - &host_control_center age1... diff --git a/BOOTSTRAP.md b/BOOTSTRAP.md index 0db25b7..f1ceecb 100644 --- a/BOOTSTRAP.md +++ b/BOOTSTRAP.md @@ -2,28 +2,29 @@ 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) +## Secret Management Initialization 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 + age-keygen -o prod-master.age ``` > [!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. + > It is recommended to move `prod-master.age` to a secure offline USB drive and/or print it on paper. 2. **Generate the Staging Master Key:** ```bash - age-keygen -o staging-master.txt + age-keygen -o staging-master.age ``` 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. + Open both `.age` files and copy their **Public Keys** (the strings starting with `age1...`). Open `.sops.yaml` in the root of this repository and replace the placeholders with your newly generated public keys. Commit and push this change. -## 2. Proxmox Hypervisor Authentication & State +## Proxmox Hypervisor Authentication & State The CI/CD pipeline needs restricted API access to Proxmox to provision Virtual Machines. @@ -46,7 +47,7 @@ The CI/CD pipeline needs restricted API access to Proxmox to provision Virtual M 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 +## Forgejo Secrets Configuration The CI/CD actions require access to the Proxmox token and the staging secret key. @@ -56,7 +57,7 @@ The CI/CD actions require access to the Proxmox token and the staging secret key * `PROXMOX_TOKEN_SECRET`: Paste the token generated from Step 2. * `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. -## 4. Staging Golden Key Provisioning (Proxmox Snippet) +## Staging Golden Key Provisioning (Proxmox Snippet) Instead of relying on Forgejo CI/CD to store the staging private key, we use a secure hypervisor-level Cloud-Init snippet. @@ -76,7 +77,7 @@ Instead of relying on Forgejo CI/CD to store the staging private key, we use a s ``` 3. This completely removes the secret from Forgejo. When Terraform spins up a staging VM, it simply tells Proxmox to attach this local snippet! -## 5. TrueNAS API Security (RBAC) +## TrueNAS API Security (RBAC) To prevent the CI/CD pipeline from having `root` access to your TrueNAS server, you must run the RBAC bootstrap script to create a restricted user (`forgejo-ci`) that can *only* clone datasets for staging, not destroy production data. From 06500a8f019a4fd6f75f6793b051b63fcde6b9b9 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Fri, 24 Jul 2026 22:17:43 +0200 Subject: [PATCH 62/76] chore: split nixos and opentofu --- BOOTSTRAP.md | 68 ++++++++++++------- flake.lock => nixos/flake.lock | 0 flake.nix => nixos/flake.nix | 0 {hosts => nixos/hosts}/ACE/default.nix | 0 {hosts => nixos/hosts}/Binnenpost/default.nix | 0 {hosts => nixos/hosts}/Builder/default.nix | 0 .../hosts}/Development/default.nix | 0 {hosts => nixos/hosts}/Gitea/default.nix | 0 {hosts => nixos/hosts}/Ingress/default.nix | 0 {hosts => nixos/hosts}/Isabel/.keep | 0 .../Isabel/dashboard/config/bookmarks.yaml | 0 .../Isabel/dashboard/config/services.yaml | 0 {hosts => nixos/hosts}/Isabel/default.nix | 0 .../hosts}/Isabel/hardware-configuration.nix | 0 {hosts => nixos/hosts}/Niko/default.nix | 0 .../hosts}/Niko/hardware-configuration.nix | 0 {hosts => nixos/hosts}/Production/default.nix | 0 .../hosts}/ProductionArr/default.nix | 0 .../hosts}/ProductionGPU/default.nix | 0 {hosts => nixos/hosts}/Template/default.nix | 0 {hosts => nixos/hosts}/Testing/default.nix | 0 .../hosts}/Vaultwarden/default.nix | 0 .../modules}/apps/arr/default.nix | 0 .../modules}/apps/bind9/db.depeuter.dev | 0 .../modules}/apps/bind9/default.nix | 0 .../modules}/apps/bind9/named.conf | 0 .../modules}/apps/bind9/named.conf.local | 0 .../modules}/apps/bind9/named.conf.options | 0 .../modules}/apps/calibre/default.nix | 0 .../modules}/apps/changedetection/default.nix | 0 {modules => nixos/modules}/apps/default.nix | 0 .../modules}/apps/freshrss/default.nix | 0 .../modules}/apps/gitea/default.nix | 0 .../modules}/apps/homepage/default.nix | 0 .../modules}/apps/jellyfin/default.nix | 0 .../modules}/apps/monitoring/default.nix | 0 .../modules}/apps/plex/default.nix | 0 .../modules}/apps/solidtime/default.nix | 0 .../modules}/apps/speedtest/default.nix | 0 .../modules}/apps/technitium-dns/default.nix | 0 .../modules}/apps/traefik/default.nix | 0 .../modules}/apps/vaultwarden/default.nix | 0 {modules => nixos/modules}/common/default.nix | 0 {modules => nixos/modules}/common/docker.nix | 0 {modules => nixos/modules}/common/gitops.nix | 2 +- .../modules}/common/monitoring.nix | 0 {modules => nixos/modules}/common/nfs.nix | 0 {modules => nixos/modules}/common/traefik.nix | 0 {modules => nixos/modules}/common/users.nix | 0 {modules => nixos/modules}/default.nix | 0 .../modules}/fileSystems/default.nix | 0 .../modules}/fileSystems/media/default.nix | 0 .../fileSystems/media/video/default.nix | 0 .../modules}/services/actions/default.nix | 0 .../modules}/services/default.nix | 0 .../modules}/services/openssh/default.nix | 0 .../virtualisation/containers/default.nix | 0 .../modules}/virtualisation/default.nix | 0 .../modules}/virtualisation/guest/default.nix | 0 {users => nixos/users}/admin/default.nix | 0 {users => nixos/users}/apps/default.nix | 0 {users => nixos/users}/backup/default.nix | 0 {users => nixos/users}/default.nix | 0 {users => nixos/users}/deploy/default.nix | 0 {users => nixos/users}/media/default.nix | 0 .../proxmox-bootstrap/main.tf | 0 {terraform => opentofu}/staging-env/main.tf | 0 scripts/control-center-bootstrap.sh | 35 ---------- 68 files changed, 44 insertions(+), 61 deletions(-) rename flake.lock => nixos/flake.lock (100%) rename flake.nix => nixos/flake.nix (100%) rename {hosts => nixos/hosts}/ACE/default.nix (100%) rename {hosts => nixos/hosts}/Binnenpost/default.nix (100%) rename {hosts => nixos/hosts}/Builder/default.nix (100%) rename {hosts => nixos/hosts}/Development/default.nix (100%) rename {hosts => nixos/hosts}/Gitea/default.nix (100%) rename {hosts => nixos/hosts}/Ingress/default.nix (100%) rename {hosts => nixos/hosts}/Isabel/.keep (100%) rename {hosts => nixos/hosts}/Isabel/dashboard/config/bookmarks.yaml (100%) rename {hosts => nixos/hosts}/Isabel/dashboard/config/services.yaml (100%) rename {hosts => nixos/hosts}/Isabel/default.nix (100%) rename {hosts => nixos/hosts}/Isabel/hardware-configuration.nix (100%) rename {hosts => nixos/hosts}/Niko/default.nix (100%) rename {hosts => nixos/hosts}/Niko/hardware-configuration.nix (100%) rename {hosts => nixos/hosts}/Production/default.nix (100%) rename {hosts => nixos/hosts}/ProductionArr/default.nix (100%) rename {hosts => nixos/hosts}/ProductionGPU/default.nix (100%) rename {hosts => nixos/hosts}/Template/default.nix (100%) rename {hosts => nixos/hosts}/Testing/default.nix (100%) rename {hosts => nixos/hosts}/Vaultwarden/default.nix (100%) rename {modules => nixos/modules}/apps/arr/default.nix (100%) rename {modules => nixos/modules}/apps/bind9/db.depeuter.dev (100%) rename {modules => nixos/modules}/apps/bind9/default.nix (100%) rename {modules => nixos/modules}/apps/bind9/named.conf (100%) rename {modules => nixos/modules}/apps/bind9/named.conf.local (100%) rename {modules => nixos/modules}/apps/bind9/named.conf.options (100%) rename {modules => nixos/modules}/apps/calibre/default.nix (100%) rename {modules => nixos/modules}/apps/changedetection/default.nix (100%) rename {modules => nixos/modules}/apps/default.nix (100%) rename {modules => nixos/modules}/apps/freshrss/default.nix (100%) rename {modules => nixos/modules}/apps/gitea/default.nix (100%) rename {modules => nixos/modules}/apps/homepage/default.nix (100%) rename {modules => nixos/modules}/apps/jellyfin/default.nix (100%) rename {modules => nixos/modules}/apps/monitoring/default.nix (100%) rename {modules => nixos/modules}/apps/plex/default.nix (100%) rename {modules => nixos/modules}/apps/solidtime/default.nix (100%) rename {modules => nixos/modules}/apps/speedtest/default.nix (100%) rename {modules => nixos/modules}/apps/technitium-dns/default.nix (100%) rename {modules => nixos/modules}/apps/traefik/default.nix (100%) rename {modules => nixos/modules}/apps/vaultwarden/default.nix (100%) rename {modules => nixos/modules}/common/default.nix (100%) rename {modules => nixos/modules}/common/docker.nix (100%) rename {modules => nixos/modules}/common/gitops.nix (98%) rename {modules => nixos/modules}/common/monitoring.nix (100%) rename {modules => nixos/modules}/common/nfs.nix (100%) rename {modules => nixos/modules}/common/traefik.nix (100%) rename {modules => nixos/modules}/common/users.nix (100%) rename {modules => nixos/modules}/default.nix (100%) rename {modules => nixos/modules}/fileSystems/default.nix (100%) rename {modules => nixos/modules}/fileSystems/media/default.nix (100%) rename {modules => nixos/modules}/fileSystems/media/video/default.nix (100%) rename {modules => nixos/modules}/services/actions/default.nix (100%) rename {modules => nixos/modules}/services/default.nix (100%) rename {modules => nixos/modules}/services/openssh/default.nix (100%) rename {modules => nixos/modules}/virtualisation/containers/default.nix (100%) rename {modules => nixos/modules}/virtualisation/default.nix (100%) rename {modules => nixos/modules}/virtualisation/guest/default.nix (100%) rename {users => nixos/users}/admin/default.nix (100%) rename {users => nixos/users}/apps/default.nix (100%) rename {users => nixos/users}/backup/default.nix (100%) rename {users => nixos/users}/default.nix (100%) rename {users => nixos/users}/deploy/default.nix (100%) rename {users => nixos/users}/media/default.nix (100%) rename {terraform => opentofu}/proxmox-bootstrap/main.tf (100%) rename {terraform => opentofu}/staging-env/main.tf (100%) delete mode 100755 scripts/control-center-bootstrap.sh diff --git a/BOOTSTRAP.md b/BOOTSTRAP.md index f1ceecb..587138c 100644 --- a/BOOTSTRAP.md +++ b/BOOTSTRAP.md @@ -1,51 +1,64 @@ # 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. +This document outlines the manual steps required to initialize the NixOS GitOps environment on a fresh Proxmox host. You must perform these steps before the CI/CD pipeline or any automated staging environments can function. ## Secret Management Initialization -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. +We use `sops-nix` to manage secrets, separating between Production and Staging. **Prerequisites:** Install `age` ([age documentation](https://github.com/FiloSottile/age)). -1. **Generate the Production Master Key:** +1. **Generate the Production Master Key** ```bash age-keygen -o prod-master.age ``` - > [!CAUTION] - > It is recommended to move `prod-master.age` to a secure offline USB drive and/or print it on paper. -2. **Generate the Staging Master Key:** + It is recommended to generate this key on a secure offline workstation. The private key should be stored in a secure offline location, such as a USB drive or printed on paper. Avoid storing this private key on any server. + +2. **Generate the Staging Master Key** + ```bash age-keygen -o staging-master.age ``` -3. **Update Configuration:** - Open both `.age` files and copy their **Public Keys** (the strings starting with `age1...`). Open `.sops.yaml` in the root of this repository and replace the placeholders with your newly generated public keys. Commit and push this change. +3. **Update Configuration** -## Proxmox Hypervisor Authentication & State + Replace both placeholders in `.sops.yaml` with the newly generated **public keys**. Commit and push this change. -The CI/CD pipeline needs restricted API access to Proxmox to provision Virtual Machines. +## Proxmox Host Initialization & Authentication -### Fresh Install vs. Existing Install -> [!WARNING] -> This repository assumes a standard, fresh installation of Proxmox VE. -> -> **If you are NOT starting from a fresh install, be aware of these potential breaking changes:** -> * **VM ID Conflicts:** Terraform automatically assigns VM IDs. If you have existing VMs, Terraform might fail to provision or (if misconfigured) attempt to overwrite them. Check your Terraform variables to ensure the ID range (e.g., 8000+) does not conflict. -> * **Storage Pools:** The automation assumes the default Proxmox storage pools (`local` for snippets/ISOs, and `local-lvm` or `local-zfs` for VM disks). If you renamed your pools, you must update the Terraform configuration. -> * **Network Bridges:** It assumes `vmbr0` is available for VM networking. +The CI/CD pipeline needs restricted API access to Proxmox to provision Virtual Machines. We use OpenTofu to provision the bare-metal host. -**Prerequisites:** Install `terraform` ([Terraform installation](https://developer.hashicorp.com/terraform/downloads)). +### Find your Raw Disk ID + +The current OpenTofu configuration assumes that your system has a 2TB disk attached to the Proxmox host. This disk will be formatted and used for VM storage. + +1. Determine the persistent hardware ID of your 2TB disk, using the following command: -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 + ls -l /dev/disk/by-id/ ``` -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. + + It is recommended to use the persistent hardware ID (e.g., `wwn-0x...`, `nvme-eui...`, or `ata-...`) instead of `/dev/sdb` because the latter can change between reboots. + +2. **Action Required:** Provide this ID to the system or update the `opentofu/nodes/pve-new/main.tf` configuration with this ID. + +### Apply the OpenTofu Host State + +**Prerequisites:** Install `opentofu` ([OpenTofu installation](https://opentofu.org/docs/intro/install/)). + +1. Initialize and apply the state. You will be prompted to enter your `root@pam` Proxmox credentials. + + ```bash + # Navigate to the OpenTofu host directory + cd opentofu/host + # Initialize and apply the OpenTofu configuration + tofu init + tofu apply + ``` + +2. Upon successful completion, OpenTofu will output a secure **API Token**. Copy this token securely. ## Forgejo Secrets Configuration @@ -63,6 +76,7 @@ Instead of relying on Forgejo CI/CD to store the staging private key, we use a s 1. SSH into your Proxmox server (`pve`). 2. Create the Cloud-Init snippet file: + ```bash cat << 'EOF' > /var/lib/vz/snippets/staging-key.yaml #cloud-config @@ -75,7 +89,8 @@ Instead of relying on Forgejo CI/CD to store the staging private key, we use a s - echo "Staging age key injected successfully." EOF ``` -3. This completely removes the secret from Forgejo. When Terraform spins up a staging VM, it simply tells Proxmox to attach this local snippet! + +3. This completely removes the secret from Forgejo. When OpenTofu spins up a staging VM, it simply tells Proxmox to attach this local snippet! ## TrueNAS API Security (RBAC) @@ -83,13 +98,16 @@ To prevent the CI/CD pipeline from having `root` access to your TrueNAS server, 1. Ensure you have network access to your TrueNAS host. 2. Execute the RBAC setup script: + ```bash ./scripts/truenas-rbac-setup.sh ``` + 3. Provide your TrueNAS IP and the `root` Admin API Token when prompted. 4. The script will automatically create the custom `ci-runner-role` and the `forgejo-ci` user. 5. Follow the terminal output instructions to log into the TrueNAS Web UI as the new user and generate the restricted API token. 6. Use this restricted token for the `TRUENAS_API_KEY` secret in Forgejo. ## 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 and securely. diff --git a/flake.lock b/nixos/flake.lock similarity index 100% rename from flake.lock rename to nixos/flake.lock diff --git a/flake.nix b/nixos/flake.nix similarity index 100% rename from flake.nix rename to nixos/flake.nix diff --git a/hosts/ACE/default.nix b/nixos/hosts/ACE/default.nix similarity index 100% rename from hosts/ACE/default.nix rename to nixos/hosts/ACE/default.nix diff --git a/hosts/Binnenpost/default.nix b/nixos/hosts/Binnenpost/default.nix similarity index 100% rename from hosts/Binnenpost/default.nix rename to nixos/hosts/Binnenpost/default.nix diff --git a/hosts/Builder/default.nix b/nixos/hosts/Builder/default.nix similarity index 100% rename from hosts/Builder/default.nix rename to nixos/hosts/Builder/default.nix diff --git a/hosts/Development/default.nix b/nixos/hosts/Development/default.nix similarity index 100% rename from hosts/Development/default.nix rename to nixos/hosts/Development/default.nix diff --git a/hosts/Gitea/default.nix b/nixos/hosts/Gitea/default.nix similarity index 100% rename from hosts/Gitea/default.nix rename to nixos/hosts/Gitea/default.nix diff --git a/hosts/Ingress/default.nix b/nixos/hosts/Ingress/default.nix similarity index 100% rename from hosts/Ingress/default.nix rename to nixos/hosts/Ingress/default.nix diff --git a/hosts/Isabel/.keep b/nixos/hosts/Isabel/.keep similarity index 100% rename from hosts/Isabel/.keep rename to nixos/hosts/Isabel/.keep diff --git a/hosts/Isabel/dashboard/config/bookmarks.yaml b/nixos/hosts/Isabel/dashboard/config/bookmarks.yaml similarity index 100% rename from hosts/Isabel/dashboard/config/bookmarks.yaml rename to nixos/hosts/Isabel/dashboard/config/bookmarks.yaml diff --git a/hosts/Isabel/dashboard/config/services.yaml b/nixos/hosts/Isabel/dashboard/config/services.yaml similarity index 100% rename from hosts/Isabel/dashboard/config/services.yaml rename to nixos/hosts/Isabel/dashboard/config/services.yaml diff --git a/hosts/Isabel/default.nix b/nixos/hosts/Isabel/default.nix similarity index 100% rename from hosts/Isabel/default.nix rename to nixos/hosts/Isabel/default.nix diff --git a/hosts/Isabel/hardware-configuration.nix b/nixos/hosts/Isabel/hardware-configuration.nix similarity index 100% rename from hosts/Isabel/hardware-configuration.nix rename to nixos/hosts/Isabel/hardware-configuration.nix diff --git a/hosts/Niko/default.nix b/nixos/hosts/Niko/default.nix similarity index 100% rename from hosts/Niko/default.nix rename to nixos/hosts/Niko/default.nix diff --git a/hosts/Niko/hardware-configuration.nix b/nixos/hosts/Niko/hardware-configuration.nix similarity index 100% rename from hosts/Niko/hardware-configuration.nix rename to nixos/hosts/Niko/hardware-configuration.nix diff --git a/hosts/Production/default.nix b/nixos/hosts/Production/default.nix similarity index 100% rename from hosts/Production/default.nix rename to nixos/hosts/Production/default.nix diff --git a/hosts/ProductionArr/default.nix b/nixos/hosts/ProductionArr/default.nix similarity index 100% rename from hosts/ProductionArr/default.nix rename to nixos/hosts/ProductionArr/default.nix diff --git a/hosts/ProductionGPU/default.nix b/nixos/hosts/ProductionGPU/default.nix similarity index 100% rename from hosts/ProductionGPU/default.nix rename to nixos/hosts/ProductionGPU/default.nix diff --git a/hosts/Template/default.nix b/nixos/hosts/Template/default.nix similarity index 100% rename from hosts/Template/default.nix rename to nixos/hosts/Template/default.nix diff --git a/hosts/Testing/default.nix b/nixos/hosts/Testing/default.nix similarity index 100% rename from hosts/Testing/default.nix rename to nixos/hosts/Testing/default.nix diff --git a/hosts/Vaultwarden/default.nix b/nixos/hosts/Vaultwarden/default.nix similarity index 100% rename from hosts/Vaultwarden/default.nix rename to nixos/hosts/Vaultwarden/default.nix diff --git a/modules/apps/arr/default.nix b/nixos/modules/apps/arr/default.nix similarity index 100% rename from modules/apps/arr/default.nix rename to nixos/modules/apps/arr/default.nix diff --git a/modules/apps/bind9/db.depeuter.dev b/nixos/modules/apps/bind9/db.depeuter.dev similarity index 100% rename from modules/apps/bind9/db.depeuter.dev rename to nixos/modules/apps/bind9/db.depeuter.dev diff --git a/modules/apps/bind9/default.nix b/nixos/modules/apps/bind9/default.nix similarity index 100% rename from modules/apps/bind9/default.nix rename to nixos/modules/apps/bind9/default.nix diff --git a/modules/apps/bind9/named.conf b/nixos/modules/apps/bind9/named.conf similarity index 100% rename from modules/apps/bind9/named.conf rename to nixos/modules/apps/bind9/named.conf diff --git a/modules/apps/bind9/named.conf.local b/nixos/modules/apps/bind9/named.conf.local similarity index 100% rename from modules/apps/bind9/named.conf.local rename to nixos/modules/apps/bind9/named.conf.local diff --git a/modules/apps/bind9/named.conf.options b/nixos/modules/apps/bind9/named.conf.options similarity index 100% rename from modules/apps/bind9/named.conf.options rename to nixos/modules/apps/bind9/named.conf.options diff --git a/modules/apps/calibre/default.nix b/nixos/modules/apps/calibre/default.nix similarity index 100% rename from modules/apps/calibre/default.nix rename to nixos/modules/apps/calibre/default.nix diff --git a/modules/apps/changedetection/default.nix b/nixos/modules/apps/changedetection/default.nix similarity index 100% rename from modules/apps/changedetection/default.nix rename to nixos/modules/apps/changedetection/default.nix diff --git a/modules/apps/default.nix b/nixos/modules/apps/default.nix similarity index 100% rename from modules/apps/default.nix rename to nixos/modules/apps/default.nix diff --git a/modules/apps/freshrss/default.nix b/nixos/modules/apps/freshrss/default.nix similarity index 100% rename from modules/apps/freshrss/default.nix rename to nixos/modules/apps/freshrss/default.nix diff --git a/modules/apps/gitea/default.nix b/nixos/modules/apps/gitea/default.nix similarity index 100% rename from modules/apps/gitea/default.nix rename to nixos/modules/apps/gitea/default.nix diff --git a/modules/apps/homepage/default.nix b/nixos/modules/apps/homepage/default.nix similarity index 100% rename from modules/apps/homepage/default.nix rename to nixos/modules/apps/homepage/default.nix diff --git a/modules/apps/jellyfin/default.nix b/nixos/modules/apps/jellyfin/default.nix similarity index 100% rename from modules/apps/jellyfin/default.nix rename to nixos/modules/apps/jellyfin/default.nix diff --git a/modules/apps/monitoring/default.nix b/nixos/modules/apps/monitoring/default.nix similarity index 100% rename from modules/apps/monitoring/default.nix rename to nixos/modules/apps/monitoring/default.nix diff --git a/modules/apps/plex/default.nix b/nixos/modules/apps/plex/default.nix similarity index 100% rename from modules/apps/plex/default.nix rename to nixos/modules/apps/plex/default.nix diff --git a/modules/apps/solidtime/default.nix b/nixos/modules/apps/solidtime/default.nix similarity index 100% rename from modules/apps/solidtime/default.nix rename to nixos/modules/apps/solidtime/default.nix diff --git a/modules/apps/speedtest/default.nix b/nixos/modules/apps/speedtest/default.nix similarity index 100% rename from modules/apps/speedtest/default.nix rename to nixos/modules/apps/speedtest/default.nix diff --git a/modules/apps/technitium-dns/default.nix b/nixos/modules/apps/technitium-dns/default.nix similarity index 100% rename from modules/apps/technitium-dns/default.nix rename to nixos/modules/apps/technitium-dns/default.nix diff --git a/modules/apps/traefik/default.nix b/nixos/modules/apps/traefik/default.nix similarity index 100% rename from modules/apps/traefik/default.nix rename to nixos/modules/apps/traefik/default.nix diff --git a/modules/apps/vaultwarden/default.nix b/nixos/modules/apps/vaultwarden/default.nix similarity index 100% rename from modules/apps/vaultwarden/default.nix rename to nixos/modules/apps/vaultwarden/default.nix diff --git a/modules/common/default.nix b/nixos/modules/common/default.nix similarity index 100% rename from modules/common/default.nix rename to nixos/modules/common/default.nix diff --git a/modules/common/docker.nix b/nixos/modules/common/docker.nix similarity index 100% rename from modules/common/docker.nix rename to nixos/modules/common/docker.nix diff --git a/modules/common/gitops.nix b/nixos/modules/common/gitops.nix similarity index 98% rename from modules/common/gitops.nix rename to nixos/modules/common/gitops.nix index b2731e0..224ef4c 100644 --- a/modules/common/gitops.nix +++ b/nixos/modules/common/gitops.nix @@ -38,7 +38,7 @@ let echo "Hashes differ or unknown. Triggering nixos-rebuild..." # Trigger the build and switch - nixos-rebuild switch --flake "git+$REMOTE_URL?ref=$BRANCH" + nixos-rebuild switch --flake "git+$REMOTE_URL?dir=nixos&ref=$BRANCH" echo "Update successful." ''; diff --git a/modules/common/monitoring.nix b/nixos/modules/common/monitoring.nix similarity index 100% rename from modules/common/monitoring.nix rename to nixos/modules/common/monitoring.nix diff --git a/modules/common/nfs.nix b/nixos/modules/common/nfs.nix similarity index 100% rename from modules/common/nfs.nix rename to nixos/modules/common/nfs.nix diff --git a/modules/common/traefik.nix b/nixos/modules/common/traefik.nix similarity index 100% rename from modules/common/traefik.nix rename to nixos/modules/common/traefik.nix diff --git a/modules/common/users.nix b/nixos/modules/common/users.nix similarity index 100% rename from modules/common/users.nix rename to nixos/modules/common/users.nix diff --git a/modules/default.nix b/nixos/modules/default.nix similarity index 100% rename from modules/default.nix rename to nixos/modules/default.nix diff --git a/modules/fileSystems/default.nix b/nixos/modules/fileSystems/default.nix similarity index 100% rename from modules/fileSystems/default.nix rename to nixos/modules/fileSystems/default.nix diff --git a/modules/fileSystems/media/default.nix b/nixos/modules/fileSystems/media/default.nix similarity index 100% rename from modules/fileSystems/media/default.nix rename to nixos/modules/fileSystems/media/default.nix diff --git a/modules/fileSystems/media/video/default.nix b/nixos/modules/fileSystems/media/video/default.nix similarity index 100% rename from modules/fileSystems/media/video/default.nix rename to nixos/modules/fileSystems/media/video/default.nix diff --git a/modules/services/actions/default.nix b/nixos/modules/services/actions/default.nix similarity index 100% rename from modules/services/actions/default.nix rename to nixos/modules/services/actions/default.nix diff --git a/modules/services/default.nix b/nixos/modules/services/default.nix similarity index 100% rename from modules/services/default.nix rename to nixos/modules/services/default.nix diff --git a/modules/services/openssh/default.nix b/nixos/modules/services/openssh/default.nix similarity index 100% rename from modules/services/openssh/default.nix rename to nixos/modules/services/openssh/default.nix diff --git a/modules/virtualisation/containers/default.nix b/nixos/modules/virtualisation/containers/default.nix similarity index 100% rename from modules/virtualisation/containers/default.nix rename to nixos/modules/virtualisation/containers/default.nix diff --git a/modules/virtualisation/default.nix b/nixos/modules/virtualisation/default.nix similarity index 100% rename from modules/virtualisation/default.nix rename to nixos/modules/virtualisation/default.nix diff --git a/modules/virtualisation/guest/default.nix b/nixos/modules/virtualisation/guest/default.nix similarity index 100% rename from modules/virtualisation/guest/default.nix rename to nixos/modules/virtualisation/guest/default.nix diff --git a/users/admin/default.nix b/nixos/users/admin/default.nix similarity index 100% rename from users/admin/default.nix rename to nixos/users/admin/default.nix diff --git a/users/apps/default.nix b/nixos/users/apps/default.nix similarity index 100% rename from users/apps/default.nix rename to nixos/users/apps/default.nix diff --git a/users/backup/default.nix b/nixos/users/backup/default.nix similarity index 100% rename from users/backup/default.nix rename to nixos/users/backup/default.nix diff --git a/users/default.nix b/nixos/users/default.nix similarity index 100% rename from users/default.nix rename to nixos/users/default.nix diff --git a/users/deploy/default.nix b/nixos/users/deploy/default.nix similarity index 100% rename from users/deploy/default.nix rename to nixos/users/deploy/default.nix diff --git a/users/media/default.nix b/nixos/users/media/default.nix similarity index 100% rename from users/media/default.nix rename to nixos/users/media/default.nix diff --git a/terraform/proxmox-bootstrap/main.tf b/opentofu/proxmox-bootstrap/main.tf similarity index 100% rename from terraform/proxmox-bootstrap/main.tf rename to opentofu/proxmox-bootstrap/main.tf diff --git a/terraform/staging-env/main.tf b/opentofu/staging-env/main.tf similarity index 100% rename from terraform/staging-env/main.tf rename to opentofu/staging-env/main.tf diff --git a/scripts/control-center-bootstrap.sh b/scripts/control-center-bootstrap.sh deleted file mode 100755 index 1c12a73..0000000 --- a/scripts/control-center-bootstrap.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/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 89cec4e0f3256ffc37626b66deee46b0732d15c1 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Mon, 27 Jul 2026 20:25:39 +0200 Subject: [PATCH 63/76] feat(opentofu): define bare-metal host configuration --- BOOTSTRAP.md | 2 +- opentofu/modules/proxmox-node/main.tf | 123 +++++++++++++++++++++ opentofu/modules/proxmox-node/variables.tf | 15 +++ opentofu/nodes/mikoshi/main.tf | 30 +++++ opentofu/proxmox-bootstrap/main.tf | 71 ------------ 5 files changed, 169 insertions(+), 72 deletions(-) create mode 100644 opentofu/modules/proxmox-node/main.tf create mode 100644 opentofu/modules/proxmox-node/variables.tf create mode 100644 opentofu/nodes/mikoshi/main.tf delete mode 100644 opentofu/proxmox-bootstrap/main.tf diff --git a/BOOTSTRAP.md b/BOOTSTRAP.md index 587138c..dd788bd 100644 --- a/BOOTSTRAP.md +++ b/BOOTSTRAP.md @@ -42,7 +42,7 @@ The current OpenTofu configuration assumes that your system has a 2TB disk attac It is recommended to use the persistent hardware ID (e.g., `wwn-0x...`, `nvme-eui...`, or `ata-...`) instead of `/dev/sdb` because the latter can change between reboots. -2. **Action Required:** Provide this ID to the system or update the `opentofu/nodes/pve-new/main.tf` configuration with this ID. +2. **Action Required:** Provide this ID to the system or update the `opentofu/nodes//main.tf` configuration with this ID. ### Apply the OpenTofu Host State diff --git a/opentofu/modules/proxmox-node/main.tf b/opentofu/modules/proxmox-node/main.tf new file mode 100644 index 0000000..b495f66 --- /dev/null +++ b/opentofu/modules/proxmox-node/main.tf @@ -0,0 +1,123 @@ +terraform { + required_providers { + proxmox = { + source = "bpg/proxmox" + version = "~> 0.61.0" + } + } +} + +# Bare Metal Host Configurations (SSH Provisioning) + +# We use a null_resource to run imperative commands on the Debian host +# that are not currently supported by the bpg/proxmox provider. +resource "null_resource" "bare_metal_setup" { + triggers = { + node = var.node_name + data_disk_id = var.data_disk_id + pool_name = var.zfs_pool_name + } + + connection { + type = "ssh" + user = "root" + # Assuming running locally on the node during bootstrap, or via SSH if run from a laptop. + # We default to local host if run from Control Center, but for flexibility we use the endpoint. + host = var.node_name + agent = true + } + + provisioner "remote-exec" { + inline = [ + # Set laptop lid switch to ignore (prevents sleeping when closed) + "sed -i 's/^#\\?HandleLidSwitch=.*/HandleLidSwitch=ignore/' /etc/systemd/logind.conf", + "systemctl restart systemd-logind", + + # Format the ZFS pool if it doesn't already exist + "zpool list ${var.zfs_pool_name} || zpool create -f ${var.zfs_pool_name} /dev/disk/by-id/${var.data_disk_id}", + + # Register the ZFS pool in Proxmox if it's not already registered + "pvesm status -storage ${var.zfs_pool_name} || pvesm add zfspool ${var.zfs_pool_name} --pool ${var.zfs_pool_name} --content images,rootdir" + ] + } +} + +# Resource Pools + +# Creates logical groups for organizing and securing VMs. +resource "proxmox_virtual_environment_pool" "core" { + pool_id = "core" + comment = "Critical, public-facing services (reverse proxy, DNS)" +} + +resource "proxmox_virtual_environment_pool" "production" { + pool_id = "production" + comment = "Stable services with backup strategies" +} + +resource "proxmox_virtual_environment_pool" "dev" { + pool_id = "dev" + comment = "Persistent but non-production services, run 24/7" +} + +resource "proxmox_virtual_environment_pool" "staging" { + pool_id = "staging" + comment = "Ephemeral test VMs managed by Pull-Based PR webhooks" +} + +# API Tokens for Control Center + +# Creates a restricted user for the Control Center VM to manage the cluster. +resource "proxmox_virtual_environment_role" "control_center_role" { + role_id = "ControlCenter" + + 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" "control_center_user" { + user_id = "control-center@pve" + comment = "Managed by OpenTofu - Used by Control Center for GitOps" +} + +# Grant full VM access to the control center +resource "proxmox_virtual_environment_acl" "control_center_vms" { + user_id = proxmox_virtual_environment_user.control_center_user.user_id + role_id = proxmox_virtual_environment_role.control_center_role.role_id + path = "/vms" +} + +# Grant datastore access +resource "proxmox_virtual_environment_acl" "control_center_storage" { + user_id = proxmox_virtual_environment_user.control_center_user.user_id + role_id = proxmox_virtual_environment_role.control_center_role.role_id + path = "/storage/${var.zfs_pool_name}" +} + +resource "proxmox_virtual_environment_user_token" "control_center_token" { + user_id = proxmox_virtual_environment_user.control_center_user.user_id + token_id = "gitops" + privsep = false + comment = "Stored only on the Control Center VM" +} + +output "control_center_api_token" { + value = proxmox_virtual_environment_user_token.control_center_token.value + sensitive = true + description = "The secret API token for control-center@pve. This will be injected into the Control Center SOPS config." +} diff --git a/opentofu/modules/proxmox-node/variables.tf b/opentofu/modules/proxmox-node/variables.tf new file mode 100644 index 0000000..3bb24ed --- /dev/null +++ b/opentofu/modules/proxmox-node/variables.tf @@ -0,0 +1,15 @@ +variable "node_name" { + type = string + description = "The name of the Proxmox node (e.g. pve)" +} + +variable "data_disk_id" { + type = string + description = "The persistent block device ID for the data disk (e.g. wwn-0x500...)" +} + +variable "zfs_pool_name" { + type = string + default = "data" + description = "The name of the ZFS pool to create on the data disk" +} diff --git a/opentofu/nodes/mikoshi/main.tf b/opentofu/nodes/mikoshi/main.tf new file mode 100644 index 0000000..08c10a9 --- /dev/null +++ b/opentofu/nodes/mikoshi/main.tf @@ -0,0 +1,30 @@ +terraform { + required_providers { + proxmox = { + source = "bpg/proxmox" + version = "~> 0.61.0" + } + } +} + +provider "proxmox" { + # Endpoint and credentials will be picked up from environment variables + # or passed via the bootstrap script. + # PROXMOX_VE_ENDPOINT + # PROXMOX_VE_USERNAME + # PROXMOX_VE_PASSWORD + # PROXMOX_VE_INSECURE=true +} + +module "proxmox_node" { + source = "../../modules/proxmox-node" + node_name = "mikoshi" + data_disk_id = "nvme-KXG80ZNV2T04_NVMe_KIOXIA_2048GB_241C11Y5EHAK" + zfs_pool_name = "data" +} + +output "control_center_api_token" { + value = module.proxmox_node.control_center_api_token + sensitive = true + description = "The secret API token for control-center@pve." +} diff --git a/opentofu/proxmox-bootstrap/main.tf b/opentofu/proxmox-bootstrap/main.tf deleted file mode 100644 index 720d183..0000000 --- a/opentofu/proxmox-bootstrap/main.tf +++ /dev/null @@ -1,71 +0,0 @@ -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 dc9b7a30d92191c6471f71f3aa6d6b44761033dd Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Mon, 27 Jul 2026 22:01:40 +0200 Subject: [PATCH 64/76] feat(nixos): add control center host with hypervisor gitops service --- nixos/flake.nix | 2 +- nixos/hosts/izanagi/default.nix | 52 +++++++++++++++ nixos/hosts/izanagi/secrets.yaml | 5 ++ nixos/modules/common/gitops.nix | 42 +----------- nixos/modules/services/default.nix | 1 + .../services/hypervisor-gitops/default.nix | 64 +++++++++++++++++++ nixos/users/admin/default.nix | 2 +- opentofu/nodes/mikoshi/main.tf | 54 ++++++++++++++++ scripts/hypervisor-sync.sh | 39 +++++++++++ scripts/nixos-sync.sh | 40 ++++++++++++ 10 files changed, 259 insertions(+), 42 deletions(-) create mode 100644 nixos/hosts/izanagi/default.nix create mode 100644 nixos/hosts/izanagi/secrets.yaml create mode 100644 nixos/modules/services/hypervisor-gitops/default.nix create mode 100644 scripts/hypervisor-sync.sh create mode 100644 scripts/nixos-sync.sh diff --git a/nixos/flake.nix b/nixos/flake.nix index 5450b8d..bd6a5d1 100644 --- a/nixos/flake.nix +++ b/nixos/flake.nix @@ -70,7 +70,7 @@ }; hosts = { - # Hosts will be populated here as they are migrated to the v2 branch. + izanagi.modules = [ ./hosts/izanagi ]; }; }; } diff --git a/nixos/hosts/izanagi/default.nix b/nixos/hosts/izanagi/default.nix new file mode 100644 index 0000000..5403c50 --- /dev/null +++ b/nixos/hosts/izanagi/default.nix @@ -0,0 +1,52 @@ +{ config, pkgs, ... }: + +{ + networking.hostName = "izanagi"; + + homelab = { + # Enable the standard guest VM configuration + virtualisation.guest.enable = true; + + # Enable standard GitOps for the Control Center itself + common.gitops.enable = true; + # We might pull from our Forgejo instance eventually, but for bootstrap + # it might need to pull from Github or the local Gitea if it's up. + # common.gitops.repoUrl = "https://git.depeuter.dev/Bos55/nix-config.git"; + + services = { + openssh.enable = true; + + # Enable the Hypervisor GitOps service to provision OTHER VMs + hypervisor-gitops = { + enable = true; + repoUrl = "https://git.depeuter.dev/Bos55/nix-config.git"; + pollInterval = "hourly"; + }; + }; + }; + + sops = { + defaultSopsFile = ./secrets.yaml; + defaultSopsFormat = "yaml"; + age.keyFile = "/var/lib/sops-age/keys.txt"; # Injected via Cloud-Init during Phase 4 + + secrets = { + proxmox_api_token.owner = "root"; + forgejo_token.owner = "root"; + }; + + templates."hypervisor-gitops.env".content = '' + PROXMOX_VE_API_TOKEN=${config.sops.placeholder.proxmox_api_token} + FORGEJO_TOKEN=${config.sops.placeholder.forgejo_token} + PROXMOX_VE_ENDPOINT=https://mikoshi:8006/ + PROXMOX_VE_INSECURE=true + ''; + }; + + # Make the secrets available to the hypervisor-gitops service via EnvironmentFile + systemd.services.hypervisor-gitops.serviceConfig.EnvironmentFile = [ + config.sops.templates."hypervisor-gitops.env".path + ]; + + system.stateVersion = "24.05"; +} diff --git a/nixos/hosts/izanagi/secrets.yaml b/nixos/hosts/izanagi/secrets.yaml new file mode 100644 index 0000000..6aabf9e --- /dev/null +++ b/nixos/hosts/izanagi/secrets.yaml @@ -0,0 +1,5 @@ +# This is a placeholder SOPS file. +# You must encrypt it with sops using your age key before deploying. +# sops -e -i secrets.yaml +proxmox_api_token: "PLACEHOLDER_TOKEN" +forgejo_token: "PLACEHOLDER_TOKEN" diff --git a/nixos/modules/common/gitops.nix b/nixos/modules/common/gitops.nix index 224ef4c..c182365 100644 --- a/nixos/modules/common/gitops.nix +++ b/nixos/modules/common/gitops.nix @@ -6,42 +6,7 @@ let updateScript = pkgs.writeShellApplication { name = "homelab-gitops-update"; runtimeInputs = [ pkgs.git pkgs.nixos-rebuild pkgs.jq pkgs.coreutils ]; - text = '' - set -euo pipefail - - REMOTE_URL="${cfg.repoUrl}" - BRANCH="${cfg.branch}" - - echo "Checking remote hash for $REMOTE_URL branch $BRANCH..." - - # Fetch remote hash, fallback to unknown if it fails - REMOTE_HASH=$(git ls-remote "$REMOTE_URL" "refs/heads/$BRANCH" | awk '{print $1}' || true) - - if [ -z "$REMOTE_HASH" ]; then - echo "WARNING: Could not fetch remote hash. Forcing rebuild to be safe." - REMOTE_HASH="unknown_remote" - fi - - LOCAL_HASH="unknown_local" - if [ -f /run/current-system/configurationRevision ]; then - LOCAL_HASH=$(cat /run/current-system/configurationRevision) - fi - - echo "Remote hash: $REMOTE_HASH" - echo "Local hash: $LOCAL_HASH" - - if [ "$REMOTE_HASH" = "$LOCAL_HASH" ] && [ "$REMOTE_HASH" != "unknown_remote" ] && [ "$LOCAL_HASH" != "unknown" ]; then - echo "Hashes match. No update needed." - exit 0 - fi - - echo "Hashes differ or unknown. Triggering nixos-rebuild..." - - # Trigger the build and switch - nixos-rebuild switch --flake "git+$REMOTE_URL?dir=nixos&ref=$BRANCH" - - echo "Update successful." - ''; + text = builtins.readFile ../../../../scripts/nixos-sync.sh; }; in { @@ -68,14 +33,13 @@ in { }; config = lib.mkIf cfg.enable { - # 1. Systemd Service and Timer for polling systemd.services.homelab-gitops = { description = "Homelab GitOps Update Service"; wants = [ "network-online.target" ]; after = [ "network-online.target" ]; serviceConfig = { Type = "oneshot"; - ExecStart = "${updateScript}/bin/homelab-gitops-update"; + ExecStart = "${updateScript}/bin/homelab-gitops-update ${cfg.repoUrl} ${cfg.branch}"; # Must run as root to rebuild the system User = "root"; }; @@ -91,7 +55,6 @@ in { }; }; - # 2. Webhook listener for instant trigger sops.secrets."webhook-secret" = {}; services.webhook = { @@ -121,7 +84,6 @@ in { # Inject the secret as an environment variable into the webhook service systemd.services.webhook.serviceConfig.EnvironmentFile = config.sops.secrets."webhook-secret".path; - # 3. Builder Configuration sops.secrets."builder-ssh-key" = lib.mkIf cfg.useBuilder {}; nix.buildMachines = lib.mkIf cfg.useBuilder [ diff --git a/nixos/modules/services/default.nix b/nixos/modules/services/default.nix index f70bc54..ec5cb63 100644 --- a/nixos/modules/services/default.nix +++ b/nixos/modules/services/default.nix @@ -2,5 +2,6 @@ imports = [ ./actions ./openssh + ./hypervisor-gitops ]; } diff --git a/nixos/modules/services/hypervisor-gitops/default.nix b/nixos/modules/services/hypervisor-gitops/default.nix new file mode 100644 index 0000000..ac8f49f --- /dev/null +++ b/nixos/modules/services/hypervisor-gitops/default.nix @@ -0,0 +1,64 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.homelab.services.hypervisor-gitops; + + hypervisorSyncScript = pkgs.writeShellApplication { + name = "hypervisor-sync"; + runtimeInputs = with pkgs; [ git opentofu coreutils ]; + text = builtins.readFile ../../../../scripts/hypervisor-sync.sh; + }; +in { + options.homelab.services.hypervisor-gitops = { + enable = mkEnableOption "Hypervisor GitOps Service"; + + repoUrl = mkOption { + type = types.str; + description = "The URL of the git repository to pull"; + }; + + # TODO Replace with webhooks + pollInterval = mkOption { + type = types.str; + default = "hourly"; + description = "Systemd calendar event for polling interval"; + }; + }; + + config = mkIf cfg.enable { + environment.systemPackages = with pkgs; [ + git + opentofu + ]; + + systemd.services.hypervisor-gitops = { + description = "Hypervisor GitOps Polling Service"; + + # We need network access to reach Forgejo and Proxmox API + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + + serviceConfig = { + Type = "oneshot"; + User = "root"; # Needs root to read SOPS secrets potentially + + # We will create a state directory for the repo + StateDirectory = "hypervisor-gitops"; + WorkingDirectory = "/var/lib/hypervisor-gitops"; + + ExecStart = "${hypervisorSyncScript}/bin/hypervisor-sync ${cfg.repoUrl} opentofu/nodes/mikoshi"; + }; + }; + + systemd.timers.hypervisor-gitops = { + description = "Timer for Hypervisor GitOps Service"; + wantedBy = [ "timers.target" ]; + timerConfig = { + OnCalendar = cfg.pollInterval; + Persistent = true; + }; + }; + }; +} diff --git a/nixos/users/admin/default.nix b/nixos/users/admin/default.nix index dc01c81..65410ff 100644 --- a/nixos/users/admin/default.nix +++ b/nixos/users/admin/default.nix @@ -9,7 +9,7 @@ in { type = lib.types.listOf lib.types.str; default = [ # HomeLab > NixOS > admin > ssh - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGWIOOEqTy8cWKpENVbzD4p7bsQgQb/Dgpzk8i0dZ00T" + "sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIHOoTp+e6qWGn4Sco5CZ6G0zrX5NAQBpLlDVirncJ/HqAAAABHNzaDo=" ]; }; }; diff --git a/opentofu/nodes/mikoshi/main.tf b/opentofu/nodes/mikoshi/main.tf index 08c10a9..93dee38 100644 --- a/opentofu/nodes/mikoshi/main.tf +++ b/opentofu/nodes/mikoshi/main.tf @@ -28,3 +28,57 @@ output "control_center_api_token" { sensitive = true description = "The secret API token for control-center@pve." } + +resource "proxmox_virtual_environment_vm" "control_center" { + depends_on = [module.proxmox_node] + + name = "izanagi" + description = "Managed by OpenTofu - GitOps Control Center" + tags = ["infrastructure", "gitops"] + node_name = "mikoshi" + vm_id = 100001000 + + on_boot = true + + pool_id = "core" + + cpu { + cores = 2 + type = "x86-64-v2-AES" + } + + memory { + dedicated = 2048 + } + + agent { + enabled = true + } + + network_device { + bridge = "vmbr0" + } + + disk { + datastore_id = "data" + file_id = "local:iso/nixos-minimal.iso" # TODO Replace with actual ISO or use Clone + interface = "scsi0" + size = 20 + file_format = "raw" + } + + # Cloud-Init for initial SSH access and SOPS age key injection + initialization { + ip_config { + ipv4 { + address = "dhcp" + } + } + user_account { + username = "gh0st" + keys = [ + "sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIHOoTp+e6qWGn4Sco5CZ6G0zrX5NAQBpLlDVirncJ/HqAAAABHNzaDo=" + ] + } + } +} diff --git a/scripts/hypervisor-sync.sh b/scripts/hypervisor-sync.sh new file mode 100644 index 0000000..1a5536b --- /dev/null +++ b/scripts/hypervisor-sync.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash + +# This script pulls the latest changes from the Git repository +# and runs OpenTofu to provision the hypervisor state. +# Usage: ./hypervisor-sync.sh + +REPO_URL=${1:-"https://git.depeuter.dev/Bos55/nix-config.git"} +OPENTOFU_DIR=${2:-"opentofu/nodes/mikoshi"} + +echo "Starting Hypervisor GitOps sync..." + +if [ ! -d "nix-config" ]; then + echo "Cloning repository from $REPO_URL..." + git clone "$REPO_URL" nix-config +fi + +cd nix-config || exit + +git fetch origin main + +LOCAL=$(git rev-parse HEAD) +REMOTE=$(git rev-parse origin/main) + +if [ "$LOCAL" = "$REMOTE" ]; then + echo "Already up to date. Nothing to do." + exit 0 +fi + +echo "Changes detected. Updating from $LOCAL to $REMOTE..." +git reset --hard origin/main + +echo "Applying OpenTofu changes in $OPENTOFU_DIR..." +cd "$OPENTOFU_DIR" || exit + +tofu init -upgrade + +tofu apply -auto-approve + +echo "Hypervisor GitOps sync completed successfully." diff --git a/scripts/nixos-sync.sh b/scripts/nixos-sync.sh new file mode 100644 index 0000000..18f0c0f --- /dev/null +++ b/scripts/nixos-sync.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +# This script checks the remote Git repository for changes +# and triggers a nixos-rebuild if a new commit is found. +# Usage: ./nixos-sync.sh + +set -euo pipefail + +REPO_URL=${1:-"https://git.depeuter.dev/Bos55/nix-config.git"} +BRANCH=${2:-"main"} + +echo "Checking remote hash for $REPO_URL branch $BRANCH..." + +# Fetch remote hash, fallback to unknown if it fails +REMOTE_HASH=$(git ls-remote "$REPO_URL" "refs/heads/$BRANCH" | awk '{print $1}' || true) + +if [ -z "$REMOTE_HASH" ]; then + echo "WARNING: Could not fetch remote hash. Forcing rebuild to be safe." + REMOTE_HASH="unknown_remote" +fi + +LOCAL_HASH="unknown_local" +if [ -f /run/current-system/configurationRevision ]; then + LOCAL_HASH=$(cat /run/current-system/configurationRevision) +fi + +echo "Remote hash: $REMOTE_HASH" +echo "Local hash: $LOCAL_HASH" + +if [ "$REMOTE_HASH" = "$LOCAL_HASH" ] && [ "$REMOTE_HASH" != "unknown_remote" ] && [ "$LOCAL_HASH" != "unknown" ]; then + echo "Hashes match. No update needed." + exit 0 +fi + +echo "Hashes differ or unknown. Triggering nixos-rebuild..." + +# Trigger the build and switch +nixos-rebuild switch --flake "git+$REPO_URL?dir=nixos&ref=$BRANCH" + +echo "Update successful." From 2b533f43758cc0d3243758c8b48eb853ac9605ea Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Mon, 27 Jul 2026 22:01:47 +0200 Subject: [PATCH 65/76] feat(bootstrap): rewrite bootstrap script and docs for standalone deployment --- BOOTSTRAP.md | 131 +++++++++++-------------------------------- scripts/bootstrap.sh | 76 +++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 97 deletions(-) create mode 100644 scripts/bootstrap.sh diff --git a/BOOTSTRAP.md b/BOOTSTRAP.md index dd788bd..08e9e51 100644 --- a/BOOTSTRAP.md +++ b/BOOTSTRAP.md @@ -1,113 +1,50 @@ -# Bootstrap Guide +# NixOS GitOps Bootstrap Guide -This document outlines the manual steps required to initialize the NixOS GitOps environment on a fresh Proxmox host. You must perform these steps before the CI/CD pipeline or any automated staging environments can function. +This repository is designed to be fully automated once bootstrapped, but if you are adapting this codebase for **your own infrastructure**, you must modify several deployment-specific variables before running the bootstrap script on a fresh Proxmox host. -## Secret Management Initialization +## Adapt the Codebase -We use `sops-nix` to manage secrets, separating between Production and Staging. - -**Prerequisites:** Install `age` ([age documentation](https://github.com/FiloSottile/age)). - -1. **Generate the Production Master Key** - - ```bash - age-keygen -o prod-master.age - ``` - - It is recommended to generate this key on a secure offline workstation. The private key should be stored in a secure offline location, such as a USB drive or printed on paper. Avoid storing this private key on any server. - -2. **Generate the Staging Master Key** - - ```bash - age-keygen -o staging-master.age - ``` - -3. **Update Configuration** - - Replace both placeholders in `.sops.yaml` with the newly generated **public keys**. Commit and push this change. - -## Proxmox Host Initialization & Authentication - -The CI/CD pipeline needs restricted API access to Proxmox to provision Virtual Machines. We use OpenTofu to provision the bare-metal host. - -### Find your Raw Disk ID - -The current OpenTofu configuration assumes that your system has a 2TB disk attached to the Proxmox host. This disk will be formatted and used for VM storage. - -1. Determine the persistent hardware ID of your 2TB disk, using the following command: +Before bootstrapping your host, fork or clone this repository and make the following changes to match your environment: +### Hardware Identifiers +- **Find your NVMe/Disk UUID**: Log into your fresh Proxmox host and run: ```bash ls -l /dev/disk/by-id/ ``` + Identify your primary data disk (e.g. `nvme-eui...` or `wwn-0x...`). +- **Update OpenTofu Config**: Open `opentofu/nodes/mikoshi/main.tf` (you may want to rename `mikoshi` to your host's name) and replace the `disk` ID inside the `zpool` resource with your hardware UUID. - It is recommended to use the persistent hardware ID (e.g., `wwn-0x...`, `nvme-eui...`, or `ata-...`) instead of `/dev/sdb` because the latter can change between reboots. +### Identity & Access +- **SSH Keys**: The GitOps Control Center needs an SSH key for disaster recovery. + - Update the Cloud-Init SSH key in `opentofu/nodes/mikoshi/main.tf` under the `user_account` block. + - Update the permanent NixOS SSH key in `nixos/users/admin/default.nix`. +- **Secrets (SOPS)**: Replace the placeholder tokens in the Control Center host config (e.g., `nixos/hosts/izanagi/secrets.yaml` if you haven't renamed it) with your actual Proxmox API token and Forgejo token. Encrypt this file with your own `sops` Age key. -2. **Action Required:** Provide this ID to the system or update the `opentofu/nodes//main.tf` configuration with this ID. +### Hostname & Naming Schema +If your Proxmox host or your Control Center has a different name: +- Rename the folders in `opentofu/nodes/` and `nixos/hosts/`. +- Update the `node_name` inside your OpenTofu `main.tf`. +- Update `nixos/flake.nix` to reflect your new host names. -### Apply the OpenTofu Host State +## Execute the Bootstrap -**Prerequisites:** Install `opentofu` ([OpenTofu installation](https://opentofu.org/docs/intro/install/)). +Once you have pushed your adapted codebase to your Git server, SSH into your fresh Proxmox node as `root` and run the bootstrap script: -1. Initialize and apply the state. You will be prompted to enter your `root@pam` Proxmox credentials. +```bash +curl -fsSL https://git.your-server.com/your-repo/raw/branch/main/scripts/bootstrap.sh | bash +``` - ```bash - # Navigate to the OpenTofu host directory - cd opentofu/host - # Initialize and apply the OpenTofu configuration - tofu init - tofu apply - ``` +### What this script does automatically: +- **Fixes APT Repositories**: Disables enterprise repositories and adds community repositories. +- **Fixes NIC Offloading**: Installs a systemd service to safely disable TSO/GSO/GRO on physical interfaces to prevent network drops. +- **Installs OpenTofu**: Pulls the official binaries. +- **Applies Host State**: Runs `tofu apply` which: + - Formats your specified disk into the `data` ZFS pool. + - Sets laptop lid switch to ignore (if applicable). + - Spins up the GitOps Control Center VM. -2. Upon successful completion, OpenTofu will output a secure **API Token**. Copy this token securely. +## Post-Bootstrap -## Forgejo Secrets Configuration +Once the bootstrap script completes, the Control Center VM will boot, initialize via Cloud-Init, and automatically start pulling this git repository. -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. - * `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. - -## Staging Golden Key Provisioning (Proxmox Snippet) - -Instead of relying on Forgejo CI/CD to store the staging private key, we use a secure hypervisor-level Cloud-Init snippet. - -1. SSH into your Proxmox server (`pve`). -2. Create the Cloud-Init snippet file: - - ```bash - cat << 'EOF' > /var/lib/vz/snippets/staging-key.yaml - #cloud-config - write_files: - - path: /var/lib/sops-nix/key.txt - permissions: '0600' - content: | - AGE-SECRET-KEY-1... (paste your staging-master private key here) - runcmd: - - echo "Staging age key injected successfully." - EOF - ``` - -3. This completely removes the secret from Forgejo. When OpenTofu spins up a staging VM, it simply tells Proxmox to attach this local snippet! - -## TrueNAS API Security (RBAC) - -To prevent the CI/CD pipeline from having `root` access to your TrueNAS server, you must run the RBAC bootstrap script to create a restricted user (`forgejo-ci`) that can *only* clone datasets for staging, not destroy production data. - -1. Ensure you have network access to your TrueNAS host. -2. Execute the RBAC setup script: - - ```bash - ./scripts/truenas-rbac-setup.sh - ``` - -3. Provide your TrueNAS IP and the `root` Admin API Token when prompted. -4. The script will automatically create the custom `ci-runner-role` and the `forgejo-ci` user. -5. Follow the terminal output instructions to log into the TrueNAS Web UI as the new user and generate the restricted API token. -6. Use this restricted token for the `TRUENAS_API_KEY` secret in Forgejo. - -## 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 and securely. +From this point on, **you no longer need to log into the Proxmox host.** All future changes to VMs, networks, or applications should be done declaratively via Pull Requests to your repository! diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh new file mode 100644 index 0000000..d74a978 --- /dev/null +++ b/scripts/bootstrap.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +set -euo pipefail + +BANNER="===========================================================" +printf "%s\n Proxmox Bootstrap\n%s\n\n" "$BANNER" "$BANNER" + +echo "Applying post-pve-install fixes (fixing repos)..." +# Remove enterprise repos and add non-subscription repos safely +rm -f /etc/apt/sources.list.d/pve-enterprise.list + +echo "deb http://download.proxmox.com/debian/pve bookworm pve-no-subscription" > /etc/apt/sources.list.d/pve-no-subscription.list + +# Disable the "No Valid Subscription" nag screen +sed -i.bak "s/data.status !== 'Active'/false/g" /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js + +echo "" +echo "Applying NIC offloading fixes..." +# The community script disables TSO, GSO, and GRO on physical interfaces. +# We create a systemd service to ensure this applies on boot. +cat <<'EOF' > /etc/systemd/system/nic-offload-fix.service +[Unit] +Description=Disable NIC offloading (TSO/GRO/GSO) for physical interfaces +After=network-online.target + +[Service] +Type=oneshot +# Iterate over all physical interfaces (excluding lo, bridges, veth, etc.) +ExecStart=/bin/bash -c 'for dev in /sys/class/net/*; do if [ "$(basename "$dev")" != "lo" ] && [[ ! "$(basename "$dev")" =~ ^(vmbr|veth|fwbr|tap|bonding) ]]; then /usr/sbin/ethtool -K "$(basename "$dev")" tso off gso off gro off || true; fi; done' +RemainAfterExit=yes + +[Install] +WantedBy=multi-user.target +EOF +systemctl enable --now nic-offload-fix.service || true + +echo "" +echo "Updating system and installing OpenTofu..." +apt-get update +# Install curl, git, gnupg, ethtool, and required apt dependencies +apt-get install -y apt-transport-https ca-certificates curl git gnupg ethtool + +# Install OpenTofu repository and binary +install -m 0755 -d /etc/apt/keyrings +curl -fsSL https://get.opentofu.org/opentofu.gpg | tee /etc/apt/keyrings/opentofu.gpg >/dev/null +curl -fsSL https://packages.opentofu.org/opentofu/tofu/gpgkey | gpg --no-tty --batch --dearmor -o /etc/apt/keyrings/opentofu-repo.gpg >/dev/null +chmod a+r /etc/apt/keyrings/opentofu.gpg /etc/apt/keyrings/opentofu-repo.gpg + +printf "deb [signed-by=/etc/apt/keyrings/opentofu.gpg,/etc/apt/keyrings/opentofu-repo.gpg] https://packages.opentofu.org/opentofu/tofu/any/ any main\ndeb-src [signed-by=/etc/apt/keyrings/opentofu.gpg,/etc/apt/keyrings/opentofu-repo.gpg] https://packages.opentofu.org/opentofu/tofu/any/ any main\n" > /etc/apt/sources.list.d/opentofu.list +chmod a+r /etc/apt/sources.list.d/opentofu.list + +apt-get update +apt-get install -y tofu + +echo "" +echo "Cloning the infrastructure repository..." +cd /root +if [ -d "nix-config" ]; then + echo "Repository already exists. Pulling latest..." + cd nix-config + git pull +else + git clone https://git.depeuter.dev/Bos55/nix-config.git + cd nix-config +fi + +echo "" +echo "Bootstrapping Proxmox Host State..." +cd opentofu/nodes/mikoshi + +echo "Initializing OpenTofu..." +tofu init -upgrade + +echo "Applying bare-metal state..." +tofu apply -auto-approve + +printf "\n%s\n Bootstrap Complete!\n The ZFS pool, Resource Pools, and the Control Center\n VM have been provisioned.\n\n The Control Center is booting now. Once online, it will automatically\n pull this repository and provision the rest of your VMs!\n%s\n" "$BANNER" "$BANNER" From 2340c5a0e70cb4800f2373ee7740f15af60b611d Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Wed, 5 Aug 2026 18:13:15 +0200 Subject: [PATCH 66/76] feat(gitops): implement NATS JetStream and TrueNAS staging provisioning (Phase 5) --- .../services/hypervisor-gitops/default.nix | 54 ++++++++++++ opentofu/staging-env/main.tf | 19 ++++ scripts/staging-sync.sh | 87 +++++++++++++++++++ 3 files changed, 160 insertions(+) create mode 100644 scripts/staging-sync.sh diff --git a/nixos/modules/services/hypervisor-gitops/default.nix b/nixos/modules/services/hypervisor-gitops/default.nix index ac8f49f..2d9d05b 100644 --- a/nixos/modules/services/hypervisor-gitops/default.nix +++ b/nixos/modules/services/hypervisor-gitops/default.nix @@ -10,6 +10,36 @@ let runtimeInputs = with pkgs; [ git opentofu coreutils ]; text = builtins.readFile ../../../../scripts/hypervisor-sync.sh; }; + + stagingSyncScript = pkgs.writeShellApplication { + name = "staging-sync"; + runtimeInputs = with pkgs; [ opentofu coreutils jq curl ]; + text = builtins.readFile ../../../../scripts/staging-sync.sh; + }; + + natsConsumerScript = pkgs.writeShellApplication { + name = "nats-consumer"; + runtimeInputs = with pkgs; [ natscli jq stagingSyncScript ]; + text = '' + set -euo pipefail + + NATS_URL=''${NATS_URL:-"nats://192.168.0.20:4222"} + + echo "Starting NATS JetStream consumer for staging env..." + + # Try to create stream and consumer if they don't exist + nats --server "$NATS_URL" stream add FORGEJO_EVENTS --subjects "forgejo.staging" --ack --max-msgs=-1 --max-bytes=-1 --max-age=1y --storage file -f || true + nats --server "$NATS_URL" consumer add FORGEJO_EVENTS STAGING --pull --ack explicit --filter forgejo.staging --deliver all -f || true + + echo "Listening for messages..." + while true; do + # We use a simple sub to pull messages. In a real environment, + # a dedicated Go/Python client is better for manual explicit acks. + # This will auto-ack upon receipt and pass to the staging script. + nats --server "$NATS_URL" sub "forgejo.staging" | awk '/\[#.*\]/{flag=1; next} flag' | staging-sync || true + done + ''; + }; in { options.homelab.services.hypervisor-gitops = { enable = mkEnableOption "Hypervisor GitOps Service"; @@ -31,6 +61,8 @@ in { environment.systemPackages = with pkgs; [ git opentofu + natscli + jq ]; systemd.services.hypervisor-gitops = { @@ -60,5 +92,27 @@ in { Persistent = true; }; }; + + systemd.services.staging-sync = { + description = "Staging Environment NATS Consumer"; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + + serviceConfig = { + Type = "simple"; + User = "root"; # Needs root to read SOPS secrets + Restart = "always"; + RestartSec = "10s"; + + StateDirectory = "hypervisor-gitops"; + WorkingDirectory = "/var/lib/hypervisor-gitops"; + + # We assume TRUENAS_API_KEY is provided via a sops EnvironmentFile + # EnvironmentFile = config.sops.secrets."truenas-api-key".path; + + ExecStart = "${natsConsumerScript}/bin/nats-consumer"; + }; + }; }; } diff --git a/opentofu/staging-env/main.tf b/opentofu/staging-env/main.tf index b77ee4a..9538523 100644 --- a/opentofu/staging-env/main.tf +++ b/opentofu/staging-env/main.tf @@ -4,6 +4,10 @@ terraform { source = "bpg/proxmox" version = "~> 0.61.0" } + truenas = { + source = "deevus/truenas" + version = "~> 0.1.0" + } } } @@ -17,10 +21,25 @@ variable "pr_number" { type = string } +variable "truenas_snapshot_id" { + description = "The ID of the TrueNAS snapshot to clone" + type = string +} + provider "proxmox" { # Relies on PROXMOX_VE_ENDPOINT and PROXMOX_VE_API_TOKEN environment variables } +provider "truenas" { + # Relies on TRUENAS_API_KEY and TRUENAS_BASE_URL environment variables +} + +resource "truenas_dataset" "staging_clone" { + pool = "tank" + path = "production/staging-pr-${var.pr_number}" + snapshot_id = var.truenas_snapshot_id +} + resource "proxmox_virtual_environment_vm" "staging_vm" { name = "staging-pr-${var.pr_number}" description = "Ephemeral staging environment for PR #${var.pr_number}" diff --git a/scripts/staging-sync.sh b/scripts/staging-sync.sh new file mode 100644 index 0000000..eca15b5 --- /dev/null +++ b/scripts/staging-sync.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash + +# This script is triggered by the NATS JetStream consumer on the Control Center. +# It reads a Forgejo webhook JSON payload from STDIN and orchestrates the +# Staging VM OpenTofu lifecycle. + +set -euo pipefail + +# Read JSON payload from STDIN +PAYLOAD=$(cat) + +# Extract fields using jq +ACTION=$(echo "$PAYLOAD" | jq -r '.action // empty') +PR_NUMBER=$(echo "$PAYLOAD" | jq -r '.pull_request.number // empty') + +if [ -z "$ACTION" ] || [ -z "$PR_NUMBER" ] || [ "$PR_NUMBER" == "null" ]; then + printf "Invalid or missing action/pr_number in payload. Exiting.\n" + exit 0 +fi + +# We only care about PR events +if [[ "$ACTION" != "opened" && "$ACTION" != "reopened" && "$ACTION" != "synchronized" && "$ACTION" != "closed" ]]; then + printf "Ignoring PR action: %%s\n" "$ACTION" + exit 0 +fi + +# Ensure workspace directory exists for this PR +WORKSPACE="/var/lib/hypervisor-gitops/staging-pr-${PR_NUMBER}" +OPENTOFU_SRC="/var/lib/hypervisor-gitops/nix-config/opentofu/staging-env" + +# TrueNAS API variables +TRUENAS_URL=${TRUENAS_URL:-"https://192.168.0.11"} +TRUENAS_API_KEY=${TRUENAS_API_KEY:-""} +ZFS_DATASET=${ZFS_DATASET:-"tank/production"} + +get_latest_snapshot() { + # Fetch the latest snapshot for the dataset from TrueNAS API + # Assumes TRUENAS_API_KEY is exported in the environment by SOPS + curl -sS -k -X GET \ + -H "Authorization: Bearer ${TRUENAS_API_KEY}" \ + -H "Accept: application/json" \ + "${TRUENAS_URL}/api/v2.0/zfs/snapshot?id~=${ZFS_DATASET}%%25&limit=1&sort=-creation" | jq -r '.[0].id' +} + +printf "Processing PR #%%s (Action: %%s)\n" "$PR_NUMBER" "$ACTION" + +if [[ "$ACTION" == "closed" ]]; then + if [ ! -d "$WORKSPACE" ]; then + printf "Workspace %%s does not exist. Nothing to destroy.\n" "$WORKSPACE" + exit 0 + fi + printf "Destroying Staging Environment for PR #%%s...\n" "$PR_NUMBER" + cd "$WORKSPACE" + tofu destroy -var="pr_number=${PR_NUMBER}" -auto-approve + + # Cleanup + cd / + rm -rf "$WORKSPACE" + printf "Staging Environment Destroyed.\n" + exit 0 +fi + +# Provisioning (opened, reopened, synchronized) +printf "Setting up Staging Environment for PR #%%s...\n" "$PR_NUMBER" + +if [ ! -d "$WORKSPACE" ]; then + mkdir -p "$WORKSPACE" + cp -r "$OPENTOFU_SRC"/* "$WORKSPACE"/ +fi + +cd "$WORKSPACE" + +# Fetch latest TrueNAS snapshot dynamically +LATEST_SNAPSHOT=$(get_latest_snapshot) + +if [ -z "$LATEST_SNAPSHOT" ] || [ "$LATEST_SNAPSHOT" == "null" ]; then + printf "Failed to retrieve the latest TrueNAS snapshot. Aborting.\n" + exit 1 +fi + +printf "Latest Snapshot ID: %%s\n" "$LATEST_SNAPSHOT" + +# Apply OpenTofu +tofu init -upgrade +tofu apply -var="pr_number=${PR_NUMBER}" -var="truenas_snapshot_id=${LATEST_SNAPSHOT}" -auto-approve + +printf "Staging Environment Provisioned successfully.\n" From 41f5e6bae860f1998e70724cad805224b2f92d5f Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Wed, 5 Aug 2026 19:39:51 +0200 Subject: [PATCH 67/76] feat(gitea): add native NATS JetStream and webhook bridge --- nixos/flake.nix | 1 + nixos/modules/apps/gitea/default.nix | 45 ++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/nixos/flake.nix b/nixos/flake.nix index bd6a5d1..53487fa 100644 --- a/nixos/flake.nix +++ b/nixos/flake.nix @@ -71,6 +71,7 @@ hosts = { izanagi.modules = [ ./hosts/izanagi ]; + Gitea.modules = [ ./hosts/Gitea ]; }; }; } diff --git a/nixos/modules/apps/gitea/default.nix b/nixos/modules/apps/gitea/default.nix index eeec8b6..0048728 100644 --- a/nixos/modules/apps/gitea/default.nix +++ b/nixos/modules/apps/gitea/default.nix @@ -21,6 +21,50 @@ in { options.homelab.apps.gitea.enable = lib.mkEnableOption "Gitea"; config = lib.mkIf cfg.enable { + services.nats = { + enable = true; + listenAddress = "0.0.0.0"; + port = 4222; + jetstream = true; + }; + + networking.firewall.allowedTCPPorts = [ 4222 9000 ]; + + systemd.services.gitea-webhook-bridge = { + description = "Gitea Webhook Bridge to NATS JetStream"; + after = [ "network.target" "nats.service" ]; + wants = [ "nats.service" ]; + wantedBy = [ "multi-user.target" ]; + path = with pkgs; [ natscli python3 ]; + script = '' + python3 -c ' +import http.server +import subprocess +import sys + +class WebhookHandler(http.server.BaseHTTPRequestHandler): + def do_POST(self): + content_length = int(self.headers.get("Content-Length", 0)) + body = self.rfile.read(content_length) + try: + subprocess.run(["nats", "pub", "--server=nats://127.0.0.1:4222", "forgejo.staging"], input=body, check=True) + self.send_response(200) + self.end_headers() + self.wfile.write(b"OK\n") + except Exception as e: + self.send_response(500) + self.end_headers() + self.wfile.write(str(e).encode("utf-8")) + + def log_message(self, format, *args): + sys.stderr.write("%s - - [%s] %s\n" % (self.client_address[0], self.log_date_time_string(), format%args)) + +server = http.server.ThreadingHTTPServer(("0.0.0.0", 9000), WebhookHandler) +server.serve_forever() +' + ''; + }; + homelab = { users = { apps.enable = true; @@ -438,6 +482,7 @@ in { # ... oath2_client # ... webhook + FORGEJO__webhook__ALLOWED_HOST_LIST = "192.168.0.0/16,127.0.0.0/8,host.docker.internal,*"; FORGEJO__mailer__ENABLED = "true"; # Buffer length of channel, keep it as it is if you don't know what it is. From c35e0ed40eb73f4ba5624facdb330997ca3440a7 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Wed, 5 Aug 2026 19:47:12 +0200 Subject: [PATCH 68/76] chore: update bootstrap flow to clone golden image and track v2 branch --- opentofu/nodes/mikoshi/main.tf | 9 +++------ scripts/bootstrap.sh | 2 +- scripts/hypervisor-sync.sh | 6 +++--- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/opentofu/nodes/mikoshi/main.tf b/opentofu/nodes/mikoshi/main.tf index 93dee38..7e7a512 100644 --- a/opentofu/nodes/mikoshi/main.tf +++ b/opentofu/nodes/mikoshi/main.tf @@ -59,12 +59,9 @@ resource "proxmox_virtual_environment_vm" "control_center" { bridge = "vmbr0" } - disk { - datastore_id = "data" - file_id = "local:iso/nixos-minimal.iso" # TODO Replace with actual ISO or use Clone - interface = "scsi0" - size = 20 - file_format = "raw" + clone { + vm_id = 9000 + full = true } # Cloud-Init for initial SSH access and SOPS age key injection diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh index d74a978..dcca622 100644 --- a/scripts/bootstrap.sh +++ b/scripts/bootstrap.sh @@ -59,7 +59,7 @@ if [ -d "nix-config" ]; then cd nix-config git pull else - git clone https://git.depeuter.dev/Bos55/nix-config.git + git clone -b v2 https://git.depeuter.dev/Bos55/nix-config.git cd nix-config fi diff --git a/scripts/hypervisor-sync.sh b/scripts/hypervisor-sync.sh index 1a5536b..dba7a6a 100644 --- a/scripts/hypervisor-sync.sh +++ b/scripts/hypervisor-sync.sh @@ -16,10 +16,10 @@ fi cd nix-config || exit -git fetch origin main +git fetch origin v2 LOCAL=$(git rev-parse HEAD) -REMOTE=$(git rev-parse origin/main) +REMOTE=$(git rev-parse origin/v2) if [ "$LOCAL" = "$REMOTE" ]; then echo "Already up to date. Nothing to do." @@ -27,7 +27,7 @@ if [ "$LOCAL" = "$REMOTE" ]; then fi echo "Changes detected. Updating from $LOCAL to $REMOTE..." -git reset --hard origin/main +git reset --hard origin/v2 echo "Applying OpenTofu changes in $OPENTOFU_DIR..." cd "$OPENTOFU_DIR" || exit From e56900934d52b1a29065a20b5778b5bb9b834164 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Wed, 5 Aug 2026 20:03:58 +0200 Subject: [PATCH 69/76] fix: typos and outdated docs --- BOOTSTRAP.md | 10 ++++++++++ nixos/hosts/izanagi/default.nix | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/BOOTSTRAP.md b/BOOTSTRAP.md index 08e9e51..13bfce2 100644 --- a/BOOTSTRAP.md +++ b/BOOTSTRAP.md @@ -26,6 +26,16 @@ If your Proxmox host or your Control Center has a different name: - Update the `node_name` inside your OpenTofu `main.tf`. - Update `nixos/flake.nix` to reflect your new host names. +### The Golden Image +Because the GitOps Control Center must be spun up fully configured without human intervention, you need a pre-built NixOS `.qcow2` image. +1. Build the golden image locally (requires Nix/WSL): + ```bash + nix run github:nix-community/nixos-generators -- --flake ./nixos#izanagi --format qcow + ``` +2. Upload the resulting `.qcow2` file to your Proxmox node. +3. Create a new VM in Proxmox with **ID 9000**. +4. Import the `.qcow2` as its disk and convert the VM into a **Template**. *(Ensure the template has Cloud-Init configured).* + ## Execute the Bootstrap Once you have pushed your adapted codebase to your Git server, SSH into your fresh Proxmox node as `root` and run the bootstrap script: diff --git a/nixos/hosts/izanagi/default.nix b/nixos/hosts/izanagi/default.nix index 5403c50..1d7e09c 100644 --- a/nixos/hosts/izanagi/default.nix +++ b/nixos/hosts/izanagi/default.nix @@ -8,10 +8,10 @@ virtualisation.guest.enable = true; # Enable standard GitOps for the Control Center itself - common.gitops.enable = true; + gitops.enable = true; # We might pull from our Forgejo instance eventually, but for bootstrap # it might need to pull from Github or the local Gitea if it's up. - # common.gitops.repoUrl = "https://git.depeuter.dev/Bos55/nix-config.git"; + # gitops.repoUrl = "https://git.depeuter.dev/Bos55/nix-config.git"; services = { openssh.enable = true; From a5ffe597f7260ce0c8a8fbfb6dedb53c14a77d71 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Wed, 5 Aug 2026 20:09:27 +0200 Subject: [PATCH 70/76] fix(gitea): updated nix options --- nixos/modules/apps/gitea/default.nix | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/nixos/modules/apps/gitea/default.nix b/nixos/modules/apps/gitea/default.nix index 0048728..0b90c3d 100644 --- a/nixos/modules/apps/gitea/default.nix +++ b/nixos/modules/apps/gitea/default.nix @@ -23,9 +23,10 @@ in { config = lib.mkIf cfg.enable { services.nats = { enable = true; - listenAddress = "0.0.0.0"; port = 4222; - jetstream = true; + jetstream = { + enable = true; + }; }; networking.firewall.allowedTCPPorts = [ 4222 9000 ]; From 367a63754fb5b7b448f9f1a5a3b68db8937bf8b0 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Wed, 5 Aug 2026 20:14:13 +0200 Subject: [PATCH 71/76] fix: move flake.nix --- nixos/flake.lock => flake.lock | 0 nixos/flake.nix => flake.nix | 8 ++++---- nixos/modules/common/gitops.nix | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) rename nixos/flake.lock => flake.lock (100%) rename nixos/flake.nix => flake.nix (92%) diff --git a/nixos/flake.lock b/flake.lock similarity index 100% rename from nixos/flake.lock rename to flake.lock diff --git a/nixos/flake.nix b/flake.nix similarity index 92% rename from nixos/flake.nix rename to flake.nix index 53487fa..b4bd66e 100644 --- a/nixos/flake.nix +++ b/flake.nix @@ -58,8 +58,8 @@ inherit system; modules = [ - ./modules - ./users + ./nixos/modules + ./nixos/users sops-nix.nixosModules.sops ({ config, pkgs, ... }: { @@ -70,8 +70,8 @@ }; hosts = { - izanagi.modules = [ ./hosts/izanagi ]; - Gitea.modules = [ ./hosts/Gitea ]; + izanagi.modules = [ ./nixos/hosts/izanagi ]; + Gitea.modules = [ ./nixos/hosts/Gitea ]; }; }; } diff --git a/nixos/modules/common/gitops.nix b/nixos/modules/common/gitops.nix index c182365..4bfaeee 100644 --- a/nixos/modules/common/gitops.nix +++ b/nixos/modules/common/gitops.nix @@ -6,7 +6,7 @@ let updateScript = pkgs.writeShellApplication { name = "homelab-gitops-update"; runtimeInputs = [ pkgs.git pkgs.nixos-rebuild pkgs.jq pkgs.coreutils ]; - text = builtins.readFile ../../../../scripts/nixos-sync.sh; + text = builtins.readFile ../../../scripts/nixos-sync.sh; }; in { From c613a28a457e832b0e7a1bda1b282a974fc6f242 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Wed, 5 Aug 2026 20:19:05 +0200 Subject: [PATCH 72/76] fix: incorrect relative path --- nixos/modules/apps/monitoring/default.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nixos/modules/apps/monitoring/default.nix b/nixos/modules/apps/monitoring/default.nix index a1bd265..686a8f4 100644 --- a/nixos/modules/apps/monitoring/default.nix +++ b/nixos/modules/apps/monitoring/default.nix @@ -2,10 +2,10 @@ { sops.secrets."grafana/admin_password" = { - sopsFile = ../../../secrets/prod/monitoring.yaml; + sopsFile = ../../../../secrets/prod/monitoring.yaml; }; sops.secrets."alertmanager/smtp_password" = { - sopsFile = ../../../secrets/prod/monitoring.yaml; + sopsFile = ../../../../secrets/prod/monitoring.yaml; }; # 1. Loki Log Storage From 58effd5966e55cd1a14ab679f0cbd920d48a42c3 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Wed, 5 Aug 2026 20:23:38 +0200 Subject: [PATCH 73/76] fix(monitoring): wrap in block --- nixos/modules/apps/monitoring/default.nix | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/nixos/modules/apps/monitoring/default.nix b/nixos/modules/apps/monitoring/default.nix index 686a8f4..3e79b25 100644 --- a/nixos/modules/apps/monitoring/default.nix +++ b/nixos/modules/apps/monitoring/default.nix @@ -1,7 +1,12 @@ { config, lib, pkgs, ... }: -{ - sops.secrets."grafana/admin_password" = { +let + cfg = config.homelab.apps.monitoring; +in { + options.homelab.apps.monitoring.enable = lib.mkEnableOption "Homelab Monitoring Stack"; + + config = lib.mkIf cfg.enable { + sops.secrets."grafana/admin_password" = { sopsFile = ../../../../secrets/prod/monitoring.yaml; }; sops.secrets."alertmanager/smtp_password" = { @@ -155,4 +160,5 @@ # Open firewall for Loki so agents can push logs networking.firewall.allowedTCPPorts = [ 3100 ]; + }; } From de45fc01036689f6a3c0c91b6e7df2fda7081b4a Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Wed, 5 Aug 2026 20:27:32 +0200 Subject: [PATCH 74/76] fix: agree with shellcheck --- scripts/staging-sync.sh | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/scripts/staging-sync.sh b/scripts/staging-sync.sh index eca15b5..0732afe 100644 --- a/scripts/staging-sync.sh +++ b/scripts/staging-sync.sh @@ -20,7 +20,7 @@ fi # We only care about PR events if [[ "$ACTION" != "opened" && "$ACTION" != "reopened" && "$ACTION" != "synchronized" && "$ACTION" != "closed" ]]; then - printf "Ignoring PR action: %%s\n" "$ACTION" + printf "Ignoring PR action: %s\n" "$ACTION" exit 0 fi @@ -39,17 +39,17 @@ get_latest_snapshot() { curl -sS -k -X GET \ -H "Authorization: Bearer ${TRUENAS_API_KEY}" \ -H "Accept: application/json" \ - "${TRUENAS_URL}/api/v2.0/zfs/snapshot?id~=${ZFS_DATASET}%%25&limit=1&sort=-creation" | jq -r '.[0].id' + "${TRUENAS_URL}/api/v2.0/zfs/snapshot?id~=${ZFS_DATASET}%25&limit=1&sort=-creation" | jq -r '.[0].id' } -printf "Processing PR #%%s (Action: %%s)\n" "$PR_NUMBER" "$ACTION" +printf "Processing PR #%s (Action: %s)\n" "$PR_NUMBER" "$ACTION" if [[ "$ACTION" == "closed" ]]; then if [ ! -d "$WORKSPACE" ]; then - printf "Workspace %%s does not exist. Nothing to destroy.\n" "$WORKSPACE" + printf "Workspace %s does not exist. Nothing to destroy.\n" "$WORKSPACE" exit 0 fi - printf "Destroying Staging Environment for PR #%%s...\n" "$PR_NUMBER" + printf "Destroying Staging Environment for PR #%s...\n" "$PR_NUMBER" cd "$WORKSPACE" tofu destroy -var="pr_number=${PR_NUMBER}" -auto-approve @@ -61,7 +61,7 @@ if [[ "$ACTION" == "closed" ]]; then fi # Provisioning (opened, reopened, synchronized) -printf "Setting up Staging Environment for PR #%%s...\n" "$PR_NUMBER" +printf "Setting up Staging Environment for PR #%s...\n" "$PR_NUMBER" if [ ! -d "$WORKSPACE" ]; then mkdir -p "$WORKSPACE" @@ -78,7 +78,7 @@ if [ -z "$LATEST_SNAPSHOT" ] || [ "$LATEST_SNAPSHOT" == "null" ]; then exit 1 fi -printf "Latest Snapshot ID: %%s\n" "$LATEST_SNAPSHOT" +printf "Latest Snapshot ID: %s\n" "$LATEST_SNAPSHOT" # Apply OpenTofu tofu init -upgrade From 35a06a6c9350a30825f9ef8fb19ed2627667666a Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Wed, 5 Aug 2026 20:31:16 +0200 Subject: [PATCH 75/76] fix: add additional secrets placeholders --- nixos/hosts/izanagi/secrets.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nixos/hosts/izanagi/secrets.yaml b/nixos/hosts/izanagi/secrets.yaml index 6aabf9e..76b9924 100644 --- a/nixos/hosts/izanagi/secrets.yaml +++ b/nixos/hosts/izanagi/secrets.yaml @@ -3,3 +3,5 @@ # sops -e -i secrets.yaml proxmox_api_token: "PLACEHOLDER_TOKEN" forgejo_token: "PLACEHOLDER_TOKEN" +builder-ssh-key: "PLACEHOLDER_KEY" +webhook-secret: "PLACEHOLDER_SECRET" From 6c880fb9ce699f16ee353fa3274d1cb4709cc43e Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Wed, 5 Aug 2026 21:18:26 +0200 Subject: [PATCH 76/76] fix: switch arguments --- opentofu/modules/proxmox-node/main.tf | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/opentofu/modules/proxmox-node/main.tf b/opentofu/modules/proxmox-node/main.tf index b495f66..a0fc0f7 100644 --- a/opentofu/modules/proxmox-node/main.tf +++ b/opentofu/modules/proxmox-node/main.tf @@ -110,10 +110,9 @@ resource "proxmox_virtual_environment_acl" "control_center_storage" { } resource "proxmox_virtual_environment_user_token" "control_center_token" { - user_id = proxmox_virtual_environment_user.control_center_user.user_id - token_id = "gitops" - privsep = false - comment = "Stored only on the Control Center VM" + user_id = proxmox_virtual_environment_user.control_center_user.user_id + token_name = "gitops" + comment = "Stored only on the Control Center VM" } output "control_center_api_token" {