{ lib, config, pkgs, namespace, ... }: let name = "nextcloud"; cfg = config.${namespace}.services.${name}; net = lib.${namespace}.network; nextcloudConfig = lib.${namespace}.mkModule { inherit config name; serviceName = "nextcloud"; description = "Nextcloud - Secure file sync and sharing platform"; options = { }; moduleConfig = { # Setup the native NixOS Nextcloud service services = { nextcloud = { enable = true; package = pkgs.nextcloud33; hostName = "cloud.mjallen.dev"; home = "${cfg.dataDir}/nextcloud"; datadir = "${cfg.dataDir}/nextcloud"; configureRedis = true; enableImagemagick = true; appstoreEnable = true; # extraApps = with pkgs.${namespace}; { # richdocumentscode = nextcloud-code-server; # # richdocuments = nextcloud-richdocuments; # }; # Use PostgreSQL for database config = { dbtype = "pgsql"; dbname = "nextcloud"; dbuser = "nextcloud"; dbhost = "/run/postgresql"; # Socket directory # dbpassFile = config.sops.secrets."jallen-nas/nextcloud/dbpassword".path; adminuser = "mjallen"; adminpassFile = config.sops.secrets."matt_password".path; }; # PHP settings phpOptions = lib.mkOverride 90 { memory_limit = "512M"; upload_max_filesize = "10G"; post_max_size = "10G"; output_buffering = "0"; "opcache.interned_strings_buffer" = "16"; "opcache.max_accelerated_files" = "10000"; "opcache.memory_consumption" = "128"; "opcache.save_comments" = "1"; "opcache.revalidate_freq" = "1"; }; # Configure caching for better performance caching = { apcu = true; redis = true; memcached = false; }; # Auto-update apps autoUpdateApps = { enable = false; startAt = "05:00:00"; }; # Configure HTTPS if enabled https = false; settings = { installed = true; auth.bruteforce.protection.enabled = false; user_oidc = { auto_provision = false; }; overwrite.cli.url = "https://cloud.mjallen.dev"; overwriteprotocol = "https"; overwritehost = "cloud.mjallen.dev"; log_type = "file"; default_phone_region = "US"; trusted_proxies = [ net.hosts.nas.lan "127.0.0.1" "::1" ]; trusted_domains = [ "cloud.mjallen.dev" "${net.hosts.nas.lan}:${toString cfg.port}" ]; enabledPreviewProviders = [ "OC\\Preview\\PNG" "OC\\Preview\\JPEG" "OC\\Preview\\GIF" "OC\\Preview\\BMP" "OC\\Preview\\XBitmap" "OC\\Preview\\Krita" "OC\\Preview\\WebP" "OC\\Preview\\MarkDown" "OC\\Preview\\TXT" "OC\\Preview\\OpenDocument" ]; }; }; nginx = { enable = true; group = "jallen-nas"; virtualHosts.${config.services.nextcloud.hostName} = { listen = [ { inherit (cfg) port; addr = "0.0.0.0"; ssl = false; } ]; }; }; }; users = { users = { nextcloud = { isSystemUser = lib.mkForce true; isNormalUser = lib.mkForce false; }; }; groups = { nextcloud = { }; }; }; # Ensure nextcloud services start after PostgreSQL is ready. # The upstream NixOS module only adds this ordering when services.postgresql.enable # is true in the same config, but here PostgreSQL is managed separately. systemd = { services = { # Override the empty systemd service created by mkModule. # The native NixOS nextcloud module doesn't create a persistent "nextcloud.service" # (it uses PHP-FPM pools and cron instead), so we clear this to avoid the error: # "Service has no ExecStart=, ExecStop=, or SuccessAction=. Refusing." nextcloud = lib.mkForce { }; nextcloud-setup = { # Also require the NAS bcachefs mount so that ExecStartPre can create # the store-apps and config directories on the actual NAS filesystem. # Without this, the dirs are created on the root tmpfs overlay before # the mount comes up, and the real NAS store-apps path never exists. after = [ "postgresql.service" "media-nas-main.mount" ]; requires = [ "postgresql.service" "media-nas-main.mount" ]; serviceConfig = let # Extract the override.config.php store-path from the already-evaluated # tmpfiles rules list at Nix eval time, so we never have to parse files at # runtime. The upstream module emits exactly one rule of the form: # "L+ - - - - " overrideLine = lib.findFirst ( r: lib.hasInfix "override.config.php" r ) null config.systemd.tmpfiles.rules; overrideStorePath = if overrideLine != null then lib.last (lib.splitString " " overrideLine) else null; # Bootstrap config.php written when the file is absent/empty. # Satisfies Nextcloud's Config.php writeData() guard (needs 'version') # and the setup script's `-s` check (needs non-empty file). # passwordsalt/secret/instanceid are intentionally left empty here — # they must be populated manually or via SOPS before first use. bootstrapConfig = pkgs.writeText "nextcloud-bootstrap-config.php" ( " true,\n" + " 'version' => '${config.services.nextcloud.package.version}',\n" + " 'datadirectory' => '${cfg.dataDir}/nextcloud/data',\n" + " 'dbtype' => 'pgsql',\n" + " 'dbname' => 'nextcloud',\n" + " 'dbhost' => '/run/postgresql',\n" + " 'dbuser' => 'nextcloud',\n" + " 'dbpassword' => " + "''" + ",\n" + " 'instanceid' => " + "''" + ",\n" + " 'passwordsalt' => " + "''" + ",\n" + " 'secret' => " + "''" + ",\n" + "];\n" ); in lib.mkIf (overrideStorePath != null) { # systemd-tmpfiles refuses to create paths under /media/nas/main because # of an "unsafe path transition" (owned by nix-apps, not root/nextcloud). # Work around by creating the required dirs/symlinks as root ('+' prefix) # before the setup script's ownership check runs. ExecStartPre = [ ( "+" + pkgs.writeShellScript "nextcloud-fix-override-config" '' ncdir="${cfg.dataDir}/nextcloud" # Ensure required directories exist with correct ownership for dir in "$ncdir" "$ncdir/config" "$ncdir/data" "$ncdir/store-apps"; do if [ ! -d "$dir" ]; then ${pkgs.coreutils}/bin/mkdir -p "$dir" fi ${pkgs.coreutils}/bin/chown nextcloud:nextcloud "$dir" ${pkgs.coreutils}/bin/chmod 0750 "$dir" done # override.config.php symlink (updated each generation) dest="$ncdir/config/override.config.php" echo "Creating symlink: $dest -> ${overrideStorePath}" ${pkgs.coreutils}/bin/ln -sf "${overrideStorePath}" "$dest" # If config.php is absent or empty, copy in a bootstrap stub. # Nextcloud's Config.php writeData() guard requires 'version' in the # merged cache, and the setup script's -s check requires a non-empty # file. The real runtime settings come from override.config.php via # array_replace_recursive; this stub just satisfies those two guards. cfgfile="$ncdir/config/config.php" if [ ! -s "$cfgfile" ]; then echo "Writing bootstrap config.php" ${pkgs.coreutils}/bin/cp ${bootstrapConfig} "$cfgfile" ${pkgs.coreutils}/bin/chown nextcloud:nextcloud "$cfgfile" ${pkgs.coreutils}/bin/chmod 0640 "$cfgfile" fi '' ) ]; }; }; nextcloud-update-db = { after = [ "postgresql.service" "media-nas-main.mount" ]; requires = [ "postgresql.service" "media-nas-main.mount" ]; }; nextcloud-cron = { after = [ "media-nas-main.mount" ]; requires = [ "media-nas-main.mount" ]; }; phpfpm-nextcloud = { after = [ "media-nas-main.mount" ]; requires = [ "media-nas-main.mount" ]; }; # One-shot repair for the oc_filecache_extended duplicate key constraint # violation that causes nextcloud-cron to fail with: # "duplicate key value violates unique constraint oc_filecache_extended_pkey" # Runs as the postgres user before nextcloud-setup so that the DB is clean # before Nextcloud starts. Idempotent: only removes rows whose fileid does # not exist in oc_filecache (true orphans). Remove this service once the # underlying Nextcloud bug is fixed and a clean run confirms cron succeeds. nextcloud-repair-filecache = { description = "Repair orphan rows in oc_filecache_extended"; wantedBy = [ "nextcloud-setup.service" ]; before = [ "nextcloud-setup.service" ]; after = [ "postgresql.service" ]; requires = [ "postgresql.service" ]; serviceConfig = { Type = "oneshot"; User = "postgres"; ExecStart = pkgs.writeShellScript "nextcloud-repair-filecache" '' ${pkgs.postgresql}/bin/psql -d nextcloud -c " DELETE FROM oc_filecache_extended WHERE fileid NOT IN (SELECT fileid FROM oc_filecache); " ''; }; }; }; }; }; }; in { imports = [ nextcloudConfig ]; }