diff --git a/nixos/modules/services/web-apps/nextcloud.nix b/nixos/modules/services/web-apps/nextcloud.nix index 1e9360dc1f92..658666680340 100644 --- a/nixos/modules/services/web-apps/nextcloud.nix +++ b/nixos/modules/services/web-apps/nextcloud.nix @@ -5,11 +5,6 @@ ... }: let - inherit (lib) - mkIf - mkMerge - ; - cfg = config.services.nextcloud; overridePackage = cfg.package.override { @@ -1024,543 +1019,545 @@ in }; }; - config = mkIf cfg.enable (mkMerge [ - { - warnings = - let - latest = 31; - upgradeWarning = major: nixos: '' - A legacy Nextcloud install (from before NixOS ${nixos}) may be installed. - - After nextcloud${toString major} is installed successfully, you can safely upgrade - to ${toString (major + 1)}. The latest version available is Nextcloud${toString latest}. - - Please note that Nextcloud doesn't support upgrades across multiple major versions - (i.e. an upgrade from 16 is possible to 17, but not 16 to 18). - - The package can be upgraded by explicitly declaring the service-option - `services.nextcloud.package`. - ''; - - in - (lib.optional (cfg.poolConfig != null) '' - Using config.services.nextcloud.poolConfig is deprecated and will become unsupported in a future release. - Please migrate your configuration to config.services.nextcloud.poolSettings. - '') - ++ (lib.optional (cfg.config.dbtableprefix != null) '' - Using `services.nextcloud.config.dbtableprefix` is deprecated. Fresh installations with this - option set are not allowed anymore since v20. - - If you have an existing installation with a custom table prefix, make sure it is - set correctly in `config.php` and remove the option from your NixOS config. - '') - ++ (lib.optional (lib.versionOlder overridePackage.version "26") (upgradeWarning 25 "23.05")) - ++ (lib.optional (lib.versionOlder overridePackage.version "27") (upgradeWarning 26 "23.11")) - ++ (lib.optional (lib.versionOlder overridePackage.version "28") (upgradeWarning 27 "24.05")) - ++ (lib.optional (lib.versionOlder overridePackage.version "29") (upgradeWarning 28 "24.11")) - ++ (lib.optional (lib.versionOlder overridePackage.version "30") (upgradeWarning 29 "24.11")) - ++ (lib.optional (lib.versionOlder overridePackage.version "31") (upgradeWarning 30 "25.05")); - - services.nextcloud.package = lib.mkDefault ( - if pkgs ? nextcloud then - throw '' - The `pkgs.nextcloud`-attribute has been removed. If it's supposed to be the default - nextcloud defined in an overlay, please set `services.nextcloud.package` to - `pkgs.nextcloud`. - '' - else if lib.versionOlder stateVersion "24.05" then - pkgs.nextcloud27 - else if lib.versionOlder stateVersion "24.11" then - pkgs.nextcloud29 - else if lib.versionOlder stateVersion "25.05" then - pkgs.nextcloud30 - else - pkgs.nextcloud31 - ); - - services.nextcloud.phpOptions = mkMerge [ - (lib.mapAttrs (lib.const lib.mkOptionDefault) defaultPHPSettings) - { - upload_max_filesize = cfg.maxUploadSize; - post_max_size = cfg.maxUploadSize; - memory_limit = cfg.maxUploadSize; - } - (mkIf cfg.caching.apcu { - "apc.enable_cli" = "1"; - }) - ]; - } - - { - assertions = [ - { - assertion = cfg.database.createLocally -> cfg.config.dbpassFile == null; - message = '' - Using `services.nextcloud.database.createLocally` with database - password authentication is no longer supported. - - If you use an external database (or want to use password auth for any - other reason), set `services.nextcloud.database.createLocally` to - `false`. The database won't be managed for you (use `services.mysql` - if you want to set it up). - - If you want this module to manage your nextcloud database for you, - unset `services.nextcloud.config.dbpassFile` and - `services.nextcloud.config.dbhost` to use socket authentication - instead of password. - ''; - } - { - assertion = cfg.config.dbtype != null; - message = '' - `services.nextcloud.config.dbtype` must be set explicitly (pgsql, mysql, or sqlite) - - Before 25.05, it used to default to sqlite but that is not recommended by upstream. - Either set it to sqlite as it used to be, or convert to another type as described - in the official db conversion page: - https://docs.nextcloud.com/server/latest/admin_manual/configuration_database/db_conversion.html - ''; - } - ]; - } - - { - systemd.timers.nextcloud-cron = { - wantedBy = [ "timers.target" ]; - after = [ "nextcloud-setup.service" ]; - timerConfig = { - OnBootSec = "5m"; - OnUnitActiveSec = "5m"; - Unit = "nextcloud-cron.service"; - }; - }; - - systemd.tmpfiles.rules = - map (dir: "d ${dir} 0750 nextcloud nextcloud - -") [ - "${cfg.home}" - "${datadir}/config" - "${datadir}/data" - "${cfg.home}/store-apps" - ] - ++ [ - "L+ ${datadir}/config/override.config.php - - - - ${overrideConfig}" - ]; - - services.nextcloud.finalPackage = webroot; - - systemd.services = { - nextcloud-setup = + config = lib.mkIf cfg.enable ( + lib.mkMerge [ + { + warnings = let - c = cfg.config; - occInstallCmd = - let - mkExport = - { arg, value }: - '' - ${arg}=${value}; - export ${arg}; - ''; - dbpass = { - arg = "DBPASS"; - value = if c.dbpassFile != null then ''"$(<"$CREDENTIALS_DIRECTORY/dbpass")"'' else ''""''; - }; - adminpass = { - arg = "ADMINPASS"; - value = ''"$(<"$CREDENTIALS_DIRECTORY/adminpass")"''; - }; - installFlags = lib.concatStringsSep " \\\n " ( - lib.mapAttrsToList (k: v: "${k} ${toString v}") { - "--database" = ''"${c.dbtype}"''; - # The following attributes are optional depending on the type of - # database. Those that evaluate to null on the left hand side - # will be omitted. - ${if c.dbname != null then "--database-name" else null} = ''"${c.dbname}"''; - ${if c.dbhost != null then "--database-host" else null} = ''"${c.dbhost}"''; - ${if c.dbuser != null then "--database-user" else null} = ''"${c.dbuser}"''; - "--database-pass" = "\"\$${dbpass.arg}\""; - "--admin-user" = ''"${c.adminuser}"''; - "--admin-pass" = "\"\$${adminpass.arg}\""; - "--data-dir" = ''"${datadir}/data"''; - } - ); - in - '' - ${mkExport dbpass} - ${mkExport adminpass} - ${lib.getExe occ} maintenance:install \ - ${installFlags} - ''; - occSetTrustedDomainsCmd = lib.concatStringsSep "\n" ( - lib.imap0 (i: v: '' - ${lib.getExe occ} config:system:set trusted_domains \ - ${toString i} --value="${toString v}" - '') (lib.unique ([ cfg.hostName ] ++ cfg.settings.trusted_domains)) - ); + latest = 31; + upgradeWarning = major: nixos: '' + A legacy Nextcloud install (from before NixOS ${nixos}) may be installed. + + After nextcloud${toString major} is installed successfully, you can safely upgrade + to ${toString (major + 1)}. The latest version available is Nextcloud${toString latest}. + + Please note that Nextcloud doesn't support upgrades across multiple major versions + (i.e. an upgrade from 16 is possible to 17, but not 16 to 18). + + The package can be upgraded by explicitly declaring the service-option + `services.nextcloud.package`. + ''; in + (lib.optional (cfg.poolConfig != null) '' + Using config.services.nextcloud.poolConfig is deprecated and will become unsupported in a future release. + Please migrate your configuration to config.services.nextcloud.poolSettings. + '') + ++ (lib.optional (cfg.config.dbtableprefix != null) '' + Using `services.nextcloud.config.dbtableprefix` is deprecated. Fresh installations with this + option set are not allowed anymore since v20. + + If you have an existing installation with a custom table prefix, make sure it is + set correctly in `config.php` and remove the option from your NixOS config. + '') + ++ (lib.optional (lib.versionOlder overridePackage.version "26") (upgradeWarning 25 "23.05")) + ++ (lib.optional (lib.versionOlder overridePackage.version "27") (upgradeWarning 26 "23.11")) + ++ (lib.optional (lib.versionOlder overridePackage.version "28") (upgradeWarning 27 "24.05")) + ++ (lib.optional (lib.versionOlder overridePackage.version "29") (upgradeWarning 28 "24.11")) + ++ (lib.optional (lib.versionOlder overridePackage.version "30") (upgradeWarning 29 "24.11")) + ++ (lib.optional (lib.versionOlder overridePackage.version "31") (upgradeWarning 30 "25.05")); + + services.nextcloud.package = lib.mkDefault ( + if pkgs ? nextcloud then + throw '' + The `pkgs.nextcloud`-attribute has been removed. If it's supposed to be the default + nextcloud defined in an overlay, please set `services.nextcloud.package` to + `pkgs.nextcloud`. + '' + else if lib.versionOlder stateVersion "24.05" then + pkgs.nextcloud27 + else if lib.versionOlder stateVersion "24.11" then + pkgs.nextcloud29 + else if lib.versionOlder stateVersion "25.05" then + pkgs.nextcloud30 + else + pkgs.nextcloud31 + ); + + services.nextcloud.phpOptions = lib.mkMerge [ + (lib.mapAttrs (lib.const lib.mkOptionDefault) defaultPHPSettings) { - wantedBy = [ "multi-user.target" ]; - wants = [ "nextcloud-update-db.service" ]; - before = [ "phpfpm-nextcloud.service" ]; - after = lib.optional mysqlLocal "mysql.service" ++ lib.optional pgsqlLocal "postgresql.target"; - requires = lib.optional mysqlLocal "mysql.service" ++ lib.optional pgsqlLocal "postgresql.target"; - path = [ occ ]; - restartTriggers = [ overrideConfig ]; - script = '' - ${lib.optionalString (c.dbpassFile != null) '' - if [ -z "$(<"$CREDENTIALS_DIRECTORY/dbpass")" ]; then - echo "dbpassFile ${c.dbpassFile} is empty!" - exit 1 - fi - ''} - if [ -z "$(<"$CREDENTIALS_DIRECTORY/adminpass")" ]; then - echo "adminpassFile ${c.adminpassFile} is empty!" - exit 1 - fi - - # Check if systemd-tmpfiles setup worked correctly - if [[ ! -O "${datadir}/config" ]]; then - echo "${datadir}/config is not owned by user 'nextcloud'!" - echo "Please check the logs via 'journalctl -u systemd-tmpfiles-setup'" - echo "and make sure there are no unsafe path transitions." - echo "(https://nixos.org/manual/nixos/stable/#module-services-nextcloud-pitfalls-during-upgrade)" - exit 1 - fi - - ${lib.concatMapStrings - (name: '' - if [ -d "${cfg.home}"/${name} ]; then - echo "Cleaning up ${name}; these are now bundled in the webroot store-path!" - rm -r "${cfg.home}"/${name} - fi - '') - [ - "nix-apps" - "apps" - ] - } - - # Do not install if already installed - if [[ ! -s ${datadir}/config/config.php ]]; then - ${occInstallCmd} - fi - - ${lib.getExe occ} upgrade - - ${lib.getExe occ} config:system:delete trusted_domains - - ${lib.optionalString (cfg.extraAppsEnable && cfg.extraApps != { }) '' - # Try to enable apps - ${lib.getExe occ} app:enable ${lib.concatStringsSep " " (lib.attrNames cfg.extraApps)} - ''} - - ${occSetTrustedDomainsCmd} - ''; - serviceConfig.Type = "oneshot"; - serviceConfig.User = "nextcloud"; - serviceConfig.LoadCredential = [ - "adminpass:${cfg.config.adminpassFile}" - ] - ++ runtimeSystemdCredentials; - # On Nextcloud ≥ 26, it is not necessary to patch the database files to prevent - # an automatic creation of the database user. - environment.NC_setup_create_db_user = "false"; - }; - nextcloud-cron = { - after = [ "nextcloud-setup.service" ]; - # NOTE: In contrast to the occ wrapper script running phpCli directly will not - # set NEXTCLOUD_CONFIG_DIR by itself currently. - environment.NEXTCLOUD_CONFIG_DIR = "${datadir}/config"; - script = '' - # NOTE: This early returns the script when nextcloud is in maintenance mode - # or needs `occ upgrade`. Using ExecCondition= is not possible here - # because it doesn't work with systemd credentials. - if [[ $(${lib.getExe occ} status --output=json | ${lib.getExe pkgs.jq} '. | if .maintenance or .needsDbUpgrade then "skip" else "" end' --raw-output) == "skip" ]]; then - echo "Nextcloud is in maintenance mode or needs DB upgrade, exiting." - exit 0 - fi - - ${phpCli} -f ${webroot}/cron.php - ''; - serviceConfig = { - Type = "exec"; - User = "nextcloud"; - KillMode = "process"; - LoadCredential = runtimeSystemdCredentials; - }; - }; - nextcloud-update-plugins = mkIf cfg.autoUpdateApps.enable { - after = [ "nextcloud-setup.service" ]; - serviceConfig = { - Type = "oneshot"; - ExecStart = "${lib.getExe occ} app:update --all"; - User = "nextcloud"; - LoadCredential = runtimeSystemdCredentials; - }; - startAt = cfg.autoUpdateApps.startAt; - }; - nextcloud-update-db = { - after = [ "nextcloud-setup.service" ]; - script = '' - # NOTE: This early returns the script when nextcloud is in maintenance mode - # or needs `occ upgrade`. Using ExecCondition= is not possible here - # because it doesn't work with systemd credentials. - if [[ $(${lib.getExe occ} status --output=json | ${lib.getExe pkgs.jq} '. | if .maintenance or .needsDbUpgrade then "skip" else "" end' --raw-output) == "skip" ]]; then - echo "Nextcloud is in maintenance mode or needs DB upgrade, exiting." - exit 0 - fi - - ${lib.getExe occ} db:add-missing-columns - ${lib.getExe occ} db:add-missing-indices - ${lib.getExe occ} db:add-missing-primary-keys - ''; - serviceConfig = { - Type = "exec"; - User = "nextcloud"; - LoadCredential = runtimeSystemdCredentials; - }; - }; - - phpfpm-nextcloud = { - # When upgrading the Nextcloud package, Nextcloud can report errors such as - # "The files of the app [all apps in /var/lib/nextcloud/apps] were not replaced correctly" - # Restarting phpfpm on Nextcloud package update fixes these issues (but this is a workaround). - restartTriggers = [ - webroot - overrideConfig - ]; - } - // lib.optionalAttrs requiresRuntimeSystemdCredentials { - serviceConfig.LoadCredential = runtimeSystemdCredentials; - - # FIXME: We use a hack to make the credential files readable by the nextcloud - # user by copying them somewhere else and overriding CREDENTIALS_DIRECTORY - # for php. This is currently necessary as the unit runs as root. - serviceConfig.RuntimeDirectory = lib.mkForce "phpfpm phpfpm-nextcloud"; - preStart = '' - umask 0077 - - # NOTE: Runtime directories for this service are currently preserved - # between restarts. - rm -rf /run/phpfpm-nextcloud/credentials/ - mkdir -p /run/phpfpm-nextcloud/credentials/ - cp "$CREDENTIALS_DIRECTORY"/* /run/phpfpm-nextcloud/credentials/ - chown -R nextcloud:nextcloud /run/phpfpm-nextcloud/credentials/ - ''; - }; - }; - - services.phpfpm = { - pools.nextcloud = { - user = "nextcloud"; - group = "nextcloud"; - phpPackage = phpPackage; - phpEnv = { - CREDENTIALS_DIRECTORY = "/run/phpfpm-nextcloud/credentials/"; - NEXTCLOUD_CONFIG_DIR = "${datadir}/config"; - PATH = "/run/wrappers/bin:/nix/var/nix/profiles/default/bin:/run/current-system/sw/bin:/usr/bin:/bin"; - }; - settings = - lib.mapAttrs (name: lib.mkDefault) { - "listen.owner" = config.services.nginx.user; - "listen.group" = config.services.nginx.group; - } - // cfg.poolSettings; - extraConfig = cfg.poolConfig; - }; - }; - - users.users.nextcloud = { - home = "${cfg.home}"; - group = "nextcloud"; - isSystemUser = true; - }; - users.groups.nextcloud.members = [ - "nextcloud" - config.services.nginx.user - ]; - - environment.systemPackages = [ occ ]; - - services.mysql = lib.mkIf mysqlLocal { - enable = true; - package = lib.mkDefault pkgs.mariadb; - ensureDatabases = [ cfg.config.dbname ]; - ensureUsers = [ - { - name = cfg.config.dbuser; - ensurePermissions = { - "${cfg.config.dbname}.*" = "ALL PRIVILEGES"; - }; + upload_max_filesize = cfg.maxUploadSize; + post_max_size = cfg.maxUploadSize; + memory_limit = cfg.maxUploadSize; } - ]; - }; - - services.postgresql = mkIf pgsqlLocal { - enable = true; - ensureDatabases = [ cfg.config.dbname ]; - ensureUsers = [ - { - name = cfg.config.dbuser; - ensureDBOwnership = true; - } - ]; - }; - - services.redis.servers.nextcloud = lib.mkIf cfg.configureRedis { - enable = true; - user = "nextcloud"; - }; - - services.nextcloud = { - caching.redis = lib.mkIf cfg.configureRedis true; - settings = mkMerge [ - { - datadirectory = lib.mkDefault "${datadir}/data"; - trusted_domains = [ cfg.hostName ]; - "upgrade.disable-web" = true; - # NixOS already provides its own integrity check and the nix store is read-only, therefore Nextcloud does not need to do its own integrity checks. - "integrity.check.disabled" = true; - } - (lib.mkIf cfg.configureRedis { - "memcache.distributed" = ''\OC\Memcache\Redis''; - "memcache.locking" = ''\OC\Memcache\Redis''; - redis = { - host = config.services.redis.servers.nextcloud.unixSocket; - port = 0; - }; + (lib.mkIf cfg.caching.apcu { + "apc.enable_cli" = "1"; }) ]; - }; + } - services.nginx.enable = lib.mkDefault true; + { + assertions = [ + { + assertion = cfg.database.createLocally -> cfg.config.dbpassFile == null; + message = '' + Using `services.nextcloud.database.createLocally` with database + password authentication is no longer supported. - services.nginx.virtualHosts.${cfg.hostName} = { - root = webroot; - locations = { - "= /robots.txt" = { - priority = 100; - extraConfig = '' - allow all; - access_log off; + If you use an external database (or want to use password auth for any + other reason), set `services.nextcloud.database.createLocally` to + `false`. The database won't be managed for you (use `services.mysql` + if you want to set it up). + + If you want this module to manage your nextcloud database for you, + unset `services.nextcloud.config.dbpassFile` and + `services.nextcloud.config.dbhost` to use socket authentication + instead of password. ''; + } + { + assertion = cfg.config.dbtype != null; + message = '' + `services.nextcloud.config.dbtype` must be set explicitly (pgsql, mysql, or sqlite) + + Before 25.05, it used to default to sqlite but that is not recommended by upstream. + Either set it to sqlite as it used to be, or convert to another type as described + in the official db conversion page: + https://docs.nextcloud.com/server/latest/admin_manual/configuration_database/db_conversion.html + ''; + } + ]; + } + + { + systemd.timers.nextcloud-cron = { + wantedBy = [ "timers.target" ]; + after = [ "nextcloud-setup.service" ]; + timerConfig = { + OnBootSec = "5m"; + OnUnitActiveSec = "5m"; + Unit = "nextcloud-cron.service"; }; - "= /" = { - priority = 100; - extraConfig = '' - if ( $http_user_agent ~ ^DavClnt ) { - return 302 /remote.php/webdav/$is_args$args; - } + }; + + systemd.tmpfiles.rules = + map (dir: "d ${dir} 0750 nextcloud nextcloud - -") [ + "${cfg.home}" + "${datadir}/config" + "${datadir}/data" + "${cfg.home}/store-apps" + ] + ++ [ + "L+ ${datadir}/config/override.config.php - - - - ${overrideConfig}" + ]; + + services.nextcloud.finalPackage = webroot; + + systemd.services = { + nextcloud-setup = + let + c = cfg.config; + occInstallCmd = + let + mkExport = + { arg, value }: + '' + ${arg}=${value}; + export ${arg}; + ''; + dbpass = { + arg = "DBPASS"; + value = if c.dbpassFile != null then ''"$(<"$CREDENTIALS_DIRECTORY/dbpass")"'' else ''""''; + }; + adminpass = { + arg = "ADMINPASS"; + value = ''"$(<"$CREDENTIALS_DIRECTORY/adminpass")"''; + }; + installFlags = lib.concatStringsSep " \\\n " ( + lib.mapAttrsToList (k: v: "${k} ${toString v}") { + "--database" = ''"${c.dbtype}"''; + # The following attributes are optional depending on the type of + # database. Those that evaluate to null on the left hand side + # will be omitted. + ${if c.dbname != null then "--database-name" else null} = ''"${c.dbname}"''; + ${if c.dbhost != null then "--database-host" else null} = ''"${c.dbhost}"''; + ${if c.dbuser != null then "--database-user" else null} = ''"${c.dbuser}"''; + "--database-pass" = "\"\$${dbpass.arg}\""; + "--admin-user" = ''"${c.adminuser}"''; + "--admin-pass" = "\"\$${adminpass.arg}\""; + "--data-dir" = ''"${datadir}/data"''; + } + ); + in + '' + ${mkExport dbpass} + ${mkExport adminpass} + ${lib.getExe occ} maintenance:install \ + ${installFlags} + ''; + occSetTrustedDomainsCmd = lib.concatStringsSep "\n" ( + lib.imap0 (i: v: '' + ${lib.getExe occ} config:system:set trusted_domains \ + ${toString i} --value="${toString v}" + '') (lib.unique ([ cfg.hostName ] ++ cfg.settings.trusted_domains)) + ); + + in + { + wantedBy = [ "multi-user.target" ]; + wants = [ "nextcloud-update-db.service" ]; + before = [ "phpfpm-nextcloud.service" ]; + after = lib.optional mysqlLocal "mysql.service" ++ lib.optional pgsqlLocal "postgresql.target"; + requires = lib.optional mysqlLocal "mysql.service" ++ lib.optional pgsqlLocal "postgresql.target"; + path = [ occ ]; + restartTriggers = [ overrideConfig ]; + script = '' + ${lib.optionalString (c.dbpassFile != null) '' + if [ -z "$(<"$CREDENTIALS_DIRECTORY/dbpass")" ]; then + echo "dbpassFile ${c.dbpassFile} is empty!" + exit 1 + fi + ''} + if [ -z "$(<"$CREDENTIALS_DIRECTORY/adminpass")" ]; then + echo "adminpassFile ${c.adminpassFile} is empty!" + exit 1 + fi + + # Check if systemd-tmpfiles setup worked correctly + if [[ ! -O "${datadir}/config" ]]; then + echo "${datadir}/config is not owned by user 'nextcloud'!" + echo "Please check the logs via 'journalctl -u systemd-tmpfiles-setup'" + echo "and make sure there are no unsafe path transitions." + echo "(https://nixos.org/manual/nixos/stable/#module-services-nextcloud-pitfalls-during-upgrade)" + exit 1 + fi + + ${lib.concatMapStrings + (name: '' + if [ -d "${cfg.home}"/${name} ]; then + echo "Cleaning up ${name}; these are now bundled in the webroot store-path!" + rm -r "${cfg.home}"/${name} + fi + '') + [ + "nix-apps" + "apps" + ] + } + + # Do not install if already installed + if [[ ! -s ${datadir}/config/config.php ]]; then + ${occInstallCmd} + fi + + ${lib.getExe occ} upgrade + + ${lib.getExe occ} config:system:delete trusted_domains + + ${lib.optionalString (cfg.extraAppsEnable && cfg.extraApps != { }) '' + # Try to enable apps + ${lib.getExe occ} app:enable ${lib.concatStringsSep " " (lib.attrNames cfg.extraApps)} + ''} + + ${occSetTrustedDomainsCmd} + ''; + serviceConfig.Type = "oneshot"; + serviceConfig.User = "nextcloud"; + serviceConfig.LoadCredential = [ + "adminpass:${cfg.config.adminpassFile}" + ] + ++ runtimeSystemdCredentials; + # On Nextcloud ≥ 26, it is not necessary to patch the database files to prevent + # an automatic creation of the database user. + environment.NC_setup_create_db_user = "false"; + }; + nextcloud-cron = { + after = [ "nextcloud-setup.service" ]; + # NOTE: In contrast to the occ wrapper script running phpCli directly will not + # set NEXTCLOUD_CONFIG_DIR by itself currently. + environment.NEXTCLOUD_CONFIG_DIR = "${datadir}/config"; + script = '' + # NOTE: This early returns the script when nextcloud is in maintenance mode + # or needs `occ upgrade`. Using ExecCondition= is not possible here + # because it doesn't work with systemd credentials. + if [[ $(${lib.getExe occ} status --output=json | ${lib.getExe pkgs.jq} '. | if .maintenance or .needsDbUpgrade then "skip" else "" end' --raw-output) == "skip" ]]; then + echo "Nextcloud is in maintenance mode or needs DB upgrade, exiting." + exit 0 + fi + + ${phpCli} -f ${webroot}/cron.php ''; + serviceConfig = { + Type = "exec"; + User = "nextcloud"; + KillMode = "process"; + LoadCredential = runtimeSystemdCredentials; + }; }; - "^~ /.well-known" = { - priority = 210; - extraConfig = '' - absolute_redirect off; - location = /.well-known/carddav { - return 301 /remote.php/dav/; - } - location = /.well-known/caldav { - return 301 /remote.php/dav/; - } - location ~ ^/\.well-known/(?!acme-challenge|pki-validation) { - return 301 /index.php$request_uri; - } - try_files $uri $uri/ =404; - ''; + nextcloud-update-plugins = lib.mkIf cfg.autoUpdateApps.enable { + after = [ "nextcloud-setup.service" ]; + serviceConfig = { + Type = "oneshot"; + ExecStart = "${lib.getExe occ} app:update --all"; + User = "nextcloud"; + LoadCredential = runtimeSystemdCredentials; + }; + startAt = cfg.autoUpdateApps.startAt; }; - "~ ^/(?:build|tests|config|lib|3rdparty|templates|data)(?:$|/)" = { - priority = 450; - extraConfig = '' - return 404; + nextcloud-update-db = { + after = [ "nextcloud-setup.service" ]; + script = '' + # NOTE: This early returns the script when nextcloud is in maintenance mode + # or needs `occ upgrade`. Using ExecCondition= is not possible here + # because it doesn't work with systemd credentials. + if [[ $(${lib.getExe occ} status --output=json | ${lib.getExe pkgs.jq} '. | if .maintenance or .needsDbUpgrade then "skip" else "" end' --raw-output) == "skip" ]]; then + echo "Nextcloud is in maintenance mode or needs DB upgrade, exiting." + exit 0 + fi + + ${lib.getExe occ} db:add-missing-columns + ${lib.getExe occ} db:add-missing-indices + ${lib.getExe occ} db:add-missing-primary-keys ''; + serviceConfig = { + Type = "exec"; + User = "nextcloud"; + LoadCredential = runtimeSystemdCredentials; + }; }; - "~ ^/(?:\\.|autotest|occ|issue|indie|db_|console)" = { - priority = 450; - extraConfig = '' - return 404; - ''; - }; - "~ \\.php(?:$|/)" = { - priority = 500; - extraConfig = '' - # legacy support (i.e. static files and directories in cfg.package) - rewrite ^/(?!index|remote|public|cron|core\/ajax\/update|status|ocs\/v[12]|updater\/.+|ocs-provider\/.+|.+\/richdocumentscode(_arm64)?\/proxy) /index.php$request_uri; - include ${config.services.nginx.package}/conf/fastcgi.conf; - fastcgi_split_path_info ^(.+?\.php)(\\/.*)$; - set $path_info $fastcgi_path_info; - try_files $fastcgi_script_name =404; - fastcgi_param PATH_INFO $path_info; - fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; - fastcgi_param HTTPS ${if cfg.https then "on" else "off"}; - fastcgi_param modHeadersAvailable true; - fastcgi_param front_controller_active true; - fastcgi_pass unix:${fpm.socket}; - fastcgi_intercept_errors on; - fastcgi_request_buffering ${if cfg.nginx.enableFastcgiRequestBuffering then "on" else "off"}; - fastcgi_read_timeout ${builtins.toString cfg.fastcgiTimeout}s; - ''; - }; - "~ \\.(?:css|js|mjs|svg|gif|ico|jpg|jpeg|png|webp|wasm|tflite|map|html|ttf|bcmap|mp4|webm|ogg|flac)$".extraConfig = - '' - try_files $uri /index.php$request_uri; - expires 6M; - access_log off; - location ~ \.mjs$ { - default_type text/javascript; - } - location ~ \.wasm$ { - default_type application/wasm; - } - ''; - "~ ^\\/(?:updater|ocs-provider)(?:$|\\/)".extraConfig = '' - try_files $uri/ =404; - index index.php; - ''; - "/remote" = { - priority = 1500; - extraConfig = '' - return 301 /remote.php$request_uri; - ''; - }; - "/" = { - priority = 1600; - extraConfig = '' - try_files $uri $uri/ /index.php$request_uri; + + phpfpm-nextcloud = { + # When upgrading the Nextcloud package, Nextcloud can report errors such as + # "The files of the app [all apps in /var/lib/nextcloud/apps] were not replaced correctly" + # Restarting phpfpm on Nextcloud package update fixes these issues (but this is a workaround). + restartTriggers = [ + webroot + overrideConfig + ]; + } + // lib.optionalAttrs requiresRuntimeSystemdCredentials { + serviceConfig.LoadCredential = runtimeSystemdCredentials; + + # FIXME: We use a hack to make the credential files readable by the nextcloud + # user by copying them somewhere else and overriding CREDENTIALS_DIRECTORY + # for php. This is currently necessary as the unit runs as root. + serviceConfig.RuntimeDirectory = lib.mkForce "phpfpm phpfpm-nextcloud"; + preStart = '' + umask 0077 + + # NOTE: Runtime directories for this service are currently preserved + # between restarts. + rm -rf /run/phpfpm-nextcloud/credentials/ + mkdir -p /run/phpfpm-nextcloud/credentials/ + cp "$CREDENTIALS_DIRECTORY"/* /run/phpfpm-nextcloud/credentials/ + chown -R nextcloud:nextcloud /run/phpfpm-nextcloud/credentials/ ''; }; }; - extraConfig = '' - index index.php index.html /index.php$request_uri; - ${lib.optionalString (cfg.nginx.recommendedHttpHeaders) '' - add_header X-Content-Type-Options nosniff; - add_header X-XSS-Protection "1; mode=block"; - add_header X-Robots-Tag "noindex, nofollow"; - add_header X-Permitted-Cross-Domain-Policies none; - add_header X-Frame-Options sameorigin; - add_header Referrer-Policy no-referrer; - ''} - ${lib.optionalString (cfg.https) '' - add_header Strict-Transport-Security "max-age=${toString cfg.nginx.hstsMaxAge}; includeSubDomains" always; - ''} - client_max_body_size ${cfg.maxUploadSize}; - fastcgi_buffers 64 4K; - fastcgi_hide_header X-Powered-By; - gzip on; - gzip_vary on; - gzip_comp_level 4; - gzip_min_length 256; - gzip_proxied expired no-cache no-store private no_last_modified no_etag auth; - gzip_types application/atom+xml text/javascript application/javascript application/json application/ld+json application/manifest+json application/rss+xml application/vnd.geo+json application/vnd.ms-fontobject application/wasm 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; - ${lib.optionalString cfg.webfinger '' - rewrite ^/.well-known/host-meta /public.php?service=host-meta last; - rewrite ^/.well-known/host-meta.json /public.php?service=host-meta-json last; - ''} - ''; - }; - } - ]); + services.phpfpm = { + pools.nextcloud = { + user = "nextcloud"; + group = "nextcloud"; + phpPackage = phpPackage; + phpEnv = { + CREDENTIALS_DIRECTORY = "/run/phpfpm-nextcloud/credentials/"; + NEXTCLOUD_CONFIG_DIR = "${datadir}/config"; + PATH = "/run/wrappers/bin:/nix/var/nix/profiles/default/bin:/run/current-system/sw/bin:/usr/bin:/bin"; + }; + settings = + lib.mapAttrs (name: lib.mkDefault) { + "listen.owner" = config.services.nginx.user; + "listen.group" = config.services.nginx.group; + } + // cfg.poolSettings; + extraConfig = cfg.poolConfig; + }; + }; + + users.users.nextcloud = { + home = "${cfg.home}"; + group = "nextcloud"; + isSystemUser = true; + }; + users.groups.nextcloud.members = [ + "nextcloud" + config.services.nginx.user + ]; + + environment.systemPackages = [ occ ]; + + services.mysql = lib.mkIf mysqlLocal { + enable = true; + package = lib.mkDefault pkgs.mariadb; + ensureDatabases = [ cfg.config.dbname ]; + ensureUsers = [ + { + name = cfg.config.dbuser; + ensurePermissions = { + "${cfg.config.dbname}.*" = "ALL PRIVILEGES"; + }; + } + ]; + }; + + services.postgresql = lib.mkIf pgsqlLocal { + enable = true; + ensureDatabases = [ cfg.config.dbname ]; + ensureUsers = [ + { + name = cfg.config.dbuser; + ensureDBOwnership = true; + } + ]; + }; + + services.redis.servers.nextcloud = lib.mkIf cfg.configureRedis { + enable = true; + user = "nextcloud"; + }; + + services.nextcloud = { + caching.redis = lib.mkIf cfg.configureRedis true; + settings = lib.mkMerge [ + { + datadirectory = lib.mkDefault "${datadir}/data"; + trusted_domains = [ cfg.hostName ]; + "upgrade.disable-web" = true; + # NixOS already provides its own integrity check and the nix store is read-only, therefore Nextcloud does not need to do its own integrity checks. + "integrity.check.disabled" = true; + } + (lib.mkIf cfg.configureRedis { + "memcache.distributed" = ''\OC\Memcache\Redis''; + "memcache.locking" = ''\OC\Memcache\Redis''; + redis = { + host = config.services.redis.servers.nextcloud.unixSocket; + port = 0; + }; + }) + ]; + }; + + services.nginx.enable = lib.mkDefault true; + + services.nginx.virtualHosts.${cfg.hostName} = { + root = webroot; + locations = { + "= /robots.txt" = { + priority = 100; + extraConfig = '' + allow all; + access_log off; + ''; + }; + "= /" = { + priority = 100; + extraConfig = '' + if ( $http_user_agent ~ ^DavClnt ) { + return 302 /remote.php/webdav/$is_args$args; + } + ''; + }; + "^~ /.well-known" = { + priority = 210; + extraConfig = '' + absolute_redirect off; + location = /.well-known/carddav { + return 301 /remote.php/dav/; + } + location = /.well-known/caldav { + return 301 /remote.php/dav/; + } + location ~ ^/\.well-known/(?!acme-challenge|pki-validation) { + return 301 /index.php$request_uri; + } + try_files $uri $uri/ =404; + ''; + }; + "~ ^/(?:build|tests|config|lib|3rdparty|templates|data)(?:$|/)" = { + priority = 450; + extraConfig = '' + return 404; + ''; + }; + "~ ^/(?:\\.|autotest|occ|issue|indie|db_|console)" = { + priority = 450; + extraConfig = '' + return 404; + ''; + }; + "~ \\.php(?:$|/)" = { + priority = 500; + extraConfig = '' + # legacy support (i.e. static files and directories in cfg.package) + rewrite ^/(?!index|remote|public|cron|core\/ajax\/update|status|ocs\/v[12]|updater\/.+|ocs-provider\/.+|.+\/richdocumentscode(_arm64)?\/proxy) /index.php$request_uri; + include ${config.services.nginx.package}/conf/fastcgi.conf; + fastcgi_split_path_info ^(.+?\.php)(\\/.*)$; + set $path_info $fastcgi_path_info; + try_files $fastcgi_script_name =404; + fastcgi_param PATH_INFO $path_info; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param HTTPS ${if cfg.https then "on" else "off"}; + fastcgi_param modHeadersAvailable true; + fastcgi_param front_controller_active true; + fastcgi_pass unix:${fpm.socket}; + fastcgi_intercept_errors on; + fastcgi_request_buffering ${if cfg.nginx.enableFastcgiRequestBuffering then "on" else "off"}; + fastcgi_read_timeout ${builtins.toString cfg.fastcgiTimeout}s; + ''; + }; + "~ \\.(?:css|js|mjs|svg|gif|ico|jpg|jpeg|png|webp|wasm|tflite|map|html|ttf|bcmap|mp4|webm|ogg|flac)$".extraConfig = + '' + try_files $uri /index.php$request_uri; + expires 6M; + access_log off; + location ~ \.mjs$ { + default_type text/javascript; + } + location ~ \.wasm$ { + default_type application/wasm; + } + ''; + "~ ^\\/(?:updater|ocs-provider)(?:$|\\/)".extraConfig = '' + try_files $uri/ =404; + index index.php; + ''; + "/remote" = { + priority = 1500; + extraConfig = '' + return 301 /remote.php$request_uri; + ''; + }; + "/" = { + priority = 1600; + extraConfig = '' + try_files $uri $uri/ /index.php$request_uri; + ''; + }; + }; + extraConfig = '' + index index.php index.html /index.php$request_uri; + ${lib.optionalString (cfg.nginx.recommendedHttpHeaders) '' + add_header X-Content-Type-Options nosniff; + add_header X-XSS-Protection "1; mode=block"; + add_header X-Robots-Tag "noindex, nofollow"; + add_header X-Permitted-Cross-Domain-Policies none; + add_header X-Frame-Options sameorigin; + add_header Referrer-Policy no-referrer; + ''} + ${lib.optionalString (cfg.https) '' + add_header Strict-Transport-Security "max-age=${toString cfg.nginx.hstsMaxAge}; includeSubDomains" always; + ''} + client_max_body_size ${cfg.maxUploadSize}; + fastcgi_buffers 64 4K; + fastcgi_hide_header X-Powered-By; + gzip on; + gzip_vary on; + gzip_comp_level 4; + gzip_min_length 256; + gzip_proxied expired no-cache no-store private no_last_modified no_etag auth; + gzip_types application/atom+xml text/javascript application/javascript application/json application/ld+json application/manifest+json application/rss+xml application/vnd.geo+json application/vnd.ms-fontobject application/wasm 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; + + ${lib.optionalString cfg.webfinger '' + rewrite ^/.well-known/host-meta /public.php?service=host-meta last; + rewrite ^/.well-known/host-meta.json /public.php?service=host-meta-json last; + ''} + ''; + }; + } + ] + ); meta.doc = ./nextcloud.md; meta.maintainers = lib.teams.nextcloud.members;