1 { config, lib, pkgs, ... }:
6 cfg = config.services.caddy;
8 certs = config.security.acme.certs;
9 virtualHosts = attrValues cfg.virtualHosts;
10 acmeEnabledVhosts = filter (hostOpts: hostOpts.useACMEHost != null) virtualHosts;
11 vhostCertNames = unique (map (hostOpts: hostOpts.useACMEHost) acmeEnabledVhosts);
12 dependentCertNames = filter (cert: certs.${cert}.dnsProvider == null) vhostCertNames; # those that might depend on the HTTP server
13 independentCertNames = filter (cert: certs.${cert}.dnsProvider != null) vhostCertNames; # those that don't depend on the HTTP server
15 mkVHostConf = hostOpts:
17 sslCertDir = config.security.acme.certs.${hostOpts.useACMEHost}.directory;
20 ${hostOpts.hostName} ${concatStringsSep " " hostOpts.serverAliases} {
21 ${optionalString (hostOpts.listenAddresses != [ ]) "bind ${concatStringsSep " " hostOpts.listenAddresses}"}
22 ${optionalString (hostOpts.useACMEHost != null) "tls ${sslCertDir}/cert.pem ${sslCertDir}/key.pem"}
27 ${hostOpts.extraConfig}
31 settingsFormat = pkgs.formats.json { };
34 if cfg.settings != { } then
35 settingsFormat.generate "caddy.json" cfg.settings
38 Caddyfile = pkgs.writeTextDir "Caddyfile" ''
43 ${concatMapStringsSep "\n" mkVHostConf virtualHosts}
46 Caddyfile-formatted = pkgs.runCommand "Caddyfile-formatted" { nativeBuildInputs = [ cfg.package ]; } ''
48 cp --no-preserve=mode ${Caddyfile}/Caddyfile $out/Caddyfile
49 caddy fmt --overwrite $out/Caddyfile
52 "${if pkgs.stdenv.buildPlatform == pkgs.stdenv.hostPlatform then Caddyfile-formatted else Caddyfile}/Caddyfile";
54 etcConfigFile = "caddy/caddy_config";
56 configPath = "/etc/${etcConfigFile}";
58 mkCertOwnershipAssertion = import ../../../security/acme/mk-cert-ownership-assertion.nix lib;
62 (mkRemovedOptionModule [ "services" "caddy" "agree" ] "this option is no longer necessary for Caddy 2")
63 (mkRenamedOptionModule [ "services" "caddy" "ca" ] [ "services" "caddy" "acmeCA" ])
64 (mkRenamedOptionModule [ "services" "caddy" "config" ] [ "services" "caddy" "extraConfig" ])
68 options.services.caddy = {
69 enable = mkEnableOption "Caddy web server";
75 User account under which caddy runs.
78 If left as the default value this user will automatically be created
79 on system activation, otherwise you are responsible for
80 ensuring the user exists before the Caddy service starts.
89 Group under which caddy runs.
92 If left as the default value this group will automatically be created
93 on system activation, otherwise you are responsible for
94 ensuring the group exists before the Caddy service starts.
99 package = mkPackageOption pkgs "caddy" { };
103 default = "/var/lib/caddy";
105 The data directory for caddy.
108 If left as the default value this directory will automatically be created
109 before the Caddy server starts, otherwise you are responsible for ensuring
110 the directory exists with appropriate ownership and permissions.
112 Caddy v2 replaced `CADDYPATH` with XDG directories.
113 See <https://caddyserver.com/docs/conventions#file-locations>.
120 default = "/var/log/caddy";
122 Directory for storing Caddy access logs.
125 If left as the default value this directory will automatically be created
126 before the Caddy server starts, otherwise the sysadmin is responsible for
127 ensuring the directory exists with appropriate ownership and permissions.
132 logFormat = mkOption {
137 example = literalExpression ''
138 mkForce "level INFO";
141 Configuration for the default logger. See
142 <https://caddyserver.com/docs/caddyfile/options#log>
147 configFile = mkOption {
149 default = configFile;
150 defaultText = "A Caddyfile automatically generated by values from services.caddy.*";
151 example = literalExpression ''
152 pkgs.writeText "Caddyfile" '''
155 root * /var/www/wordpress
156 php_fastcgi unix//run/php/php-version-fpm.sock
161 Override the configuration file used by Caddy. By default,
162 NixOS generates one automatically.
164 The configuration file is exposed at {file}`${configPath}`.
169 default = if ((cfg.configFile != configFile) || (builtins.baseNameOf cfg.configFile) == "Caddyfile") then "caddyfile" else null;
170 defaultText = literalExpression ''
171 if ((cfg.configFile != configFile) || (builtins.baseNameOf cfg.configFile) == "Caddyfile") then "caddyfile" else null
173 example = literalExpression "nginx";
174 type = with types; nullOr str;
176 Name of the config adapter to use.
177 See <https://caddyserver.com/docs/config-adapters>
180 If `null` is specified, the `--adapter` argument is omitted when
181 starting or restarting Caddy. Notably, this allows specification of a
182 configuration file in Caddy's native JSON format, as long as the
183 filename does not start with `Caddyfile` (in which case the `caddyfile`
184 adapter is implicitly enabled). See
185 <https://caddyserver.com/docs/command-line#caddy-run> for details.
188 Any value other than `null` or `caddyfile` is only valid when providing
189 your own `configFile`.
198 Use saved config, if any (and prefer over any specified configuration passed with `--config`).
202 globalConfig = mkOption {
214 Additional lines of configuration appended to the global config section
217 Refer to <https://caddyserver.com/docs/caddyfile/options#global-options>
218 for details on supported values.
222 extraConfig = mkOption {
233 Additional lines of configuration appended to the automatically
234 generated `Caddyfile`.
238 virtualHosts = mkOption {
239 type = with types; attrsOf (submodule (import ./vhost-options.nix { inherit cfg; }));
241 example = literalExpression ''
243 "hydra.example.com" = {
244 serverAliases = [ "www.hydra.example.com" ];
253 Declarative specification of virtual hosts served by Caddy.
259 example = "https://acme-v02.api.letsencrypt.org/directory";
260 type = with types; nullOr str;
263 Sets the [`acme_ca` option](https://caddyserver.com/docs/caddyfile/options#acme-ca)
264 in the global options block of the resulting Caddyfile.
267 The URL to the ACME CA's directory. It is strongly recommended to set
268 this to `https://acme-staging-v02.api.letsencrypt.org/directory` for
269 Let's Encrypt's [staging endpoint](https://letsencrypt.org/docs/staging-environment/)
270 while testing or in development.
272 Value `null` should be prefered for production setups,
273 as it omits the `acme_ca` option to enable
274 [automatic issuer fallback](https://caddyserver.com/docs/automatic-https#issuer-fallback).
280 type = with types; nullOr str;
282 Your email address. Mainly used when creating an ACME account with your
283 CA, and is highly recommended in case there are problems with your
288 enableReload = mkOption {
292 Reload Caddy instead of restarting it when configuration file changes.
294 Note that enabling this option requires the [admin API](https://caddyserver.com/docs/caddyfile/options#admin)
295 to not be turned off.
297 If you enable this option, consider setting [`grace_period`](https://caddyserver.com/docs/caddyfile/options#grace-period)
298 to a non-infinite value in {option}`services.caddy.globalConfig`
299 to prevent Caddy waiting for active connections to finish,
300 which could delay the reload essentially indefinitely.
304 settings = mkOption {
305 type = settingsFormat.type;
308 Structured configuration for Caddy to generate a Caddy JSON configuration file.
309 See <https://caddyserver.com/docs/json/> for available options.
312 Using a [Caddyfile](https://caddyserver.com/docs/caddyfile) instead of a JSON config is highly recommended by upstream.
313 There are only very few exception to this.
315 Please use a Caddyfile via {option}`services.caddy.configFile`, {option}`services.caddy.virtualHosts` or
316 {option}`services.caddy.extraConfig` with {option}`services.caddy.globalConfig` instead.
320 Takes presence over most `services.caddy.*` options, such as {option}`services.caddy.configFile` and {option}`services.caddy.virtualHosts`, if specified.
327 config = mkIf cfg.enable {
330 { assertion = cfg.configFile == configFile -> cfg.adapter == "caddyfile" || cfg.adapter == null;
331 message = "To specify an adapter other than 'caddyfile' please provide your own configuration via `services.caddy.configFile`";
333 ] ++ map (name: mkCertOwnershipAssertion {
334 cert = config.security.acme.certs.${name};
335 groups = config.users.groups;
336 services = [ config.systemd.services.caddy ];
339 services.caddy.globalConfig = ''
340 ${optionalString (cfg.email != null) "email ${cfg.email}"}
341 ${optionalString (cfg.acmeCA != null) "acme_ca ${cfg.acmeCA}"}
347 # https://github.com/quic-go/quic-go/wiki/UDP-Buffer-Sizes
348 boot.kernel.sysctl."net.core.rmem_max" = mkDefault 2500000;
349 boot.kernel.sysctl."net.core.wmem_max" = mkDefault 2500000;
351 systemd.packages = [ cfg.package ];
352 systemd.services.caddy = {
353 wants = map (certName: "acme-finished-${certName}.target") vhostCertNames;
354 after = map (certName: "acme-selfsigned-${certName}.service") vhostCertNames
355 ++ map (certName: "acme-${certName}.service") independentCertNames; # avoid loading self-signed key w/ real cert, or vice-versa
356 before = map (certName: "acme-${certName}.service") dependentCertNames;
358 wantedBy = [ "multi-user.target" ];
359 startLimitIntervalSec = 14400;
360 startLimitBurst = 10;
361 reloadTriggers = optional cfg.enableReload cfg.configFile;
362 restartTriggers = optional (! cfg.enableReload) cfg.configFile;
365 runOptions = ''--config ${configPath} ${optionalString (cfg.adapter != null) "--adapter ${cfg.adapter}"}'';
367 # Override the `ExecStart` line from upstream's systemd unit file by our own:
368 # https://www.freedesktop.org/software/systemd/man/systemd.service.html#ExecStart=
369 # 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.
370 ExecStart = [ "" ''${cfg.package}/bin/caddy run ${runOptions} ${optionalString cfg.resume "--resume"}'' ];
371 # Validating the configuration before applying it ensures we’ll get a proper error that will be reported when switching to the configuration
372 ExecReload = [ "" ] ++ lib.optional cfg.enableReload "${lib.getExe cfg.package} reload ${runOptions} --force";
375 ReadWritePaths = [ cfg.dataDir ];
376 StateDirectory = mkIf (cfg.dataDir == "/var/lib/caddy") [ "caddy" ];
377 LogsDirectory = mkIf (cfg.logDir == "/var/log/caddy") [ "caddy" ];
378 Restart = "on-failure";
379 RestartPreventExitStatus = 1;
382 # TODO: attempt to upstream these options
383 NoNewPrivileges = true;
384 PrivateDevices = true;
389 users.users = optionalAttrs (cfg.user == "caddy") {
392 uid = config.ids.uids.caddy;
397 users.groups = optionalAttrs (cfg.group == "caddy") {
398 caddy.gid = config.ids.gids.caddy;
401 security.acme.certs =
403 certCfg = map (certName: nameValuePair certName {
404 group = mkDefault cfg.group;
405 reloadServices = [ "caddy.service" ];
410 environment.etc.${etcConfigFile}.source = cfg.configFile;