diff --git a/machines/porthos/services.nix b/machines/porthos/services.nix index ac33819..cc8672d 100644 --- a/machines/porthos/services.nix +++ b/machines/porthos/services.nix @@ -90,6 +90,9 @@ in enable = true; password = my.secrets.nextcloud.password; }; + nginx = { + enable = true; # FIXME: remove this when done migrating + }; # The whole *arr software suite pirate.enable = true; # Podcast automatic downloader diff --git a/modules/services/nginx.nix b/modules/services/nginx.nix index ac70c48..2a64c65 100644 --- a/modules/services/nginx.nix +++ b/modules/services/nginx.nix @@ -1,12 +1,147 @@ -# Configuration shamelessly stolen from [1] -# -# [1]: https://github.com/delroth/infra.delroth.net/blob/master/common/nginx.nix +# A simple abstraction layer for almost all of my services' needs { config, lib, pkgs, ... }: +let + cfg = config.my.services.nginx; + virtualHostOption = with lib; types.submodule { + options = { + subdomain = mkOption { + type = types.str; + example = "dev"; + description = '' + Which subdomain, under config.networking.domain, to use + for this virtual host. + ''; + }; + + port = mkOption { + type = with types; nullOr port; + default = null; + example = 8080; + description = '' + Which port to proxy to, through 127.0.0.1, for this virtual host. + This option is incompatible with `root`. + ''; + }; + + root = mkOption { + type = with types; nullOr path; + default = null; + example = "/var/www/blog"; + description = '' + The root folder for this virtual host. This option is incompatible + with `port`. + ''; + }; + + extraConfig = mkOption { + type = types.attrs; # FIXME: forward type of virtualHosts + example = litteralExample '' + { + locations."/socket" = { + proxyPass = "http://127.0.0.1:8096/"; + proxyWebsockets = true; + }; + } + ''; + default = { }; + description = '' + Any extra configuration that should be applied to this virtual host. + ''; + }; + }; + }; +in { - # Whenever something defines an nginx vhost, ensure that nginx defaults are - # properly set. - config = lib.mkIf ((builtins.attrNames config.services.nginx.virtualHosts) != [ ]) { + options.my.services.nginx = with lib; { + enable = + mkEnableOption "Nginx, activates when `virtualHosts` is not empty" // { + default = builtins.length cfg.virtualHosts != 0; + }; + + monitoring = { + enable = my.mkDisableOption "monitoring through grafana and prometheus"; + }; + + virtualHosts = mkOption { + type = types.listOf virtualHostOption; + default = [ ]; + example = litteralExample '' + [ + { + subdomain = "gitea"; + port = 8080; + } + { + subdomain = "dev"; + root = "/var/www/dev"; + } + { + subdomain = "jellyfin"; + port = 8096; + extraConfig = { + locations."/socket" = { + proxyPass = "http://127.0.0.1:8096/"; + proxyWebsockets = true; + }; + }; + } + ] + ''; + description = '' + List of virtual hosts to set-up using default settings. + ''; + }; + }; + + config = lib.mkIf cfg.enable { + assertions = [ ] + ++ (lib.flip builtins.map cfg.virtualHosts ({ subdomain, ... } @ args: + let + conflicts = [ "port" "root" ]; + optionsNotNull = builtins.map (v: args.${v} != null) conflicts; + optionsSet = lib.filter lib.id optionsNotNull; + in + { + assertion = builtins.length optionsSet == 1; + message = '' + Subdomain '${subdomain}' must have exactly one of ${ + lib.concatStringsSep ", " (builtins.map (v: "'${v}'") conflicts) + } configured. + ''; + })) + ++ ( + let + ports = lib.my.mapFilter + (v: v != null) + ({ port, ... }: port) + cfg.virtualHosts; + portCounts = lib.my.countValues ports; + nonUniquesCounts = lib.filterAttrs (_: v: v != 1) portCounts; + nonUniques = builtins.attrNames nonUniquesCounts; + mkAssertion = port: { + assertion = false; + message = "Port ${port} cannot appear in multiple virtual hosts."; + }; + in + map mkAssertion nonUniques + ) ++ ( + let + subs = map ({ subdomain, ... }: subdomain) cfg.virtualHosts; + subsCounts = lib.my.countValues subs; + nonUniquesCounts = lib.filterAttrs (_: v: v != 1) subsCounts; + nonUniques = builtins.attrNames nonUniquesCounts; + mkAssertion = v: { + assertion = false; + message = '' + Subdomain '${v}' cannot appear in multiple virtual hosts. + ''; + }; + in + map mkAssertion nonUniques + ) + ; + services.nginx = { enable = true; statusPage = true; # For monitoring scraping. @@ -15,6 +150,33 @@ recommendedOptimisation = true; recommendedTlsSettings = true; recommendedProxySettings = true; + + virtualHosts = + let + domain = config.networking.domain; + mkVHost = ({ subdomain, ... } @ args: lib.nameValuePair + "${subdomain}.${domain}" + (builtins.foldl' lib.recursiveUpdate { } [ + # Base configuration + { + forceSSL = true; + useACMEHost = domain; + } + # Proxy to port + (lib.optionalAttrs (args.port != null) { + locations."/".proxyPass = + "http://127.0.0.1:${toString args.port}"; + }) + # Serve filesystem content + (lib.optionalAttrs (args.root != null) { + inherit (args) root; + }) + # VHost specific configuration + args.extraConfig + ]) + ); + in + lib.my.genAttrs' cfg.virtualHosts mkVHost; }; networking.firewall.allowedTCPPorts = [ 80 443 ]; @@ -22,10 +184,10 @@ # Nginx needs to be able to read the certificates users.users.nginx.extraGroups = [ "acme" ]; - # Use DNS wildcard certificate security.acme = { email = "bruno.acme@belanyi.fr"; acceptTerms = true; + # Use DNS wildcard certificate certs = let domain = config.networking.domain; @@ -40,8 +202,8 @@ }; }; }; - # Setup monitoring - services.grafana.provision.dashboards = [ + + services.grafana.provision.dashboards = lib.mkIf cfg.monitoring.enable [ { name = "NGINX"; options.path = pkgs.nur.repos.alarsyo.grafanaDashboards.nginx; @@ -49,7 +211,7 @@ } ]; - services.prometheus = { + services.prometheus = lib.mkIf cfg.monitoring.enable { exporters.nginx = { enable = true; listenAddress = "127.0.0.1";