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