From 95e67dd1518a199291d0af661cda96904790abb4 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Fri, 17 Jul 2026 23:29:17 +0200 Subject: [PATCH] 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; + }; +}