base16-schemes: unstable-2024-06-21 -> unstable-2024-11-12
[NixPkgs.git] / nixos / modules / services / web-apps / nextcloud.nix
blob8cb4b4c439f9d158756e1bc04dc3d78782b14c0a
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 }: ''
934               ${arg}=${value};
935               export ${arg};
936             '';
937             dbpass = {
938               arg = "DBPASS";
939               value = if c.dbpassFile != null
940                 then ''"$(<"${toString c.dbpassFile}")"''
941                 else ''""'';
942             };
943             adminpass = {
944               arg = "ADMINPASS";
945               value = ''"$(<"${toString c.adminpassFile}")"'';
946             };
947             installFlags = concatStringsSep " \\\n    "
948               (mapAttrsToList (k: v: "${k} ${toString v}") {
949               "--database" = ''"${c.dbtype}"'';
950               # The following attributes are optional depending on the type of
951               # database.  Those that evaluate to null on the left hand side
952               # will be omitted.
953               ${if c.dbname != null then "--database-name" else null} = ''"${c.dbname}"'';
954               ${if c.dbhost != null then "--database-host" else null} = ''"${c.dbhost}"'';
955               ${if c.dbuser != null then "--database-user" else null} = ''"${c.dbuser}"'';
956               "--database-pass" = "\"\$${dbpass.arg}\"";
957               "--admin-user" = ''"${c.adminuser}"'';
958               "--admin-pass" = "\"\$${adminpass.arg}\"";
959               "--data-dir" = ''"${datadir}/data"'';
960             });
961           in ''
962             ${mkExport dbpass}
963             ${mkExport adminpass}
964             ${occ}/bin/nextcloud-occ maintenance:install \
965                 ${installFlags}
966           '';
967           occSetTrustedDomainsCmd = concatStringsSep "\n" (imap0
968             (i: v: ''
969               ${occ}/bin/nextcloud-occ config:system:set trusted_domains \
970                 ${toString i} --value="${toString v}"
971             '') ([ cfg.hostName ] ++ cfg.settings.trusted_domains));
973         in {
974           wantedBy = [ "multi-user.target" ];
975           wants = [ "nextcloud-update-db.service" ];
976           before = [ "phpfpm-nextcloud.service" ];
977           after = optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service";
978           requires = optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service";
979           path = [ occ ];
980           restartTriggers = [ overrideConfig ];
981           script = ''
982             ${optionalString (c.dbpassFile != null) ''
983               if [ ! -r "${c.dbpassFile}" ]; then
984                 echo "dbpassFile ${c.dbpassFile} is not readable by nextcloud:nextcloud! Aborting..."
985                 exit 1
986               fi
987               if [ -z "$(<${c.dbpassFile})" ]; then
988                 echo "dbpassFile ${c.dbpassFile} is empty!"
989                 exit 1
990               fi
991             ''}
992             if [ ! -r "${c.adminpassFile}" ]; then
993               echo "adminpassFile ${c.adminpassFile} is not readable by nextcloud:nextcloud! Aborting..."
994               exit 1
995             fi
996             if [ -z "$(<${c.adminpassFile})" ]; then
997               echo "adminpassFile ${c.adminpassFile} is empty!"
998               exit 1
999             fi
1001             ${concatMapStrings (name: ''
1002               if [ -d "${cfg.home}"/${name} ]; then
1003                 echo "Cleaning up ${name}; these are now bundled in the webroot store-path!"
1004                 rm -r "${cfg.home}"/${name}
1005               fi
1006             '') [ "nix-apps" "apps" ]}
1008             # Do not install if already installed
1009             if [[ ! -e ${datadir}/config/config.php ]]; then
1010               ${occInstallCmd}
1011             fi
1013             ${occ}/bin/nextcloud-occ upgrade
1015             ${occ}/bin/nextcloud-occ config:system:delete trusted_domains
1017             ${optionalString (cfg.extraAppsEnable && cfg.extraApps != { }) ''
1018                 # Try to enable apps
1019                 ${occ}/bin/nextcloud-occ app:enable ${concatStringsSep " " (attrNames cfg.extraApps)}
1020             ''}
1022             ${occSetTrustedDomainsCmd}
1023           '';
1024           serviceConfig.Type = "oneshot";
1025           serviceConfig.User = "nextcloud";
1026           # On Nextcloud ≥ 26, it is not necessary to patch the database files to prevent
1027           # an automatic creation of the database user.
1028           environment.NC_setup_create_db_user = lib.mkIf (nextcloudGreaterOrEqualThan "26") "false";
1029         };
1030         nextcloud-cron = {
1031           after = [ "nextcloud-setup.service" ];
1032           environment.NEXTCLOUD_CONFIG_DIR = "${datadir}/config";
1033           serviceConfig = {
1034             Type = "exec";
1035             User = "nextcloud";
1036             ExecCondition = "${phpCli} -f ${webroot}/occ status -e";
1037             ExecStart = "${phpCli} -f ${webroot}/cron.php";
1038             KillMode = "process";
1039           };
1040         };
1041         nextcloud-update-plugins = mkIf cfg.autoUpdateApps.enable {
1042           after = [ "nextcloud-setup.service" ];
1043           serviceConfig = {
1044             Type = "oneshot";
1045             ExecStart = "${occ}/bin/nextcloud-occ app:update --all";
1046             User = "nextcloud";
1047           };
1048           startAt = cfg.autoUpdateApps.startAt;
1049         };
1050         nextcloud-update-db = {
1051           after = [ "nextcloud-setup.service" ];
1052           environment.NEXTCLOUD_CONFIG_DIR = "${datadir}/config";
1053           script = ''
1054             ${occ}/bin/nextcloud-occ db:add-missing-columns
1055             ${occ}/bin/nextcloud-occ db:add-missing-indices
1056             ${occ}/bin/nextcloud-occ db:add-missing-primary-keys
1057           '';
1058           serviceConfig = {
1059             Type = "exec";
1060             User = "nextcloud";
1061             ExecCondition = "${phpCli} -f ${webroot}/occ status -e";
1062           };
1063         };
1064       };
1066       services.phpfpm = {
1067         pools.nextcloud = {
1068           user = "nextcloud";
1069           group = "nextcloud";
1070           phpPackage = phpPackage;
1071           phpEnv = {
1072             NEXTCLOUD_CONFIG_DIR = "${datadir}/config";
1073             PATH = "/run/wrappers/bin:/nix/var/nix/profiles/default/bin:/run/current-system/sw/bin:/usr/bin:/bin";
1074           };
1075           settings = mapAttrs (name: mkDefault) {
1076             "listen.owner" = config.services.nginx.user;
1077             "listen.group" = config.services.nginx.group;
1078           } // cfg.poolSettings;
1079           extraConfig = cfg.poolConfig;
1080         };
1081       };
1083       users.users.nextcloud = {
1084         home = "${cfg.home}";
1085         group = "nextcloud";
1086         isSystemUser = true;
1087       };
1088       users.groups.nextcloud.members = [ "nextcloud" config.services.nginx.user ];
1090       environment.systemPackages = [ occ ];
1092       services.mysql = lib.mkIf mysqlLocal {
1093         enable = true;
1094         package = lib.mkDefault pkgs.mariadb;
1095         ensureDatabases = [ cfg.config.dbname ];
1096         ensureUsers = [{
1097           name = cfg.config.dbuser;
1098           ensurePermissions = { "${cfg.config.dbname}.*" = "ALL PRIVILEGES"; };
1099         }];
1100       };
1102       services.postgresql = mkIf pgsqlLocal {
1103         enable = true;
1104         ensureDatabases = [ cfg.config.dbname ];
1105         ensureUsers = [{
1106           name = cfg.config.dbuser;
1107           ensureDBOwnership = true;
1108         }];
1109       };
1111       services.redis.servers.nextcloud = lib.mkIf cfg.configureRedis {
1112         enable = true;
1113         user = "nextcloud";
1114       };
1116       services.nextcloud = {
1117         caching.redis = lib.mkIf cfg.configureRedis true;
1118         settings = mkMerge [({
1119           datadirectory = lib.mkDefault "${datadir}/data";
1120           trusted_domains = [ cfg.hostName ];
1121         }) (lib.mkIf cfg.configureRedis {
1122           "memcache.distributed" = ''\OC\Memcache\Redis'';
1123           "memcache.locking" = ''\OC\Memcache\Redis'';
1124           redis = {
1125             host = config.services.redis.servers.nextcloud.unixSocket;
1126             port = 0;
1127           };
1128         })];
1129       };
1131       services.nginx.enable = mkDefault true;
1133       services.nginx.virtualHosts.${cfg.hostName} = {
1134         root = webroot;
1135         locations = {
1136           "= /robots.txt" = {
1137             priority = 100;
1138             extraConfig = ''
1139               allow all;
1140               access_log off;
1141             '';
1142           };
1143           "= /" = {
1144             priority = 100;
1145             extraConfig = ''
1146               if ( $http_user_agent ~ ^DavClnt ) {
1147                 return 302 /remote.php/webdav/$is_args$args;
1148               }
1149             '';
1150           };
1151           "^~ /.well-known" = {
1152             priority = 210;
1153             extraConfig = ''
1154               absolute_redirect off;
1155               location = /.well-known/carddav {
1156                 return 301 /remote.php/dav/;
1157               }
1158               location = /.well-known/caldav {
1159                 return 301 /remote.php/dav/;
1160               }
1161               location ~ ^/\.well-known/(?!acme-challenge|pki-validation) {
1162                 return 301 /index.php$request_uri;
1163               }
1164               try_files $uri $uri/ =404;
1165             '';
1166           };
1167           "~ ^/(?:build|tests|config|lib|3rdparty|templates|data)(?:$|/)" = {
1168             priority = 450;
1169             extraConfig = ''
1170               return 404;
1171             '';
1172           };
1173           "~ ^/(?:\\.|autotest|occ|issue|indie|db_|console)" = {
1174             priority = 450;
1175             extraConfig = ''
1176               return 404;
1177             '';
1178           };
1179           "~ \\.php(?:$|/)" = {
1180             priority = 500;
1181             extraConfig = ''
1182               # legacy support (i.e. static files and directories in cfg.package)
1183               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;
1184               include ${config.services.nginx.package}/conf/fastcgi.conf;
1185               fastcgi_split_path_info ^(.+?\.php)(\\/.*)$;
1186               set $path_info $fastcgi_path_info;
1187               try_files $fastcgi_script_name =404;
1188               fastcgi_param PATH_INFO $path_info;
1189               fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
1190               fastcgi_param HTTPS ${if cfg.https then "on" else "off"};
1191               fastcgi_param modHeadersAvailable true;
1192               fastcgi_param front_controller_active true;
1193               fastcgi_pass unix:${fpm.socket};
1194               fastcgi_intercept_errors on;
1195               fastcgi_request_buffering off;
1196               fastcgi_read_timeout ${builtins.toString cfg.fastcgiTimeout}s;
1197             '';
1198           };
1199           "~ \\.(?:css|js|mjs|svg|gif|png|jpg|jpeg|ico|wasm|tflite|map|html|ttf|bcmap|mp4|webm|ogg|flac)$".extraConfig = ''
1200             try_files $uri /index.php$request_uri;
1201             expires 6M;
1202             access_log off;
1203             location ~ \.mjs$ {
1204               default_type text/javascript;
1205             }
1206             location ~ \.wasm$ {
1207               default_type application/wasm;
1208             }
1209           '';
1210           "~ ^\\/(?:updater|ocs-provider${optionalString (!ocmProviderIsNotAStaticDirAnymore) "|ocm-provider"})(?:$|\\/)".extraConfig = ''
1211             try_files $uri/ =404;
1212             index index.php;
1213           '';
1214           "/remote" = {
1215             priority = 1500;
1216             extraConfig = ''
1217               return 301 /remote.php$request_uri;
1218             '';
1219           };
1220           "/" = {
1221             priority = 1600;
1222             extraConfig = ''
1223               try_files $uri $uri/ /index.php$request_uri;
1224             '';
1225           };
1226         };
1227         extraConfig = ''
1228           index index.php index.html /index.php$request_uri;
1229           ${optionalString (cfg.nginx.recommendedHttpHeaders) ''
1230             add_header X-Content-Type-Options nosniff;
1231             add_header X-XSS-Protection "1; mode=block";
1232             add_header X-Robots-Tag "noindex, nofollow";
1233             add_header X-Download-Options noopen;
1234             add_header X-Permitted-Cross-Domain-Policies none;
1235             add_header X-Frame-Options sameorigin;
1236             add_header Referrer-Policy no-referrer;
1237           ''}
1238           ${optionalString (cfg.https) ''
1239             add_header Strict-Transport-Security "max-age=${toString cfg.nginx.hstsMaxAge}; includeSubDomains" always;
1240           ''}
1241           client_max_body_size ${cfg.maxUploadSize};
1242           fastcgi_buffers 64 4K;
1243           fastcgi_hide_header X-Powered-By;
1244           gzip on;
1245           gzip_vary on;
1246           gzip_comp_level 4;
1247           gzip_min_length 256;
1248           gzip_proxied expired no-cache no-store private no_last_modified no_etag auth;
1249           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;
1251           ${optionalString cfg.webfinger ''
1252             rewrite ^/.well-known/host-meta /public.php?service=host-meta last;
1253             rewrite ^/.well-known/host-meta.json /public.php?service=host-meta-json last;
1254           ''}
1255         '';
1256       };
1257     }
1258   ]);
1260   meta.doc = ./nextcloud.md;