9 cfg = config.services.davis;
13 mysqlLocal = db.createLocally && db.driver == "mysql";
14 pgsqlLocal = db.createLocally && db.driver == "postgresql";
19 isSecret = v: lib.isAttrs v && v ? _secret && (lib.isString v._secret || builtins.isPath v._secret);
20 davisEnvVars = lib.generators.toKeyValue {
21 mkKeyValue = lib.flip lib.generators.mkKeyValueDefault "=" {
24 if builtins.isInt v then
26 else if lib.isString v then
28 else if true == v then
30 else if false == v then
32 else if null == v then
34 else if isSecret v then
35 if (lib.isString v._secret) then
36 builtins.hashString "sha256" v._secret
38 builtins.hashString "sha256" (builtins.readFile v._secret)
40 throw "unsupported type ${builtins.typeOf v}: ${(lib.generators.toPretty { }) v}";
43 secretPaths = lib.mapAttrsToList (_: v: v._secret) (lib.filterAttrs (_: isSecret) cfg.config);
44 mkSecretReplacement = file: ''
48 if (lib.isString file) then
49 builtins.hashString "sha256" file
51 builtins.hashString "sha256" (builtins.readFile file)
54 "${cfg.dataDir}/.env.local"
58 secretReplacements = lib.concatMapStrings mkSecretReplacement secretPaths;
59 filteredConfig = lib.converge (lib.filterAttrsRecursive (
66 davisEnv = pkgs.writeText "davis.env" (davisEnvVars filteredConfig);
69 options.services.davis = {
70 enable = lib.mkEnableOption "Davis is a caldav and carddav server";
74 description = "User davis runs as.";
78 group = lib.mkOption {
80 description = "Group davis runs as.";
84 package = lib.mkPackageOption pkgs "davis" { };
86 dataDir = lib.mkOption {
87 type = lib.types.path;
88 default = "/var/lib/davis";
94 hostname = lib.mkOption {
96 example = "davis.yourdomain.org";
98 Domain of the host to serve davis under. You may want to change it if you
99 run Davis on a different URL than davis.yourdomain.
103 config = lib.mkOption {
104 type = lib.types.attrsOf (
115 lib.types.submodule {
117 _secret = lib.mkOption {
118 type = lib.types.nullOr (
125 The path to a file containing the value the
126 option should be set to in the final
141 adminLogin = lib.mkOption {
142 type = lib.types.str;
145 Username for the admin account.
148 adminPasswordFile = lib.mkOption {
149 type = lib.types.path;
151 The full path to a file that contains the admin's password. Must be
152 readable by the user.
154 example = "/run/secrets/davis-admin-pass";
157 appSecretFile = lib.mkOption {
158 type = lib.types.path;
160 A file containing the Symfony APP_SECRET - Its value should be a series
161 of characters, numbers and symbols chosen randomly and the recommended
162 length is around 32 characters. Can be generated with <code>cat
163 /dev/urandom | tr -dc a-zA-Z0-9 | fold -w 48 | head -n 1</code>.
165 example = "/run/secrets/davis-appsecret";
169 driver = lib.mkOption {
170 type = lib.types.enum [
176 description = "Database type, required in all circumstances.";
178 urlFile = lib.mkOption {
179 type = lib.types.nullOr lib.types.path;
181 example = "/run/secrets/davis-db-url";
183 A file containing the database connection url. If set then it
184 overrides all other database settings (except driver). This is
185 mandatory if you want to use an external database, that is when
186 `services.davis.database.createLocally` is `false`.
189 name = lib.mkOption {
190 type = lib.types.nullOr lib.types.str;
192 description = "Database name, only used when the databse is created locally.";
194 createLocally = lib.mkOption {
195 type = lib.types.bool;
197 description = "Create the database and database user locally.";
203 type = lib.types.nullOr lib.types.str;
205 description = "Mail DSN for sending emails. Mutually exclusive with `services.davis.mail.dsnFile`.";
206 example = "smtp://username:password@example.com:25";
208 dsnFile = lib.mkOption {
209 type = lib.types.nullOr lib.types.str;
211 example = "/run/secrets/davis-mail-dsn";
212 description = "A file containing the mail DSN for sending emails. Mutually exclusive with `servies.davis.mail.dsn`.";
214 inviteFromAddress = lib.mkOption {
215 type = lib.types.nullOr lib.types.str;
217 description = "Email address to send invitations from.";
218 example = "no-reply@dav.example.com";
222 nginx = lib.mkOption {
223 type = lib.types.submodule (
224 lib.recursiveUpdate (import ../web-servers/nginx/vhost-options.nix { inherit config lib; }) { }
230 "dav.''${config.networking.domain}"
232 # To enable encryption and let let's encrypt take care of certificate
238 With this option, you can customize the nginx virtualHost settings.
242 poolConfig = lib.mkOption {
243 type = lib.types.attrsOf (
252 "pm.max_children" = 32;
253 "pm.start_servers" = 2;
254 "pm.min_spare_servers" = 2;
255 "pm.max_spare_servers" = 4;
256 "pm.max_requests" = 500;
259 Options for the davis PHP pool. See the documentation on <literal>php-fpm.conf</literal>
260 for details on configuration directives.
267 defaultServiceConfig = {
268 ReadWritePaths = "${cfg.dataDir}";
272 LockPersonality = true;
273 NoNewPrivileges = true;
274 PrivateDevices = true;
279 ProtectControlGroups = true;
281 ProtectHostname = true;
282 ProtectKernelLogs = true;
283 ProtectKernelModules = true;
284 ProtectKernelTunables = true;
285 ProtectProc = "invisible";
286 ProtectSystem = "strict";
288 RestrictNamespaces = true;
289 RestrictRealtime = true;
290 RestrictSUIDSGID = true;
291 SystemCallArchitectures = "native";
297 WorkingDirectory = "${cfg.package}/";
300 lib.mkIf cfg.enable {
303 assertion = db.createLocally -> db.urlFile == null;
304 message = "services.davis.database.urlFile must be unset if services.davis.database.createLocally is set true.";
307 assertion = db.createLocally || db.urlFile != null;
308 message = "One of services.davis.database.urlFile or services.davis.database.createLocally must be set.";
311 assertion = (mail.dsn != null) != (mail.dsnFile != null);
312 message = "One of (and only one of) services.davis.mail.dsn or services.davis.mail.dsnFile must be set.";
315 services.davis.config =
318 APP_CACHE_DIR = "${cfg.dataDir}/var/cache";
319 # note: we do not need the log dir (we log to stdout/journald), by davis/symfony will try to create it, and the default value is one in the nix-store
320 # so we set it to a path under dataDir to avoid something like: Unable to create the "logs" directory (/nix/store/5cfskz0ybbx37s1161gjn5klwb5si1zg-davis-4.4.1/var/log).
321 APP_LOG_DIR = "${cfg.dataDir}/var/log";
322 LOG_FILE_PATH = "/dev/stdout";
323 DATABASE_DRIVER = db.driver;
324 INVITE_FROM_ADDRESS = mail.inviteFromAddress;
325 APP_SECRET._secret = cfg.appSecretFile;
326 ADMIN_LOGIN = cfg.adminLogin;
327 ADMIN_PASSWORD._secret = cfg.adminPasswordFile;
328 APP_TIMEZONE = config.time.timeZone;
329 WEBDAV_ENABLED = false;
330 CALDAV_ENABLED = true;
331 CARDDAV_ENABLED = true;
333 // (if mail.dsn != null then { MAILER_DSN = mail.dsn; } else { MAILER_DSN._secret = mail.dsnFile; })
335 if db.createLocally then
338 if db.driver == "sqlite" then
339 "sqlite:///${cfg.dataDir}/davis.db" # note: sqlite needs 4 slashes for an absolute path
342 # note: davis expects a non-standard postgres uri (due to the underlying doctrine library)
343 # specifically the dummy hostname which is overriden by the host query parameter
345 "postgres://${user}@localhost/${db.name}?host=/run/postgresql"
346 else if mysqlLocal then
347 "mysql://${user}@localhost/${db.name}?socket=/run/mysqld/mysqld.sock"
352 { DATABASE_URL._secret = db.urlFile; }
356 users = lib.mkIf (user == "davis") {
358 description = "Davis service user";
364 groups = lib.mkIf (group == "davis") { davis = { }; };
367 systemd.tmpfiles.rules = [
368 "d ${cfg.dataDir} 0710 ${user} ${group} - -"
369 "d ${cfg.dataDir}/var 0700 ${user} ${group} - -"
370 "d ${cfg.dataDir}/var/log 0700 ${user} ${group} - -"
371 "d ${cfg.dataDir}/var/cache 0700 ${user} ${group} - -"
374 services.phpfpm.pools.davis = {
380 ENV_DIR = "${cfg.dataDir}";
381 APP_CACHE_DIR = "${cfg.dataDir}/var/cache";
382 APP_LOG_DIR = "${cfg.dataDir}/var/log";
386 "listen.mode" = "0660";
388 "pm.max_children" = 256;
389 "pm.start_servers" = 10;
390 "pm.min_spare_servers" = 5;
391 "pm.max_spare_servers" = 20;
394 if cfg.nginx != null then
396 "listen.owner" = config.services.nginx.user;
397 "listen.group" = config.services.nginx.group;
405 # Reading the user-provided secret files requires root access
406 systemd.services.davis-env-setup = {
407 description = "Setup davis environment";
409 "phpfpm-davis.service"
410 "davis-db-migrate.service"
412 wantedBy = [ "multi-user.target" ];
415 RemainAfterExit = true;
417 path = [ pkgs.replace-secret ];
425 # create .env file with the upstream values
426 install -T -m 0600 -o ${user} ${cfg.package}/env-upstream "${cfg.dataDir}/.env"
427 # create .env.local file with the user-provided values
428 install -T -m 0600 -o ${user} ${davisEnv} "${cfg.dataDir}/.env.local"
429 ${secretReplacements}
433 systemd.services.davis-db-migrate = {
434 description = "Migrate davis database";
435 before = [ "phpfpm-davis.service" ];
437 lib.optional mysqlLocal "mysql.service"
438 ++ lib.optional pgsqlLocal "postgresql.service"
439 ++ [ "davis-env-setup.service" ];
441 lib.optional mysqlLocal "mysql.service"
442 ++ lib.optional pgsqlLocal "postgresql.service"
443 ++ [ "davis-env-setup.service" ];
444 wantedBy = [ "multi-user.target" ];
445 serviceConfig = defaultServiceConfig // {
447 RemainAfterExit = true;
449 "ENV_DIR=${cfg.dataDir}"
450 "APP_CACHE_DIR=${cfg.dataDir}/var/cache"
451 "APP_LOG_DIR=${cfg.dataDir}/var/log"
453 EnvironmentFile = "${cfg.dataDir}/.env.local";
461 ${cfg.package}/bin/console cache:clear --no-debug
462 ${cfg.package}/bin/console cache:warmup --no-debug
463 ${cfg.package}/bin/console doctrine:migrations:migrate
467 systemd.services.phpfpm-davis.after = [
468 "davis-env-setup.service"
469 "davis-db-migrate.service"
471 systemd.services.phpfpm-davis.requires = [
472 "davis-env-setup.service"
473 "davis-db-migrate.service"
474 ] ++ lib.optional mysqlLocal "mysql.service" ++ lib.optional pgsqlLocal "postgresql.service";
475 systemd.services.phpfpm-davis.serviceConfig.ReadWritePaths = [ cfg.dataDir ];
477 services.nginx = lib.mkIf (cfg.nginx != null) {
478 enable = lib.mkDefault true;
480 "${cfg.hostname}" = lib.mkMerge [
483 root = lib.mkForce "${cfg.package}/public";
491 try_files $uri $uri/ /index.php$is_args$args;
494 "~* ^/.well-known/(caldav|carddav)$" = {
496 return 302 https://$host/dav/;
499 "~ ^(.+\.php)(.*)$" = {
501 try_files $fastcgi_script_name =404;
502 include ${config.services.nginx.package}/conf/fastcgi_params;
503 include ${config.services.nginx.package}/conf/fastcgi.conf;
504 fastcgi_pass unix:${config.services.phpfpm.pools.davis.socket};
505 fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
506 fastcgi_param PATH_INFO $fastcgi_path_info;
507 fastcgi_split_path_info ^(.+\.php)(.*)$;
508 fastcgi_param X-Forwarded-Proto https;
509 fastcgi_param X-Forwarded-Port $http_x_forwarded_port;
524 services.mysql = lib.mkIf mysqlLocal {
526 package = lib.mkDefault pkgs.mariadb;
527 ensureDatabases = [ db.name ];
531 ensurePermissions = {
532 "${db.name}.*" = "ALL PRIVILEGES";
538 services.postgresql = lib.mkIf pgsqlLocal {
540 ensureDatabases = [ db.name ];
544 ensureDBOwnership = true;
552 maintainers = pkgs.davis.meta.maintainers;