{ config, pkgs, lib, ... }: lib.mkIf config.sovran_systemsOS.services.nextcloud { # ── PostgreSQL database ─────────────────────────────────── # Cluster-wide tuning (shared_buffers, autovacuum) lives in # configuration.nix so it is shared with Matrix Synapse. services.postgresql = { enable = true; }; # ── Redis for Nextcloud distributed cache + file locking ─── # Nextcloud does not recommend APCu for memcache.locking in production. # TCP on localhost avoids unix-socket permission juggling with the caddy user. # Scoped to Nextcloud only — Synapse / MariaDB / Bitcoin are unaffected. services.redis.servers.nextcloud = { enable = true; bind = "127.0.0.1"; port = 6379; }; # ── Auto-generate DB password and initialize ────────────── systemd.services.nextcloud-db-init = { description = "Initialize Nextcloud PostgreSQL database with auto-generated password"; after = [ "postgresql.service" ]; requires = [ "postgresql.service" ]; before = [ "nextcloud-init.service" ]; wantedBy = [ "multi-user.target" ]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; }; path = [ config.services.postgresql.package pkgs.pwgen pkgs.coreutils ]; script = '' set -euo pipefail SECRET_FILE="/var/lib/secrets/nextclouddb" if [ ! -f "$SECRET_FILE" ]; then mkdir -p /var/lib/secrets pwgen -s 64 1 > "$SECRET_FILE" chmod 600 "$SECRET_FILE" fi DB_PASS=$(cat "$SECRET_FILE") psql -U postgres </dev/null; then echo "Database ready." break fi sleep 2 done echo "Running Nextcloud installation..." /run/wrappers/bin/su -s /bin/sh caddy -c " php $INSTALL_DIR/occ maintenance:install \ --database 'pgsql' \ --database-name '$DB_NAME' \ --database-user '$DB_USER' \ --database-pass '$DB_PASS' \ --database-host '$DB_HOST' \ --admin-user '$ADMIN_USER' \ --admin-pass '$ADMIN_PASS' \ --data-dir '$DATA_DIR' " /run/wrappers/bin/su -s /bin/sh caddy -c " php $INSTALL_DIR/occ config:system:set trusted_domains 0 --value='$DOMAIN' php $INSTALL_DIR/occ config:system:set overwrite.cli.url --value='https://$DOMAIN' php $INSTALL_DIR/occ config:system:set overwritehost --value='$DOMAIN' php $INSTALL_DIR/occ config:system:set overwriteprotocol --value='https' " /run/wrappers/bin/su -s /bin/sh caddy -c " php $INSTALL_DIR/occ config:system:set trusted_proxies 0 --value='127.0.0.1' php $INSTALL_DIR/occ config:system:set trusted_proxies 1 --value='::1' php $INSTALL_DIR/occ config:system:set forwarded_for_headers 0 --value='HTTP_X_FORWARDED_FOR' php $INSTALL_DIR/occ config:system:set default_phone_region --value='US' php $INSTALL_DIR/occ config:system:set maintenance_window_start --type=integer --value=1 php $INSTALL_DIR/occ config:system:set memcache.local --value='\OC\Memcache\APCu' php $INSTALL_DIR/occ config:system:set memcache.distributed --value='\OC\Memcache\Redis' php $INSTALL_DIR/occ config:system:set memcache.locking --value='\OC\Memcache\Redis' php $INSTALL_DIR/occ config:system:set redis host --value='127.0.0.1' php $INSTALL_DIR/occ config:system:set redis port --type=integer --value=6379 php $INSTALL_DIR/occ config:system:set redis timeout --value='1.5' php $INSTALL_DIR/occ config:system:set server_id --value='$SERVER_ID' php $INSTALL_DIR/occ background:cron " /run/wrappers/bin/su -s /bin/sh caddy -c " php $INSTALL_DIR/occ integrity:check-core php $INSTALL_DIR/occ maintenance:repair php $INSTALL_DIR/occ db:add-missing-indices php $INSTALL_DIR/occ db:add-missing-columns php $INSTALL_DIR/occ db:add-missing-primary-keys php $INSTALL_DIR/occ maintenance:repair --include-expensive # AppAPI deploy daemon warnings are avoided by disabling app_api when present. if php $INSTALL_DIR/occ app:info app_api >/dev/null 2>&1; then php $INSTALL_DIR/occ app:disable app_api fi " /run/wrappers/bin/su -s /bin/sh caddy -c " php $INSTALL_DIR/occ app:install calendar || true php $INSTALL_DIR/occ app:install contacts || true php $INSTALL_DIR/occ app:install tasks || true php $INSTALL_DIR/occ app:install notes || true php $INSTALL_DIR/occ app:install deck || true php $INSTALL_DIR/occ app:enable calendar || true php $INSTALL_DIR/occ app:enable contacts || true php $INSTALL_DIR/occ app:enable tasks || true php $INSTALL_DIR/occ app:enable notes || true php $INSTALL_DIR/occ app:enable deck || true " CREDS_FILE="/var/lib/secrets/nextcloud-admin" cat > "$CREDS_FILE" << CREDS Nextcloud Admin Credentials ═══════════════════════════ URL: https://$DOMAIN/ Username: $ADMIN_USER Password: $ADMIN_PASS CREDS chmod 600 "$CREDS_FILE" echo "" echo "══════════════════════════════════════════════" echo " Nextcloud installation complete!" echo " Credentials saved to: $CREDS_FILE" echo "══════════════════════════════════════════════" ''; }; systemd.services.nextcloud-detect-existing = { description = "Detect pre-existing Nextcloud installation and populate hub credentials"; after = [ "postgresql.service" ]; wants = [ "postgresql.service" ]; wantedBy = [ "multi-user.target" ]; unitConfig = { ConditionPathExists = [ "/var/lib/www/nextcloud/config/config.php" "!/var/lib/secrets/nextcloud-admin" ]; }; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; }; path = with pkgs; [ coreutils gnused ]; script = '' set -euo pipefail CREDS_FILE="/var/lib/secrets/nextcloud-admin" DOMAIN_FILE="/var/lib/domains/nextcloud" DOMAIN="your-domain" if [ -f "$DOMAIN_FILE" ]; then FILE_DOMAIN="$(sed -n '1{s/^[[:space:]]*//;s/[[:space:]]*$//;p;}' "$DOMAIN_FILE")" if [ -n "$FILE_DOMAIN" ]; then DOMAIN="$FILE_DOMAIN" fi fi mkdir -p /var/lib/secrets cat > "$CREDS_FILE" << CREDS Nextcloud (Pre-existing Installation) ═══════════════════════════════════════ URL: https://$DOMAIN/ Note: This Nextcloud was installed before Sovran_SystemsOS. Use your existing admin credentials to log in. Reset: sudo -u caddy php /var/lib/www/nextcloud/occ user:resetpassword CREDS chmod 600 "$CREDS_FILE" ''; }; # ── Migrate existing installs to Redis locking ──────────── # nextcloud-init only runs on fresh installs (ConditionPathExists # !config.php), so pre-existing / pre-Sovran installs would keep # APCu locking forever. This one-shot is idempotent and safe to # re-run on every boot — occ just overwrites the same values. systemd.services.nextcloud-redis-migrate = { description = "Point existing Nextcloud installs at Redis locking"; after = [ "postgresql.service" "redis-nextcloud.service" "phpfpm-nextcloud.service" ]; wants = [ "redis-nextcloud.service" ]; wantedBy = [ "multi-user.target" ]; unitConfig = { ConditionPathExists = [ "/var/lib/www/nextcloud/occ" "/var/lib/www/nextcloud/config/config.php" ]; }; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; }; path = with pkgs; [ coreutils shadow ]; script = '' set -euo pipefail INSTALL_DIR="/var/lib/www/nextcloud" # Wait briefly for Redis (TCP localhost:6379). for i in $(seq 1 15); do if (echo > /dev/tcp/127.0.0.1/6379) >/dev/null 2>&1; then break fi sleep 2 done /run/wrappers/bin/su -s /bin/sh caddy -c " php $INSTALL_DIR/occ config:system:set memcache.local --value='\OC\Memcache\APCu' php $INSTALL_DIR/occ config:system:set memcache.distributed --value='\OC\Memcache\Redis' php $INSTALL_DIR/occ config:system:set memcache.locking --value='\OC\Memcache\Redis' php $INSTALL_DIR/occ config:system:set redis host --value='127.0.0.1' php $INSTALL_DIR/occ config:system:set redis port --type=integer --value=6379 php $INSTALL_DIR/occ config:system:set redis timeout --value='1.5' " ''; }; # ── Recurring DB maintenance (Nextcloud 35 checks) ─────────── # nextcloud-init runs db:add-missing-indices exactly once. Upgrades # (e.g. to NC35) and later app installs (Mail, Guests) add tables # like oc_mail_tags / oc_guests_users that then seq-scan forever. # Weekly: VACUUM ANALYZE (dead tuples) + backfill missing indices. # Scoped to nextclouddb only — matrix-synapse is untouched. systemd.services.nextcloud-db-maintenance = { description = "Nextcloud DB maintenance: VACUUM + missing indices"; after = [ "postgresql.service" "redis-nextcloud.service" "phpfpm-nextcloud.service" ]; wants = [ "postgresql.service" ]; unitConfig = { ConditionPathExists = [ "/var/lib/www/nextcloud/occ" "/var/lib/www/nextcloud/config/config.php" ]; }; serviceConfig = { Type = "oneshot"; }; path = [ config.services.postgresql.package pkgs.coreutils pkgs.shadow ]; script = '' set -euo pipefail INSTALL_DIR="/var/lib/www/nextcloud" echo "Vacuuming nextclouddb..." psql -U postgres -d nextclouddb -c "VACUUM (ANALYZE);" echo "Backfilling Nextcloud indices..." /run/wrappers/bin/su -s /bin/sh caddy -c " php $INSTALL_DIR/occ db:add-missing-indices php $INSTALL_DIR/occ db:add-missing-columns php $INSTALL_DIR/occ db:add-missing-primary-keys " echo "Nextcloud DB maintenance complete." ''; }; systemd.timers.nextcloud-db-maintenance = { description = "Weekly Nextcloud DB maintenance"; wantedBy = [ "timers.target" ]; timerConfig = { OnCalendar = "Sun 03:30"; Persistent = true; RandomizedDelaySec = "30m"; }; }; services.cron.systemCronJobs = [ "*/5 * * * * caddy /run/current-system/sw/bin/php -f /var/lib/www/nextcloud/cron.php" ]; systemd.tmpfiles.rules = [ "d /var/lib/www 0755 caddy php -" "d /var/lib/www/nextcloud 0750 caddy php -" "d /var/lib/nextcloud 0770 caddy php -" ]; services.phpfpm.pools.nextcloud = { user = "caddy"; group = "php"; phpPackage = config.sovran_systemsOS.phpPackage; phpOptions = lib.mkAfter '' output_buffering = 0 ''; settings = { "pm" = "dynamic"; "pm.max_children" = 75; "pm.start_servers" = 10; "pm.min_spare_servers" = 5; "pm.max_spare_servers" = 20; "pm.max_requests" = 500; "clear_env" = "no"; "listen" = "/run/phpfpm/nextcloud.sock"; }; }; environment.systemPackages = with pkgs; [ unzip ]; sovran_systemsOS.domainRequirements = [ { name = "nextcloud"; label = "Nextcloud"; example = "cloud.yourdomain.com"; } ]; }