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