{ config, lib, pkgs, ... }: with lib; let cfg = config.services.caddy; vhostToConfig = vhostName: vhostAttrs: '' ${vhostName} ${builtins.concatStringsSep " " vhostAttrs.serverAliases} { ${vhostAttrs.extraConfig} } ''; configFile = pkgs.writeText "Caddyfile" (builtins.concatStringsSep "\n" ([ cfg.config ] ++ (mapAttrsToList vhostToConfig cfg.virtualHosts))); formattedConfig = pkgs.runCommand "formattedCaddyFile" { } '' ${cfg.package}/bin/caddy fmt ${configFile} > $out ''; tlsConfig = { apps.tls.automation.policies = [{ issuers = [{ inherit (cfg) ca email; module = "acme"; }]; }]; }; adaptedConfig = pkgs.runCommand "caddy-config-adapted.json" { } '' ${cfg.package}/bin/caddy adapt \ --config ${formattedConfig} --adapter ${cfg.adapter} > $out ''; tlsJSON = pkgs.writeText "tls.json" (builtins.toJSON tlsConfig); # merge the TLS config options we expose with the ones originating in the Caddyfile configJSON = if cfg.ca != null then let tlsConfigMerge = '' {"apps": {"tls": {"automation": {"policies": (if .[0].apps.tls.automation.policies == .[1]?.apps.tls.automation.policies then .[0].apps.tls.automation.policies else (.[0].apps.tls.automation.policies + .[1]?.apps.tls.automation.policies) end) } } } }''; in pkgs.runCommand "caddy-config.json" { } '' ${pkgs.jq}/bin/jq -s '.[0] * ${tlsConfigMerge}' ${adaptedConfig} ${tlsJSON} > $out '' else adaptedConfig; in { imports = [ (mkRemovedOptionModule [ "services" "caddy" "agree" ] "this option is no longer necessary for Caddy 2") ]; options.services.caddy = { enable = mkEnableOption "Caddy web server"; config = mkOption { default = ""; example = '' example.com { encode gzip log root /srv/http } ''; type = types.lines; description = '' Verbatim Caddyfile to use. Caddy v2 supports multiple config formats via adapters (see ). ''; }; virtualHosts = mkOption { type = types.attrsOf (types.submodule (import ./vhost-options.nix { inherit config lib; })); default = { }; example = literalExpression '' { "hydra.example.com" = { serverAliases = [ "www.hydra.example.com" ]; extraConfig = '''''' encode gzip log root /srv/http ''''''; }; }; ''; description = "Declarative vhost config"; }; user = mkOption { default = "caddy"; type = types.str; description = "User account under which caddy runs."; }; group = mkOption { default = "caddy"; type = types.str; description = "Group account under which caddy runs."; }; adapter = mkOption { default = "caddyfile"; example = "nginx"; type = types.str; description = '' Name of the config adapter to use. See https://caddyserver.com/docs/config-adapters for the full list. ''; }; resume = mkOption { default = false; type = types.bool; description = '' Use saved config, if any (and prefer over configuration passed with ). ''; }; ca = mkOption { default = "https://acme-v02.api.letsencrypt.org/directory"; example = "https://acme-staging-v02.api.letsencrypt.org/directory"; type = types.nullOr types.str; description = '' Certificate authority ACME server. The default (Let's Encrypt production server) should be fine for most people. Set it to null if you don't want to include any authority (or if you want to write a more fine-graned configuration manually) ''; }; email = mkOption { default = ""; type = types.str; description = "Email address (for Let's Encrypt certificate)"; }; dataDir = mkOption { default = "/var/lib/caddy"; type = types.path; description = '' The data directory, for storing certificates. Before 17.09, this would create a .caddy directory. With 17.09 the contents of the .caddy directory are in the specified data directory instead. Caddy v2 replaced CADDYPATH with XDG directories. See https://caddyserver.com/docs/conventions#file-locations. ''; }; package = mkOption { default = pkgs.caddy; defaultText = literalExpression "pkgs.caddy"; type = types.package; description = '' Caddy package to use. ''; }; }; config = mkIf cfg.enable { systemd.packages = [ cfg.package ]; systemd.services.caddy = { wantedBy = [ "multi-user.target" ]; startLimitIntervalSec = 14400; startLimitBurst = 10; serviceConfig = { # https://www.freedesktop.org/software/systemd/man/systemd.service.html#ExecStart= # If the empty string is assigned to this option, the list of commands to start is reset, prior assignments of this option will have no effect. ExecStart = [ "" "${cfg.package}/bin/caddy run ${optionalString cfg.resume "--resume"} --config ${configJSON}" ]; ExecReload = [ "" "${cfg.package}/bin/caddy reload --config ${configJSON}" ]; User = cfg.user; Group = cfg.group; ReadWriteDirectories = cfg.dataDir; Restart = "on-abnormal"; # TODO: attempt to upstream these options NoNewPrivileges = true; PrivateDevices = true; ProtectHome = true; }; }; users.users = optionalAttrs (cfg.user == "caddy") { caddy = { group = cfg.group; uid = config.ids.uids.caddy; home = cfg.dataDir; createHome = true; }; }; users.groups = optionalAttrs (cfg.group == "caddy") { caddy.gid = config.ids.gids.caddy; }; }; }