vuls: init at 0.27.0
[NixPkgs.git] / nixos / modules / services / web-apps / nextcloud.nix
blobfcaca9244c76f244007d954b1ad297c1a81b73a2
1 { config, lib, pkgs, ... }:
3 with lib;
5 let
6   cfg = config.services.nextcloud;
7   fpm = config.services.phpfpm.pools.nextcloud;
9   jsonFormat = pkgs.formats.json {};
11   defaultPHPSettings = {
12     output_buffering = "0";
13     short_open_tag = "Off";
14     expose_php = "Off";
15     error_reporting = "E_ALL & ~E_DEPRECATED & ~E_STRICT";
16     display_errors = "stderr";
17     "opcache.interned_strings_buffer" = "8";
18     "opcache.max_accelerated_files" = "10000";
19     "opcache.memory_consumption" = "128";
20     "opcache.revalidate_freq" = "1";
21     "opcache.fast_shutdown" = "1";
22     "openssl.cafile" = "/etc/ssl/certs/ca-certificates.crt";
23     catch_workers_output = "yes";
24   };
26   appStores = {
27     # default apps bundled with pkgs.nextcloudXX, e.g. files, contacts
28     apps = {
29       enabled = true;
30       writable = false;
31     };
32     # apps installed via cfg.extraApps
33     nix-apps = {
34       enabled = cfg.extraApps != { };
35       linkTarget = pkgs.linkFarm "nix-apps"
36         (mapAttrsToList (name: path: { inherit name path; }) cfg.extraApps);
37       writable = false;
38     };
39     # apps installed via the app store.
40     store-apps = {
41       enabled = cfg.appstoreEnable == null || cfg.appstoreEnable;
42       linkTarget = "${cfg.home}/store-apps";
43       writable = true;
44     };
45   };
47   webroot = pkgs.runCommandLocal
48     "${cfg.package.name or "nextcloud"}-with-apps"
49     { }
50     ''
51       mkdir $out
52       ln -sfv "${cfg.package}"/* "$out"
53       ${concatStrings
54         (mapAttrsToList (name: store: optionalString (store.enabled && store?linkTarget) ''
55           if [ -e "$out"/${name} ]; then
56             echo "Didn't expect ${name} already in $out!"
57             exit 1
58           fi
59           ln -sfTv ${store.linkTarget} "$out"/${name}
60         '') appStores)}
61     '';
63   inherit (cfg) datadir;
65   phpPackage = cfg.phpPackage.buildEnv {
66     extensions = { enabled, all }:
67       (with all; enabled
68         ++ [ bz2 intl sodium ] # recommended
69         ++ optional cfg.enableImagemagick imagick
70         # Optionally enabled depending on caching settings
71         ++ optional cfg.caching.apcu apcu
72         ++ optional cfg.caching.redis redis
73         ++ optional cfg.caching.memcached memcached
74       )
75       ++ cfg.phpExtraExtensions all; # Enabled by user
76     extraConfig = toKeyValue cfg.phpOptions;
77   };
79   toKeyValue = generators.toKeyValue {
80     mkKeyValue = generators.mkKeyValueDefault {} " = ";
81   };
83   phpCli = concatStringsSep " " ([
84     "${getExe phpPackage}"
85   ] ++ optionals (cfg.cli.memoryLimit != null) [
86     "-dmemory_limit=${cfg.cli.memoryLimit}"
87   ]);
89   occ = pkgs.writeScriptBin "nextcloud-occ" ''
90     #! ${pkgs.runtimeShell}
91     cd ${webroot}
92     sudo=exec
93     if [[ "$USER" != nextcloud ]]; then
94       sudo='exec /run/wrappers/bin/sudo -u nextcloud'
95     fi
96     $sudo ${pkgs.coreutils}/bin/env \
97       NEXTCLOUD_CONFIG_DIR="${datadir}/config" \
98       ${phpCli} \
99       occ "$@"
100   '';
102   inherit (config.system) stateVersion;
104   mysqlLocal = cfg.database.createLocally && cfg.config.dbtype == "mysql";
105   pgsqlLocal = cfg.database.createLocally && cfg.config.dbtype == "pgsql";
107   nextcloudGreaterOrEqualThan = versionAtLeast cfg.package.version;
108   nextcloudOlderThan = versionOlder cfg.package.version;
110   # https://github.com/nextcloud/documentation/pull/11179
111   ocmProviderIsNotAStaticDirAnymore = nextcloudGreaterOrEqualThan "27.1.2"
112     || (nextcloudOlderThan "27.0.0" && nextcloudGreaterOrEqualThan "26.0.8");
114   overrideConfig = let
115     c = cfg.config;
116     requiresReadSecretFunction = c.dbpassFile != null || c.objectstore.s3.enable;
117     objectstoreConfig = let s3 = c.objectstore.s3; in optionalString s3.enable ''
118       'objectstore' => [
119         'class' => '\\OC\\Files\\ObjectStore\\S3',
120         'arguments' => [
121           'bucket' => '${s3.bucket}',
122           'autocreate' => ${boolToString s3.autocreate},
123           'key' => '${s3.key}',
124           'secret' => nix_read_secret('${s3.secretFile}'),
125           ${optionalString (s3.hostname != null) "'hostname' => '${s3.hostname}',"}
126           ${optionalString (s3.port != null) "'port' => ${toString s3.port},"}
127           'use_ssl' => ${boolToString s3.useSsl},
128           ${optionalString (s3.region != null) "'region' => '${s3.region}',"}
129           'use_path_style' => ${boolToString s3.usePathStyle},
130           ${optionalString (s3.sseCKeyFile != null) "'sse_c_key' => nix_read_secret('${s3.sseCKeyFile}'),"}
131         ],
132       ]
133     '';
134     showAppStoreSetting = cfg.appstoreEnable != null || cfg.extraApps != {};
135     renderedAppStoreSetting =
136       let
137         x = cfg.appstoreEnable;
138       in
139         if x == null then "false"
140         else boolToString x;
141     mkAppStoreConfig = name: { enabled, writable, ... }: optionalString enabled ''
142       [ 'path' => '${webroot}/${name}', 'url' => '/${name}', 'writable' => ${boolToString writable} ],
143     '';
144   in pkgs.writeText "nextcloud-config.php" ''
145     <?php
146     ${optionalString requiresReadSecretFunction ''
147       function nix_read_secret($file) {
148         if (!file_exists($file)) {
149           throw new \RuntimeException(sprintf(
150             "Cannot start Nextcloud, secret file %s set by NixOS doesn't seem to "
151             . "exist! Please make sure that the file exists and has appropriate "
152             . "permissions for user & group 'nextcloud'!",
153             $file
154           ));
155         }
156         return trim(file_get_contents($file));
157       }''}
158     function nix_decode_json_file($file, $error) {
159       if (!file_exists($file)) {
160         throw new \RuntimeException(sprintf($error, $file));
161       }
162       $decoded = json_decode(file_get_contents($file), true);
164       if (json_last_error() !== JSON_ERROR_NONE) {
165         throw new \RuntimeException(sprintf("Cannot decode %s, because: %s", $file, json_last_error_msg()));
166       }
168       return $decoded;
169     }
170     $CONFIG = [
171       'apps_paths' => [
172         ${concatStrings (mapAttrsToList mkAppStoreConfig appStores)}
173       ],
174       ${optionalString (showAppStoreSetting) "'appstoreenabled' => ${renderedAppStoreSetting},"}
175       ${optionalString cfg.caching.apcu "'memcache.local' => '\\OC\\Memcache\\APCu',"}
176       ${optionalString (c.dbname != null) "'dbname' => '${c.dbname}',"}
177       ${optionalString (c.dbhost != null) "'dbhost' => '${c.dbhost}',"}
178       ${optionalString (c.dbuser != null) "'dbuser' => '${c.dbuser}',"}
179       ${optionalString (c.dbtableprefix != null) "'dbtableprefix' => '${toString c.dbtableprefix}',"}
180       ${optionalString (c.dbpassFile != null) ''
181           'dbpassword' => nix_read_secret(
182             "${c.dbpassFile}"
183           ),
184         ''
185       }
186       'dbtype' => '${c.dbtype}',
187       ${objectstoreConfig}
188     ];
190     $CONFIG = array_replace_recursive($CONFIG, nix_decode_json_file(
191       "${jsonFormat.generate "nextcloud-settings.json" cfg.settings}",
192       "impossible: this should never happen (decoding generated settings file %s failed)"
193     ));
195     ${optionalString (cfg.secretFile != null) ''
196       $CONFIG = array_replace_recursive($CONFIG, nix_decode_json_file(
197         "${cfg.secretFile}",
198         "Cannot start Nextcloud, secrets file %s set by NixOS doesn't exist!"
199       ));
200     ''}
201   '';
202 in {
204   imports = [
205     (mkRenamedOptionModule
206       [ "services" "nextcloud" "cron" "memoryLimit" ]
207       [ "services" "nextcloud" "cli" "memoryLimit" ])
208     (mkRemovedOptionModule [ "services" "nextcloud" "enableBrokenCiphersForSSE" ] ''
209       This option has no effect since there's no supported Nextcloud version packaged here
210       using OpenSSL for RC4 SSE.
211     '')
212     (mkRemovedOptionModule [ "services" "nextcloud" "config" "dbport" ] ''
213       Add port to services.nextcloud.config.dbhost instead.
214     '')
215     (mkRenamedOptionModule
216       [ "services" "nextcloud" "logLevel" ] [ "services" "nextcloud" "settings" "loglevel" ])
217     (mkRenamedOptionModule
218       [ "services" "nextcloud" "logType" ] [ "services" "nextcloud" "settings" "log_type" ])
219     (mkRenamedOptionModule
220       [ "services" "nextcloud" "config" "defaultPhoneRegion" ] [ "services" "nextcloud" "settings" "default_phone_region" ])
221     (mkRenamedOptionModule
222       [ "services" "nextcloud" "config" "overwriteProtocol" ] [ "services" "nextcloud" "settings" "overwriteprotocol" ])
223     (mkRenamedOptionModule
224       [ "services" "nextcloud" "skeletonDirectory" ] [ "services" "nextcloud" "settings" "skeletondirectory" ])
225     (mkRenamedOptionModule
226       [ "services" "nextcloud" "globalProfiles" ] [ "services" "nextcloud" "settings" "profile.enabled" ])
227     (mkRenamedOptionModule
228       [ "services" "nextcloud" "config" "extraTrustedDomains" ] [ "services" "nextcloud" "settings" "trusted_domains" ])
229     (mkRenamedOptionModule
230       [ "services" "nextcloud" "config" "trustedProxies" ] [ "services" "nextcloud" "settings" "trusted_proxies" ])
231     (mkRenamedOptionModule ["services" "nextcloud" "extraOptions" ] [ "services" "nextcloud" "settings" ])
232   ];
234   options.services.nextcloud = {
235     enable = mkEnableOption "nextcloud";
237     hostName = mkOption {
238       type = types.str;
239       description = "FQDN for the nextcloud instance.";
240     };
241     home = mkOption {
242       type = types.str;
243       default = "/var/lib/nextcloud";
244       description = "Storage path of nextcloud.";
245     };
246     datadir = mkOption {
247       type = types.str;
248       default = config.services.nextcloud.home;
249       defaultText = literalExpression "config.services.nextcloud.home";
250       description = ''
251         Nextcloud's data storage path.  Will be [](#opt-services.nextcloud.home) by default.
252         This folder will be populated with a config.php file and a data folder which contains the state of the instance (excluding the database).";
253       '';
254       example = "/mnt/nextcloud-file";
255     };
256     extraApps = mkOption {
257       type = types.attrsOf types.package;
258       default = { };
259       description = ''
260         Extra apps to install. Should be an attrSet of appid to packages generated by fetchNextcloudApp.
261         The appid must be identical to the "id" value in the apps appinfo/info.xml.
262         Using this will disable the appstore to prevent Nextcloud from updating these apps (see [](#opt-services.nextcloud.appstoreEnable)).
263       '';
264       example = literalExpression ''
265         {
266           inherit (pkgs.nextcloud25Packages.apps) mail calendar contact;
267           phonetrack = pkgs.fetchNextcloudApp {
268             name = "phonetrack";
269             sha256 = "0qf366vbahyl27p9mshfma1as4nvql6w75zy2zk5xwwbp343vsbc";
270             url = "https://gitlab.com/eneiluj/phonetrack-oc/-/wikis/uploads/931aaaf8dca24bf31a7e169a83c17235/phonetrack-0.6.9.tar.gz";
271             version = "0.6.9";
272           };
273         }
274         '';
275     };
276     extraAppsEnable = mkOption {
277       type = types.bool;
278       default = true;
279       description = ''
280         Automatically enable the apps in [](#opt-services.nextcloud.extraApps) every time Nextcloud starts.
281         If set to false, apps need to be enabled in the Nextcloud web user interface or with `nextcloud-occ app:enable`.
282       '';
283     };
284     appstoreEnable = mkOption {
285       type = types.nullOr types.bool;
286       default = null;
287       example = true;
288       description = ''
289         Allow the installation and updating of apps from the Nextcloud appstore.
290         Enabled by default unless there are packages in [](#opt-services.nextcloud.extraApps).
291         Set this to true to force enable the store even if [](#opt-services.nextcloud.extraApps) is used.
292         Set this to false to disable the installation of apps from the global appstore. App management is always enabled regardless of this setting.
293       '';
294     };
295     https = mkOption {
296       type = types.bool;
297       default = false;
298       description = "Use HTTPS for generated links.";
299     };
300     package = mkOption {
301       type = types.package;
302       description = "Which package to use for the Nextcloud instance.";
303       relatedPackages = [ "nextcloud28" "nextcloud29" "nextcloud30" ];
304     };
305     phpPackage = mkPackageOption pkgs "php" {
306       example = "php82";
307     };
309     maxUploadSize = mkOption {
310       default = "512M";
311       type = types.str;
312       description = ''
313         The upload limit for files. This changes the relevant options
314         in php.ini and nginx if enabled.
315       '';
316     };
318     webfinger = mkOption {
319       type = types.bool;
320       default = false;
321       description = ''
322         Enable this option if you plan on using the webfinger plugin.
323         The appropriate nginx rewrite rules will be added to your configuration.
324       '';
325     };
327     phpExtraExtensions = mkOption {
328       type = with types; functionTo (listOf package);
329       default = all: [];
330       defaultText = literalExpression "all: []";
331       description = ''
332         Additional PHP extensions to use for Nextcloud.
333         By default, only extensions necessary for a vanilla Nextcloud installation are enabled,
334         but you may choose from the list of available extensions and add further ones.
335         This is sometimes necessary to be able to install a certain Nextcloud app that has additional requirements.
336       '';
337       example = literalExpression ''
338         all: [ all.pdlib all.bz2 ]
339       '';
340     };
342     phpOptions = mkOption {
343       type = with types; attrsOf (oneOf [ str int ]);
344       defaultText = literalExpression (generators.toPretty { } defaultPHPSettings);
345       description = ''
346         Options for PHP's php.ini file for nextcloud.
348         Please note that this option is _additive_ on purpose while the
349         attribute values inside the default are option defaults: that means that
351         ```nix
352         {
353           services.nextcloud.phpOptions."opcache.interned_strings_buffer" = "23";
354         }
355         ```
357         will override the `php.ini` option `opcache.interned_strings_buffer` without
358         discarding the rest of the defaults.
360         Overriding all of `phpOptions` (including `upload_max_filesize`, `post_max_size`
361         and `memory_limit` which all point to [](#opt-services.nextcloud.maxUploadSize)
362         by default) can be done like this:
364         ```nix
365         {
366           services.nextcloud.phpOptions = lib.mkForce {
367             /* ... */
368           };
369         }
370         ```
371       '';
372     };
374     poolSettings = mkOption {
375       type = with types; attrsOf (oneOf [ str int bool ]);
376       default = {
377         "pm" = "dynamic";
378         "pm.max_children" = "32";
379         "pm.start_servers" = "2";
380         "pm.min_spare_servers" = "2";
381         "pm.max_spare_servers" = "4";
382         "pm.max_requests" = "500";
383       };
384       description = ''
385         Options for nextcloud's PHP pool. See the documentation on `php-fpm.conf` for details on configuration directives.
386       '';
387     };
389     poolConfig = mkOption {
390       type = types.nullOr types.lines;
391       default = null;
392       description = ''
393         Options for Nextcloud's PHP pool. See the documentation on `php-fpm.conf` for details on configuration directives.
394       '';
395     };
397     fastcgiTimeout = mkOption {
398       type = types.int;
399       default = 120;
400       description = ''
401         FastCGI timeout for database connection in seconds.
402       '';
403     };
405     database = {
407       createLocally = mkOption {
408         type = types.bool;
409         default = false;
410         description = ''
411           Whether to create the database and database user locally.
412         '';
413       };
415     };
417     config = {
418       dbtype = mkOption {
419         type = types.enum [ "sqlite" "pgsql" "mysql" ];
420         default = "sqlite";
421         description = "Database type.";
422       };
423       dbname = mkOption {
424         type = types.nullOr types.str;
425         default = "nextcloud";
426         description = "Database name.";
427       };
428       dbuser = mkOption {
429         type = types.nullOr types.str;
430         default = "nextcloud";
431         description = "Database user.";
432       };
433       dbpassFile = mkOption {
434         type = types.nullOr types.str;
435         default = null;
436         description = ''
437           The full path to a file that contains the database password.
438         '';
439       };
440       dbhost = mkOption {
441         type = types.nullOr types.str;
442         default =
443           if pgsqlLocal then "/run/postgresql"
444           else if mysqlLocal then "localhost:/run/mysqld/mysqld.sock"
445           else "localhost";
446         defaultText = "localhost";
447         example = "localhost:5000";
448         description = ''
449           Database host (+port) or socket path.
450           If [](#opt-services.nextcloud.database.createLocally) is true and
451           [](#opt-services.nextcloud.config.dbtype) is either `pgsql` or `mysql`,
452           defaults to the correct Unix socket instead.
453         '';
454       };
455       dbtableprefix = mkOption {
456         type = types.nullOr types.str;
457         default = null;
458         description = ''
459           Table prefix in Nextcloud's database.
461           __Note:__ since Nextcloud 20 it's not an option anymore to create a database
462           schema with a custom table prefix. This option only exists for backwards compatibility
463           with installations that were originally provisioned with Nextcloud <20.
464         '';
465       };
466       adminuser = mkOption {
467         type = types.str;
468         default = "root";
469         description = ''
470           Username for the admin account. The username is only set during the
471           initial setup of Nextcloud! Since the username also acts as unique
472           ID internally, it cannot be changed later!
473         '';
474       };
475       adminpassFile = mkOption {
476         type = types.str;
477         description = ''
478           The full path to a file that contains the admin's password. Must be
479           readable by user `nextcloud`. The password is set only in the initial
480           setup of Nextcloud by the systemd service `nextcloud-setup.service`.
481         '';
482       };
483       objectstore = {
484         s3 = {
485           enable = mkEnableOption ''
486             S3 object storage as primary storage.
488             This mounts a bucket on an Amazon S3 object storage or compatible
489             implementation into the virtual filesystem.
491             Further details about this feature can be found in the
492             [upstream documentation](https://docs.nextcloud.com/server/22/admin_manual/configuration_files/primary_storage.html)
493           '';
494           bucket = mkOption {
495             type = types.str;
496             example = "nextcloud";
497             description = ''
498               The name of the S3 bucket.
499             '';
500           };
501           autocreate = mkOption {
502             type = types.bool;
503             description = ''
504               Create the objectstore if it does not exist.
505             '';
506           };
507           key = mkOption {
508             type = types.str;
509             example = "EJ39ITYZEUH5BGWDRUFY";
510             description = ''
511               The access key for the S3 bucket.
512             '';
513           };
514           secretFile = mkOption {
515             type = types.str;
516             example = "/var/nextcloud-objectstore-s3-secret";
517             description = ''
518               The full path to a file that contains the access secret. Must be
519               readable by user `nextcloud`.
520             '';
521           };
522           hostname = mkOption {
523             type = types.nullOr types.str;
524             default = null;
525             example = "example.com";
526             description = ''
527               Required for some non-Amazon implementations.
528             '';
529           };
530           port = mkOption {
531             type = types.nullOr types.port;
532             default = null;
533             description = ''
534               Required for some non-Amazon implementations.
535             '';
536           };
537           useSsl = mkOption {
538             type = types.bool;
539             default = true;
540             description = ''
541               Use SSL for objectstore access.
542             '';
543           };
544           region = mkOption {
545             type = types.nullOr types.str;
546             default = null;
547             example = "REGION";
548             description = ''
549               Required for some non-Amazon implementations.
550             '';
551           };
552           usePathStyle = mkOption {
553             type = types.bool;
554             default = false;
555             description = ''
556               Required for some non-Amazon S3 implementations.
558               Ordinarily, requests will be made with
559               `http://bucket.hostname.domain/`, but with path style
560               enabled requests are made with
561               `http://hostname.domain/bucket` instead.
562             '';
563           };
564           sseCKeyFile = mkOption {
565             type = types.nullOr types.path;
566             default = null;
567             example = "/var/nextcloud-objectstore-s3-sse-c-key";
568             description = ''
569               If provided this is the full path to a file that contains the key
570               to enable [server-side encryption with customer-provided keys][1]
571               (SSE-C).
573               The file must contain a random 32-byte key encoded as a base64
574               string, e.g. generated with the command
576               ```
577               openssl rand 32 | base64
578               ```
580               Must be readable by user `nextcloud`.
582               [1]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/ServerSideEncryptionCustomerKeys.html
583             '';
584           };
585         };
586       };
587     };
589     enableImagemagick = mkEnableOption ''
590         the ImageMagick module for PHP.
591         This is used by the theming app and for generating previews of certain images (e.g. SVG and HEIF).
592         You may want to disable it for increased security. In that case, previews will still be available
593         for some images (e.g. JPEG and PNG).
594         See <https://github.com/nextcloud/server/issues/13099>
595     '' // {
596       default = true;
597     };
599     configureRedis = lib.mkOption {
600       type = lib.types.bool;
601       default = config.services.nextcloud.notify_push.enable;
602       defaultText = literalExpression "config.services.nextcloud.notify_push.enable";
603       description = ''
604         Whether to configure Nextcloud to use the recommended Redis settings for small instances.
606         ::: {.note}
607         The `notify_push` app requires Redis to be configured. If this option is turned off, this must be configured manually.
608         :::
609       '';
610     };
612     caching = {
613       apcu = mkOption {
614         type = types.bool;
615         default = true;
616         description = ''
617           Whether to load the APCu module into PHP.
618         '';
619       };
620       redis = mkOption {
621         type = types.bool;
622         default = false;
623         description = ''
624           Whether to load the Redis module into PHP.
625           You still need to enable Redis in your config.php.
626           See https://docs.nextcloud.com/server/14/admin_manual/configuration_server/caching_configuration.html
627         '';
628       };
629       memcached = mkOption {
630         type = types.bool;
631         default = false;
632         description = ''
633           Whether to load the Memcached module into PHP.
634           You still need to enable Memcached in your config.php.
635           See https://docs.nextcloud.com/server/14/admin_manual/configuration_server/caching_configuration.html
636         '';
637       };
638     };
639     autoUpdateApps = {
640       enable = mkOption {
641         type = types.bool;
642         default = false;
643         description = ''
644           Run a regular auto-update of all apps installed from the Nextcloud app store.
645         '';
646       };
647       startAt = mkOption {
648         type = with types; either str (listOf str);
649         default = "05:00:00";
650         example = "Sun 14:00:00";
651         description = ''
652           When to run the update. See `systemd.services.<name>.startAt`.
653         '';
654       };
655     };
656     occ = mkOption {
657       type = types.package;
658       default = occ;
659       defaultText = literalMD "generated script";
660       description = ''
661         The nextcloud-occ program preconfigured to target this Nextcloud instance.
662       '';
663     };
665     settings = mkOption {
666       type = types.submodule {
667         freeformType = jsonFormat.type;
668         options = {
670           loglevel = mkOption {
671             type = types.ints.between 0 4;
672             default = 2;
673             description = ''
674               Log level value between 0 (DEBUG) and 4 (FATAL).
676               - 0 (debug): Log all activity.
678               - 1 (info): Log activity such as user logins and file activities, plus warnings, errors, and fatal errors.
680               - 2 (warn): Log successful operations, as well as warnings of potential problems, errors and fatal errors.
682               - 3 (error): Log failed operations and fatal errors.
684               - 4 (fatal): Log only fatal errors that cause the server to stop.
685             '';
686           };
687           log_type = mkOption {
688             type = types.enum [ "errorlog" "file" "syslog" "systemd" ];
689             default = "syslog";
690             description = ''
691               Logging backend to use.
692               systemd requires the php-systemd package to be added to services.nextcloud.phpExtraExtensions.
693               See the [nextcloud documentation](https://docs.nextcloud.com/server/latest/admin_manual/configuration_server/logging_configuration.html) for details.
694             '';
695           };
696           skeletondirectory = mkOption {
697             default = "";
698             type = types.str;
699             description = ''
700               The directory where the skeleton files are located. These files will be
701               copied to the data directory of new users. Leave empty to not copy any
702               skeleton files.
703             '';
704           };
705           trusted_domains = mkOption {
706             type = types.listOf types.str;
707             default = [];
708             description = ''
709               Trusted domains, from which the nextcloud installation will be
710               accessible. You don't need to add
711               `services.nextcloud.hostname` here.
712             '';
713           };
714           trusted_proxies = mkOption {
715             type = types.listOf types.str;
716             default = [];
717             description = ''
718               Trusted proxies, to provide if the nextcloud installation is being
719               proxied to secure against e.g. spoofing.
720             '';
721           };
722           overwriteprotocol = mkOption {
723             type = types.enum [ "" "http" "https" ];
724             default = "";
725             example = "https";
726             description = ''
727               Force Nextcloud to always use HTTP or HTTPS i.e. for link generation.
728               Nextcloud uses the currently used protocol by default, but when
729               behind a reverse-proxy, it may use `http` for everything although
730               Nextcloud may be served via HTTPS.
731             '';
732           };
733           default_phone_region = mkOption {
734             default = "";
735             type = types.str;
736             example = "DE";
737             description = ''
738               An [ISO 3166-1](https://www.iso.org/iso-3166-country-codes.html)
739               country code which replaces automatic phone-number detection
740               without a country code.
742               As an example, with `DE` set as the default phone region,
743               the `+49` prefix can be omitted for phone numbers.
744             '';
745           };
746           "profile.enabled" = mkEnableOption "global profiles" // {
747             description = ''
748               Makes user-profiles globally available under `nextcloud.tld/u/user.name`.
749               Even though it's enabled by default in Nextcloud, it must be explicitly enabled
750               here because it has the side-effect that personal information is even accessible to
751               unauthenticated users by default.
752               By default, the following properties are set to “Show to everyone”
753               if this flag is enabled:
754               - About
755               - Full name
756               - Headline
757               - Organisation
758               - Profile picture
759               - Role
760               - Twitter
761               - Website
762               Only has an effect in Nextcloud 23 and later.
763             '';
764           };
765         };
766       };
767       default = {};
768       description = ''
769         Extra options which should be appended to Nextcloud's config.php file.
770       '';
771       example = literalExpression '' {
772         redis = {
773           host = "/run/redis/redis.sock";
774           port = 0;
775           dbindex = 0;
776           password = "secret";
777           timeout = 1.5;
778         };
779       } '';
780     };
782     secretFile = mkOption {
783       type = types.nullOr types.str;
784       default = null;
785       description = ''
786         Secret options which will be appended to Nextcloud's config.php file (written as JSON, in the same
787         form as the [](#opt-services.nextcloud.settings) option), for example
788         `{"redis":{"password":"secret"}}`.
789       '';
790     };
792     nginx = {
793       recommendedHttpHeaders = mkOption {
794         type = types.bool;
795         default = true;
796         description = "Enable additional recommended HTTP response headers";
797       };
798       hstsMaxAge = mkOption {
799         type = types.ints.positive;
800         default = 15552000;
801         description = ''
802           Value for the `max-age` directive of the HTTP
803           `Strict-Transport-Security` header.
805           See section 6.1.1 of IETF RFC 6797 for detailed information on this
806           directive and header.
807         '';
808       };
809     };
811     cli.memoryLimit = mkOption {
812       type = types.nullOr types.str;
813       default = null;
814       example = "1G";
815       description = ''
816         The `memory_limit` of PHP is equal to [](#opt-services.nextcloud.maxUploadSize).
817         The value can be customized for `nextcloud-cron.service` using this option.
818       '';
819     };
820   };
822   config = mkIf cfg.enable (mkMerge [
823     { warnings = let
824         latest = 30;
825         upgradeWarning = major: nixos:
826           ''
827             A legacy Nextcloud install (from before NixOS ${nixos}) may be installed.
829             After nextcloud${toString major} is installed successfully, you can safely upgrade
830             to ${toString (major + 1)}. The latest version available is Nextcloud${toString latest}.
832             Please note that Nextcloud doesn't support upgrades across multiple major versions
833             (i.e. an upgrade from 16 is possible to 17, but not 16 to 18).
835             The package can be upgraded by explicitly declaring the service-option
836             `services.nextcloud.package`.
837           '';
839       in (optional (cfg.poolConfig != null) ''
840           Using config.services.nextcloud.poolConfig is deprecated and will become unsupported in a future release.
841           Please migrate your configuration to config.services.nextcloud.poolSettings.
842         '')
843         ++ (optional (cfg.config.dbtableprefix != null) ''
844           Using `services.nextcloud.config.dbtableprefix` is deprecated. Fresh installations with this
845           option set are not allowed anymore since v20.
847           If you have an existing installation with a custom table prefix, make sure it is
848           set correctly in `config.php` and remove the option from your NixOS config.
849         '')
850         ++ (optional (versionOlder cfg.package.version "26") (upgradeWarning 25 "23.05"))
851         ++ (optional (versionOlder cfg.package.version "27") (upgradeWarning 26 "23.11"))
852         ++ (optional (versionOlder cfg.package.version "28") (upgradeWarning 27 "24.05"))
853         ++ (optional (versionOlder cfg.package.version "29") (upgradeWarning 28 "24.11"))
854         ++ (optional (versionOlder cfg.package.version "30") (upgradeWarning 29 "24.11"));
856       services.nextcloud.package = with pkgs;
857         mkDefault (
858           if pkgs ? nextcloud
859             then throw ''
860               The `pkgs.nextcloud`-attribute has been removed. If it's supposed to be the default
861               nextcloud defined in an overlay, please set `services.nextcloud.package` to
862               `pkgs.nextcloud`.
863             ''
864           else if versionOlder stateVersion "24.05" then nextcloud27
865           else if versionOlder stateVersion "24.11" then nextcloud29
866           else nextcloud30
867         );
869       services.nextcloud.phpPackage =
870         if versionOlder cfg.package.version "29" then pkgs.php82
871         else pkgs.php83;
873       services.nextcloud.phpOptions = mkMerge [
874         (mapAttrs (const mkOptionDefault) defaultPHPSettings)
875         {
876           upload_max_filesize = cfg.maxUploadSize;
877           post_max_size = cfg.maxUploadSize;
878           memory_limit = cfg.maxUploadSize;
879         }
880         (mkIf cfg.caching.apcu {
881           "apc.enable_cli" = "1";
882         })
883       ];
884     }
886     { assertions = [
887       { assertion = cfg.database.createLocally -> cfg.config.dbpassFile == null;
888         message = ''
889           Using `services.nextcloud.database.createLocally` with database
890           password authentication is no longer supported.
892           If you use an external database (or want to use password auth for any
893           other reason), set `services.nextcloud.database.createLocally` to
894           `false`. The database won't be managed for you (use `services.mysql`
895           if you want to set it up).
897           If you want this module to manage your nextcloud database for you,
898           unset `services.nextcloud.config.dbpassFile` and
899           `services.nextcloud.config.dbhost` to use socket authentication
900           instead of password.
901         '';
902       }
903     ]; }
905     { systemd.timers.nextcloud-cron = {
906         wantedBy = [ "timers.target" ];
907         after = [ "nextcloud-setup.service" ];
908         timerConfig = {
909           OnBootSec = "5m";
910           OnUnitActiveSec = "5m";
911           Unit = "nextcloud-cron.service";
912         };
913       };
915       systemd.tmpfiles.rules = map (dir: "d ${dir} 0750 nextcloud nextcloud - -") [
916         "${cfg.home}"
917         "${datadir}/config"
918         "${datadir}/data"
919         "${cfg.home}/store-apps"
920       ] ++ [
921         "L+ ${datadir}/config/override.config.php - - - - ${overrideConfig}"
922       ];
924       systemd.services = {
925         # When upgrading the Nextcloud package, Nextcloud can report errors such as
926         # "The files of the app [all apps in /var/lib/nextcloud/apps] were not replaced correctly"
927         # Restarting phpfpm on Nextcloud package update fixes these issues (but this is a workaround).
928         phpfpm-nextcloud.restartTriggers = [ webroot overrideConfig ];
930         nextcloud-setup = let
931           c = cfg.config;
932           occInstallCmd = let
933             mkExport = { arg, value }: "export ${arg}=${value}";
934             dbpass = {
935               arg = "DBPASS";
936               value = if c.dbpassFile != null
937                 then ''"$(<"${toString c.dbpassFile}")"''
938                 else ''""'';
939             };
940             adminpass = {
941               arg = "ADMINPASS";
942               value = ''"$(<"${toString c.adminpassFile}")"'';
943             };
944             installFlags = concatStringsSep " \\\n    "
945               (mapAttrsToList (k: v: "${k} ${toString v}") {
946               "--database" = ''"${c.dbtype}"'';
947               # The following attributes are optional depending on the type of
948               # database.  Those that evaluate to null on the left hand side
949               # will be omitted.
950               ${if c.dbname != null then "--database-name" else null} = ''"${c.dbname}"'';
951               ${if c.dbhost != null then "--database-host" else null} = ''"${c.dbhost}"'';
952               ${if c.dbuser != null then "--database-user" else null} = ''"${c.dbuser}"'';
953               "--database-pass" = "\"\$${dbpass.arg}\"";
954               "--admin-user" = ''"${c.adminuser}"'';
955               "--admin-pass" = "\"\$${adminpass.arg}\"";
956               "--data-dir" = ''"${datadir}/data"'';
957             });
958           in ''
959             ${mkExport dbpass}
960             ${mkExport adminpass}
961             ${occ}/bin/nextcloud-occ maintenance:install \
962                 ${installFlags}
963           '';
964           occSetTrustedDomainsCmd = concatStringsSep "\n" (imap0
965             (i: v: ''
966               ${occ}/bin/nextcloud-occ config:system:set trusted_domains \
967                 ${toString i} --value="${toString v}"
968             '') ([ cfg.hostName ] ++ cfg.settings.trusted_domains));
970         in {
971           wantedBy = [ "multi-user.target" ];
972           wants = [ "nextcloud-update-db.service" ];
973           before = [ "phpfpm-nextcloud.service" ];
974           after = optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service";
975           requires = optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service";
976           path = [ occ ];
977           restartTriggers = [ overrideConfig ];
978           script = ''
979             ${optionalString (c.dbpassFile != null) ''
980               if [ ! -r "${c.dbpassFile}" ]; then
981                 echo "dbpassFile ${c.dbpassFile} is not readable by nextcloud:nextcloud! Aborting..."
982                 exit 1
983               fi
984               if [ -z "$(<${c.dbpassFile})" ]; then
985                 echo "dbpassFile ${c.dbpassFile} is empty!"
986                 exit 1
987               fi
988             ''}
989             if [ ! -r "${c.adminpassFile}" ]; then
990               echo "adminpassFile ${c.adminpassFile} is not readable by nextcloud:nextcloud! Aborting..."
991               exit 1
992             fi
993             if [ -z "$(<${c.adminpassFile})" ]; then
994               echo "adminpassFile ${c.adminpassFile} is empty!"
995               exit 1
996             fi
998             ${concatMapStrings (name: ''
999               if [ -d "${cfg.home}"/${name} ]; then
1000                 echo "Cleaning up ${name}; these are now bundled in the webroot store-path!"
1001                 rm -r "${cfg.home}"/${name}
1002               fi
1003             '') [ "nix-apps" "apps" ]}
1005             # Do not install if already installed
1006             if [[ ! -e ${datadir}/config/config.php ]]; then
1007               ${occInstallCmd}
1008             fi
1010             ${occ}/bin/nextcloud-occ upgrade
1012             ${occ}/bin/nextcloud-occ config:system:delete trusted_domains
1014             ${optionalString (cfg.extraAppsEnable && cfg.extraApps != { }) ''
1015                 # Try to enable apps
1016                 ${occ}/bin/nextcloud-occ app:enable ${concatStringsSep " " (attrNames cfg.extraApps)}
1017             ''}
1019             ${occSetTrustedDomainsCmd}
1020           '';
1021           serviceConfig.Type = "oneshot";
1022           serviceConfig.User = "nextcloud";
1023           # On Nextcloud ≥ 26, it is not necessary to patch the database files to prevent
1024           # an automatic creation of the database user.
1025           environment.NC_setup_create_db_user = lib.mkIf (nextcloudGreaterOrEqualThan "26") "false";
1026         };
1027         nextcloud-cron = {
1028           after = [ "nextcloud-setup.service" ];
1029           environment.NEXTCLOUD_CONFIG_DIR = "${datadir}/config";
1030           serviceConfig = {
1031             Type = "exec";
1032             User = "nextcloud";
1033             ExecCondition = "${phpCli} -f ${webroot}/occ status -e";
1034             ExecStart = "${phpCli} -f ${webroot}/cron.php";
1035             KillMode = "process";
1036           };
1037         };
1038         nextcloud-update-plugins = mkIf cfg.autoUpdateApps.enable {
1039           after = [ "nextcloud-setup.service" ];
1040           serviceConfig = {
1041             Type = "oneshot";
1042             ExecStart = "${occ}/bin/nextcloud-occ app:update --all";
1043             User = "nextcloud";
1044           };
1045           startAt = cfg.autoUpdateApps.startAt;
1046         };
1047         nextcloud-update-db = {
1048           after = [ "nextcloud-setup.service" ];
1049           environment.NEXTCLOUD_CONFIG_DIR = "${datadir}/config";
1050           script = ''
1051             ${occ}/bin/nextcloud-occ db:add-missing-columns
1052             ${occ}/bin/nextcloud-occ db:add-missing-indices
1053             ${occ}/bin/nextcloud-occ db:add-missing-primary-keys
1054           '';
1055           serviceConfig = {
1056             Type = "exec";
1057             User = "nextcloud";
1058             ExecCondition = "${phpCli} -f ${webroot}/occ status -e";
1059           };
1060         };
1061       };
1063       services.phpfpm = {
1064         pools.nextcloud = {
1065           user = "nextcloud";
1066           group = "nextcloud";
1067           phpPackage = phpPackage;
1068           phpEnv = {
1069             NEXTCLOUD_CONFIG_DIR = "${datadir}/config";
1070             PATH = "/run/wrappers/bin:/nix/var/nix/profiles/default/bin:/run/current-system/sw/bin:/usr/bin:/bin";
1071           };
1072           settings = mapAttrs (name: mkDefault) {
1073             "listen.owner" = config.services.nginx.user;
1074             "listen.group" = config.services.nginx.group;
1075           } // cfg.poolSettings;
1076           extraConfig = cfg.poolConfig;
1077         };
1078       };
1080       users.users.nextcloud = {
1081         home = "${cfg.home}";
1082         group = "nextcloud";
1083         isSystemUser = true;
1084       };
1085       users.groups.nextcloud.members = [ "nextcloud" config.services.nginx.user ];
1087       environment.systemPackages = [ occ ];
1089       services.mysql = lib.mkIf mysqlLocal {
1090         enable = true;
1091         package = lib.mkDefault pkgs.mariadb;
1092         ensureDatabases = [ cfg.config.dbname ];
1093         ensureUsers = [{
1094           name = cfg.config.dbuser;
1095           ensurePermissions = { "${cfg.config.dbname}.*" = "ALL PRIVILEGES"; };
1096         }];
1097       };
1099       services.postgresql = mkIf pgsqlLocal {
1100         enable = true;
1101         ensureDatabases = [ cfg.config.dbname ];
1102         ensureUsers = [{
1103           name = cfg.config.dbuser;
1104           ensureDBOwnership = true;
1105         }];
1106       };
1108       services.redis.servers.nextcloud = lib.mkIf cfg.configureRedis {
1109         enable = true;
1110         user = "nextcloud";
1111       };
1113       services.nextcloud = {
1114         caching.redis = lib.mkIf cfg.configureRedis true;
1115         settings = mkMerge [({
1116           datadirectory = lib.mkDefault "${datadir}/data";
1117           trusted_domains = [ cfg.hostName ];
1118         }) (lib.mkIf cfg.configureRedis {
1119           "memcache.distributed" = ''\OC\Memcache\Redis'';
1120           "memcache.locking" = ''\OC\Memcache\Redis'';
1121           redis = {
1122             host = config.services.redis.servers.nextcloud.unixSocket;
1123             port = 0;
1124           };
1125         })];
1126       };
1128       services.nginx.enable = mkDefault true;
1130       services.nginx.virtualHosts.${cfg.hostName} = {
1131         root = webroot;
1132         locations = {
1133           "= /robots.txt" = {
1134             priority = 100;
1135             extraConfig = ''
1136               allow all;
1137               access_log off;
1138             '';
1139           };
1140           "= /" = {
1141             priority = 100;
1142             extraConfig = ''
1143               if ( $http_user_agent ~ ^DavClnt ) {
1144                 return 302 /remote.php/webdav/$is_args$args;
1145               }
1146             '';
1147           };
1148           "^~ /.well-known" = {
1149             priority = 210;
1150             extraConfig = ''
1151               absolute_redirect off;
1152               location = /.well-known/carddav {
1153                 return 301 /remote.php/dav/;
1154               }
1155               location = /.well-known/caldav {
1156                 return 301 /remote.php/dav/;
1157               }
1158               location ~ ^/\.well-known/(?!acme-challenge|pki-validation) {
1159                 return 301 /index.php$request_uri;
1160               }
1161               try_files $uri $uri/ =404;
1162             '';
1163           };
1164           "~ ^/(?:build|tests|config|lib|3rdparty|templates|data)(?:$|/)" = {
1165             priority = 450;
1166             extraConfig = ''
1167               return 404;
1168             '';
1169           };
1170           "~ ^/(?:\\.|autotest|occ|issue|indie|db_|console)" = {
1171             priority = 450;
1172             extraConfig = ''
1173               return 404;
1174             '';
1175           };
1176           "~ \\.php(?:$|/)" = {
1177             priority = 500;
1178             extraConfig = ''
1179               # legacy support (i.e. static files and directories in cfg.package)
1180               rewrite ^/(?!index|remote|public|cron|core\/ajax\/update|status|ocs\/v[12]|updater\/.+|oc[s${optionalString (!ocmProviderIsNotAStaticDirAnymore) "m"}]-provider\/.+|.+\/richdocumentscode\/proxy) /index.php$request_uri;
1181               include ${config.services.nginx.package}/conf/fastcgi.conf;
1182               fastcgi_split_path_info ^(.+?\.php)(\\/.*)$;
1183               set $path_info $fastcgi_path_info;
1184               try_files $fastcgi_script_name =404;
1185               fastcgi_param PATH_INFO $path_info;
1186               fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
1187               fastcgi_param HTTPS ${if cfg.https then "on" else "off"};
1188               fastcgi_param modHeadersAvailable true;
1189               fastcgi_param front_controller_active true;
1190               fastcgi_pass unix:${fpm.socket};
1191               fastcgi_intercept_errors on;
1192               fastcgi_request_buffering off;
1193               fastcgi_read_timeout ${builtins.toString cfg.fastcgiTimeout}s;
1194             '';
1195           };
1196           "~ \\.(?:css|js|mjs|svg|gif|png|jpg|jpeg|ico|wasm|tflite|map|html|ttf|bcmap|mp4|webm|ogg|flac)$".extraConfig = ''
1197             try_files $uri /index.php$request_uri;
1198             expires 6M;
1199             access_log off;
1200             location ~ \.mjs$ {
1201               default_type text/javascript;
1202             }
1203             location ~ \.wasm$ {
1204               default_type application/wasm;
1205             }
1206           '';
1207           "~ ^\\/(?:updater|ocs-provider${optionalString (!ocmProviderIsNotAStaticDirAnymore) "|ocm-provider"})(?:$|\\/)".extraConfig = ''
1208             try_files $uri/ =404;
1209             index index.php;
1210           '';
1211           "/remote" = {
1212             priority = 1500;
1213             extraConfig = ''
1214               return 301 /remote.php$request_uri;
1215             '';
1216           };
1217           "/" = {
1218             priority = 1600;
1219             extraConfig = ''
1220               try_files $uri $uri/ /index.php$request_uri;
1221             '';
1222           };
1223         };
1224         extraConfig = ''
1225           index index.php index.html /index.php$request_uri;
1226           ${optionalString (cfg.nginx.recommendedHttpHeaders) ''
1227             add_header X-Content-Type-Options nosniff;
1228             add_header X-XSS-Protection "1; mode=block";
1229             add_header X-Robots-Tag "noindex, nofollow";
1230             add_header X-Download-Options noopen;
1231             add_header X-Permitted-Cross-Domain-Policies none;
1232             add_header X-Frame-Options sameorigin;
1233             add_header Referrer-Policy no-referrer;
1234           ''}
1235           ${optionalString (cfg.https) ''
1236             add_header Strict-Transport-Security "max-age=${toString cfg.nginx.hstsMaxAge}; includeSubDomains" always;
1237           ''}
1238           client_max_body_size ${cfg.maxUploadSize};
1239           fastcgi_buffers 64 4K;
1240           fastcgi_hide_header X-Powered-By;
1241           gzip on;
1242           gzip_vary on;
1243           gzip_comp_level 4;
1244           gzip_min_length 256;
1245           gzip_proxied expired no-cache no-store private no_last_modified no_etag auth;
1246           gzip_types application/atom+xml application/javascript application/json application/ld+json application/manifest+json application/rss+xml application/vnd.geo+json application/vnd.ms-fontobject application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/bmp image/svg+xml image/x-icon text/cache-manifest text/css text/plain text/vcard text/vnd.rim.location.xloc text/vtt text/x-component text/x-cross-domain-policy;
1248           ${optionalString cfg.webfinger ''
1249             rewrite ^/.well-known/host-meta /public.php?service=host-meta last;
1250             rewrite ^/.well-known/host-meta.json /public.php?service=host-meta-json last;
1251           ''}
1252         '';
1253       };
1254     }
1255   ]);
1257   meta.doc = ./nextcloud.md;