refactor: abstract traefik router labels and dynamic config

This commit is contained in:
Tibo De Peuter 2026-07-17 23:29:17 +02:00
parent d742671460
commit 95e67dd151
Signed by: tdpeuter
SSH key fingerprint: SHA256:u/h/LVoqKF1Iz02uOyxe6hcjmoZASCGV2HM0TG9ZMoU
9 changed files with 152 additions and 74 deletions

View file

@ -110,6 +110,29 @@ in {
sonarr = lib.mkIf cfg.sonarr.enable (mkUser 3007); 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 virtualisation.oci-containers.containers = let
videoHostPath = config.homelab.fileSystems.media.video.hostPath; videoHostPath = config.homelab.fileSystems.media.video.hostPath;
in { in {
@ -141,12 +164,6 @@ in {
"${videoHostPath}/Films:/media/movies" "${videoHostPath}/Films:/media/movies"
"${videoHostPath}/Series:/media/series" "${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 prowlarr = let
@ -172,12 +189,6 @@ in {
"/srv/prowlarr-backup:/config/Backups" "/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 qbittorrent = let
@ -205,12 +216,6 @@ in {
"/srv/torrent:/media/cache" "/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 radarr = let
@ -239,12 +244,6 @@ in {
"/srv/torrent:/media/cache" "/srv/torrent:/media/cache"
"${videoHostPath}/Films:/media/movies" "${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 sonarr = let
@ -273,12 +272,6 @@ in {
"/srv/torrent:/media/cache" "/srv/torrent:/media/cache"
"${videoHostPath}/Series:/media/series" "${videoHostPath}/Series:/media/series"
]; ];
labels = {
"traefik.enable" = "true";
"traefik.docker.network" = proxyNet;
"traefik.http.routers.sonarr.rule" = "Host(`sonarr.depeuter.dev`)";
"traefik.http.services.sonarr.loadbalancer.server.port" = toString port;
};
}; };
}; };
}; };

View file

@ -55,6 +55,16 @@ in {
"docker-calibre.service" "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 # Calibre desktop
@ -102,12 +112,6 @@ in {
"${books}:/media/books" "${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" "${books}:/media/books"
]; ];
labels = {
"traefik.enable" = "true";
"traefik.docker.network" = proxyNet;
"traefik.http.routers.calibre-web.rule" = "Host(`books.depeuter.dev`)";
"traefik.http.services.calibre-web.loadbalancer.server.port" = toString innerPort;
};
}; };
}) })
]; ];

View file

@ -4,6 +4,7 @@ let
cfg = config.homelab.apps.freshrss; cfg = config.homelab.apps.freshrss;
networkName = "freshrss"; networkName = "freshrss";
proxyNet = config.homelab.apps.traefik.sharedNetworkName;
in { in {
options.homelab.apps.freshrss = { options.homelab.apps.freshrss = {
enable = lib.mkEnableOption "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 = { virtualisation.oci-containers.containers.freshrss = {
hostname = "freshrss"; hostname = "freshrss";
image = "freshrss/freshrss:1.25.0"; image = "freshrss/freshrss:1.25.0";
@ -46,6 +63,7 @@ in {
]; ];
extraOptions = [ extraOptions = [
"--network=${networkName}" "--network=${networkName}"
"--network=${proxyNet}"
]; ];
environment = { environment = {
TZ = config.time.timeZone; TZ = config.time.timeZone;
@ -57,22 +75,6 @@ in {
"/srv/freshrss/www/freshrss/data:/var/www/FreshRSS/data" "/srv/freshrss/www/freshrss/data:/var/www/FreshRSS/data"
"/srv/freshrss/www/freshrss/extensions:/var/www/FreshRSS/extensions" "/srv/freshrss/www/freshrss/extensions:/var/www/FreshRSS/extensions"
]; ];
labels = {
"traefik.enable" = "true";
"traefik.http.middlewares.freshrssM1.compress" = "true";
"traefik.http.middlewares.freshrssM2.headers.browserXssFilter" = "true";
"traefik.http.middlewares.freshrssM2.headers.forceSTSHeader" = "true";
"traefik.http.middlewares.freshrssM2.headers.frameDeny" = "true";
"traefik.http.middlewares.freshrssM2.headers.referrerPolicy" = "no-referrer-when-downgrade";
"traefik.http.middlewares.freshrssM2.headers.stsSeconds" = "31536000";
"traefik.http.routers.freshrss.entryPoints" = "websecure";
"traefik.http.routers.freshrss.tls" = "true";
"traefik.http.services.freshrss.loadbalancer.server.port" = "80";
"traefik.http.routers.freshrss.middlewares" = "freshrssM1,freshrssM2";
"traefik.http.routers.freshrss.rule" = "Host(`rss.depeuter.dev`)";
};
}; };
}; };
} }

View file

@ -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 virtualisation.oci-containers.containers.homepage = let
host = "homepage.${config.networking.domain}"; host = "homepage.${config.networking.domain}";
in { in {
@ -54,12 +61,6 @@ in {
"${homepage-config}:/app/config" "${homepage-config}:/app/config"
# "/var/run/docker.sock:/var/run/docker.sock:ro" # For docker integrations # "/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 = { environment = {
inherit PUID PGID; inherit PUID PGID;

View file

@ -4,6 +4,7 @@ let
cfg = config.homelab.apps.jellyfin; cfg = config.homelab.apps.jellyfin;
networkName = "jellyfin"; networkName = "jellyfin";
proxyNet = config.homelab.apps.traefik.sharedNetworkName;
inherit (config.homelab.fileSystems) media; inherit (config.homelab.fileSystems) media;
UID = 3008; 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 = { virtualisation.oci-containers.containers = {
jellyfin = { jellyfin = {
hostname = "jellyfin"; hostname = "jellyfin";
@ -102,6 +111,7 @@ in {
]; ];
extraOptions = [ extraOptions = [
"--network=${networkName}" "--network=${networkName}"
"--network=${proxyNet}"
]; ];
environment = { environment = {
# pre defined server name # pre defined server name
@ -114,12 +124,6 @@ in {
SERVER_URL= "https://jelly.depeuter.dev"; SERVER_URL= "https://jelly.depeuter.dev";
TZ = config.time.timeZone; 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";
};
}; };
}; };
}; };

View file

@ -136,7 +136,7 @@
}; };
# 6. Traefik Reverse Proxy for Grafana and NTFY # 6. Traefik Reverse Proxy for Grafana and NTFY
services.traefik.dynamicConfigOptions.http = { homelab.apps.traefik.dynamicConfigOptions.http = {
routers = { routers = {
grafana = { grafana = {
rule = "Host(`grafana.lab.depeuter.dev`)"; rule = "Host(`grafana.lab.depeuter.dev`)";
@ -148,8 +148,8 @@
}; };
}; };
services = { services = {
grafana.loadBalancer.servers = [{ url = "http://127.0.0.1:3000"; }]; grafana.loadBalancer.servers = [{ url = "http://host.docker.internal:3000"; }];
ntfy.loadBalancer.servers = [{ url = "http://127.0.0.1:2586"; }]; ntfy.loadBalancer.servers = [{ url = "http://host.docker.internal:2586"; }];
}; };
}; };

View file

@ -12,6 +12,11 @@ in {
default = "traefik"; default = "traefik";
description = "The name of the shared network to connect the container to."; 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 { config = lib.mkIf cfg.enable {
@ -35,6 +40,7 @@ in {
]; ];
extraOptions = [ extraOptions = [
"--network=${cfg.sharedNetworkName}" "--network=${cfg.sharedNetworkName}"
"--add-host=host.docker.internal:host-gateway"
]; ];
environmentFiles = [ environmentFiles = [
/home/admin/.cloudflare.secret /home/admin/.cloudflare.secret
@ -46,6 +52,10 @@ in {
"--providers.docker=true" "--providers.docker=true"
"--providers.docker.exposedByDefault=false" "--providers.docker.exposedByDefault=false"
# Add File provider
"--providers.file.filename=/etc/traefik/dynamic_conf.yml"
"--providers.file.watch=true"
# Add web entrypoint # Add web entrypoint
"--entrypoints.web.address=:80/tcp" "--entrypoints.web.address=:80/tcp"
"--entrypoints.web.http.redirections.entrypoint.to=websecure" "--entrypoints.web.http.redirections.entrypoint.to=websecure"
@ -65,10 +75,14 @@ in {
"--certificatesresolvers.letsencrypt.acme.email=tibo.depeuter@telenet.be" "--certificatesresolvers.letsencrypt.acme.email=tibo.depeuter@telenet.be"
"--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json" "--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" "letsencryp:/letsencrypt"
"/var/run/docker.sock:/var/run/docker.sock:ro" "/var/run/docker.sock:/var/run/docker.sock:ro"
"${dynamicConfFile}:/etc/traefik/dynamic_conf.yml:ro"
]; ];
labels = { labels = {
"traefik.enable" = "true"; "traefik.enable" = "true";

View file

@ -3,6 +3,7 @@
./docker.nix ./docker.nix
./monitoring.nix ./monitoring.nix
./nfs.nix ./nfs.nix
./traefik.nix
./users.nix ./users.nix
]; ];

View file

@ -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;
};
}