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