1 { config, lib, pkgs, ... }:
6 cfg = config.services.caddy;
8 virtualHosts = attrValues cfg.virtualHosts;
9 acmeVHosts = filter (hostOpts: hostOpts.useACMEHost != null) virtualHosts;
11 mkVHostConf = hostOpts:
13 sslCertDir = config.security.acme.certs.${hostOpts.useACMEHost}.directory;
16 ${hostOpts.hostName} ${concatStringsSep " " hostOpts.serverAliases} {
17 ${optionalString (hostOpts.listenAddresses != [ ]) "bind ${concatStringsSep " " hostOpts.listenAddresses}"}
18 ${optionalString (hostOpts.useACMEHost != null) "tls ${sslCertDir}/cert.pem ${sslCertDir}/key.pem"}
23 ${hostOpts.extraConfig}
27 settingsFormat = pkgs.formats.json { };
30 if cfg.settings != { } then
31 settingsFormat.generate "caddy.json" cfg.settings
34 Caddyfile = pkgs.writeTextDir "Caddyfile" ''
39 ${concatMapStringsSep "\n" mkVHostConf virtualHosts}
42 Caddyfile-formatted = pkgs.runCommand "Caddyfile-formatted" { nativeBuildInputs = [ cfg.package ]; } ''
44 cp --no-preserve=mode ${Caddyfile}/Caddyfile $out/Caddyfile
45 caddy fmt --overwrite $out/Caddyfile
48 "${if pkgs.stdenv.buildPlatform == pkgs.stdenv.hostPlatform then Caddyfile-formatted else Caddyfile}/Caddyfile";
50 etcConfigFile = "caddy/caddy_config";
52 configPath = "/etc/${etcConfigFile}";
54 acmeHosts = unique (catAttrs "useACMEHost" acmeVHosts);
56 mkCertOwnershipAssertion = import ../../../security/acme/mk-cert-ownership-assertion.nix;
60 (mkRemovedOptionModule [ "services" "caddy" "agree" ] "this option is no longer necessary for Caddy 2")
61 (mkRenamedOptionModule [ "services" "caddy" "ca" ] [ "services" "caddy" "acmeCA" ])
62 (mkRenamedOptionModule [ "services" "caddy" "config" ] [ "services" "caddy" "extraConfig" ])
66 options.services.caddy = {
67 enable = mkEnableOption (lib.mdDoc "Caddy web server");
72 description = lib.mdDoc ''
73 User account under which caddy runs.
76 If left as the default value this user will automatically be created
77 on system activation, otherwise you are responsible for
78 ensuring the user exists before the Caddy service starts.
86 description = lib.mdDoc ''
87 Group account under which caddy runs.
90 If left as the default value this user will automatically be created
91 on system activation, otherwise you are responsible for
92 ensuring the user exists before the Caddy service starts.
99 defaultText = literalExpression "pkgs.caddy";
100 type = types.package;
101 description = lib.mdDoc ''
102 Caddy package to use.
108 default = "/var/lib/caddy";
109 description = lib.mdDoc ''
110 The data directory for caddy.
113 If left as the default value this directory will automatically be created
114 before the Caddy server starts, otherwise you are responsible for ensuring
115 the directory exists with appropriate ownership and permissions.
117 Caddy v2 replaced `CADDYPATH` with XDG directories.
118 See <https://caddyserver.com/docs/conventions#file-locations>.
125 default = "/var/log/caddy";
126 description = lib.mdDoc ''
127 Directory for storing Caddy access logs.
130 If left as the default value this directory will automatically be created
131 before the Caddy server starts, otherwise the sysadmin is responsible for
132 ensuring the directory exists with appropriate ownership and permissions.
137 logFormat = mkOption {
142 example = literalExpression ''
143 mkForce "level INFO";
145 description = lib.mdDoc ''
146 Configuration for the default logger. See
147 <https://caddyserver.com/docs/caddyfile/options#log>
152 configFile = mkOption {
154 default = configFile;
155 defaultText = "A Caddyfile automatically generated by values from services.caddy.*";
156 example = literalExpression ''
157 pkgs.writeTextDir "Caddyfile" '''
160 root * /var/www/wordpress
161 php_fastcgi unix//run/php/php-version-fpm.sock
165 description = lib.mdDoc ''
166 Override the configuration file used by Caddy. By default,
167 NixOS generates one automatically.
169 The configuration file is exposed at {file}`${configPath}`.
174 default = if (builtins.baseNameOf cfg.configFile) == "Caddyfile" then "caddyfile" else null;
175 defaultText = literalExpression ''
176 if (builtins.baseNameOf cfg.configFile) == "Caddyfile" then "caddyfile" else null
178 example = literalExpression "nginx";
179 type = with types; nullOr str;
180 description = lib.mdDoc ''
181 Name of the config adapter to use.
182 See <https://caddyserver.com/docs/config-adapters>
185 If `null` is specified, the `--adapter` argument is omitted when
186 starting or restarting Caddy. Notably, this allows specification of a
187 configuration file in Caddy's native JSON format, as long as the
188 filename does not start with `Caddyfile` (in which case the `caddyfile`
189 adapter is implicitly enabled). See
190 <https://caddyserver.com/docs/command-line#caddy-run> for details.
193 Any value other than `null` or `caddyfile` is only valid when providing
194 your own `configFile`.
202 description = lib.mdDoc ''
203 Use saved config, if any (and prefer over any specified configuration passed with `--config`).
207 globalConfig = mkOption {
218 description = lib.mdDoc ''
219 Additional lines of configuration appended to the global config section
222 Refer to <https://caddyserver.com/docs/caddyfile/options#global-options>
223 for details on supported values.
227 extraConfig = mkOption {
237 description = lib.mdDoc ''
238 Additional lines of configuration appended to the automatically
239 generated `Caddyfile`.
243 virtualHosts = mkOption {
244 type = with types; attrsOf (submodule (import ./vhost-options.nix { inherit cfg; }));
246 example = literalExpression ''
248 "hydra.example.com" = {
249 serverAliases = [ "www.hydra.example.com" ];
257 description = lib.mdDoc ''
258 Declarative specification of virtual hosts served by Caddy.
264 example = "https://acme-v02.api.letsencrypt.org/directory";
265 type = with types; nullOr str;
266 description = lib.mdDoc ''
268 Sets the [`acme_ca` option](https://caddyserver.com/docs/caddyfile/options#acme-ca)
269 in the global options block of the resulting Caddyfile.
272 The URL to the ACME CA's directory. It is strongly recommended to set
273 this to `https://acme-staging-v02.api.letsencrypt.org/directory` for
274 Let's Encrypt's [staging endpoint](https://letsencrypt.org/docs/staging-environment/)
275 while testing or in development.
277 Value `null` should be prefered for production setups,
278 as it omits the `acme_ca` option to enable
279 [automatic issuer fallback](https://caddyserver.com/docs/automatic-https#issuer-fallback).
285 type = with types; nullOr str;
286 description = lib.mdDoc ''
287 Your email address. Mainly used when creating an ACME account with your
288 CA, and is highly recommended in case there are problems with your
293 enableReload = mkOption {
296 description = lib.mdDoc ''
297 Reload Caddy instead of restarting it when configuration file changes.
299 Note that enabling this option requires the [admin API](https://caddyserver.com/docs/caddyfile/options#admin)
300 to not be turned off.
302 If you enable this option, consider setting [`grace_period`](https://caddyserver.com/docs/caddyfile/options#grace-period)
303 to a non-infinite value in {option}`services.caddy.globalConfig`
304 to prevent Caddy waiting for active connections to finish,
305 which could delay the reload essentially indefinitely.
309 settings = mkOption {
310 type = settingsFormat.type;
312 description = lib.mdDoc ''
313 Structured configuration for Caddy to generate a Caddy JSON configuration file.
314 See <https://caddyserver.com/docs/json/> for available options.
317 Using a [Caddyfile](https://caddyserver.com/docs/caddyfile) instead of a JSON config is highly recommended by upstream.
318 There are only very few exception to this.
320 Please use a Caddyfile via {option}`services.caddy.configFile`, {option}`services.caddy.virtualHosts` or
321 {option}`services.caddy.extraConfig` with {option}`services.caddy.globalConfig` instead.
325 Takes presence over most `services.caddy.*` options, such as {option}`services.caddy.configFile` and {option}`services.caddy.virtualHosts`, if specified.
332 config = mkIf cfg.enable {
335 { assertion = cfg.configFile == configFile -> cfg.adapter == "caddyfile" || cfg.adapter == null;
336 message = "To specify an adapter other than 'caddyfile' please provide your own configuration via `services.caddy.configFile`";
338 ] ++ map (name: mkCertOwnershipAssertion {
339 inherit (cfg) group user;
340 cert = config.security.acme.certs.${name};
341 groups = config.users.groups;
344 services.caddy.globalConfig = ''
345 ${optionalString (cfg.email != null) "email ${cfg.email}"}
346 ${optionalString (cfg.acmeCA != null) "acme_ca ${cfg.acmeCA}"}
352 # https://github.com/lucas-clemente/quic-go/wiki/UDP-Receive-Buffer-Size
353 boot.kernel.sysctl."net.core.rmem_max" = mkDefault 2500000;
355 systemd.packages = [ cfg.package ];
356 systemd.services.caddy = {
357 wants = map (hostOpts: "acme-finished-${hostOpts.useACMEHost}.target") acmeVHosts;
358 after = map (hostOpts: "acme-selfsigned-${hostOpts.useACMEHost}.service") acmeVHosts;
359 before = map (hostOpts: "acme-${hostOpts.useACMEHost}.service") acmeVHosts;
361 wantedBy = [ "multi-user.target" ];
362 startLimitIntervalSec = 14400;
363 startLimitBurst = 10;
364 reloadTriggers = optional cfg.enableReload cfg.configFile;
367 runOptions = ''--config ${configPath} ${optionalString (cfg.adapter != null) "--adapter ${cfg.adapter}"}'';
369 # https://www.freedesktop.org/software/systemd/man/systemd.service.html#ExecStart=
370 # 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.
371 ExecStart = [ "" ''${cfg.package}/bin/caddy run ${runOptions} ${optionalString cfg.resume "--resume"}'' ];
372 # Validating the configuration before applying it ensures we’ll get a proper error that will be reported when switching to the configuration
373 ExecReload = [ "" ''${cfg.package}/bin/caddy reload ${runOptions} --force'' ];
376 ReadWriteDirectories = cfg.dataDir;
377 StateDirectory = mkIf (cfg.dataDir == "/var/lib/caddy") [ "caddy" ];
378 LogsDirectory = mkIf (cfg.logDir == "/var/log/caddy") [ "caddy" ];
379 Restart = "on-failure";
380 RestartPreventExitStatus = 1;
383 # TODO: attempt to upstream these options
384 NoNewPrivileges = true;
385 PrivateDevices = true;
390 users.users = optionalAttrs (cfg.user == "caddy") {
393 uid = config.ids.uids.caddy;
398 users.groups = optionalAttrs (cfg.group == "caddy") {
399 caddy.gid = config.ids.gids.caddy;
402 security.acme.certs =
404 certCfg = map (useACMEHost: nameValuePair useACMEHost {
405 group = mkDefault cfg.group;
406 reloadServices = [ "caddy.service" ];
411 environment.etc.${etcConfigFile}.source = cfg.configFile;