diff --git a/hosts/pi-main/default.nix b/hosts/pi-main/default.nix index 24b2d8d..84e9e2a 100644 --- a/hosts/pi-main/default.nix +++ b/hosts/pi-main/default.nix @@ -222,6 +222,30 @@ }; }; + # Nextcloud's "cron" backgroundjobs_mode (config:app:get core backgroundjobs_mode) + # doesn't schedule itself — something external must call cron.php periodically. + # Nothing did, so background jobs (including the News app's feed fetcher) + # went stale. Nextcloud recommends every 5 minutes. + systemd.services.nextcloud-cron = { + description = "Nextcloud background job runner (cron.php)"; + after = [ "podman-nextcloud.service" ]; + requires = [ "podman-nextcloud.service" ]; + serviceConfig = { + Type = "oneshot"; + ExecStart = "${pkgs.podman}/bin/podman exec -u www-data nextcloud php -f cron.php"; + }; + }; + + systemd.timers.nextcloud-cron = { + description = "Run Nextcloud cron.php every 5 minutes"; + wantedBy = [ "timers.target" ]; + timerConfig = { + OnBootSec = "5min"; + OnUnitActiveSec = "5min"; + Persistent = true; + }; + }; + # ------------------------------------------------------------------------- # Local DNS overrides (optional — makes LAN clients hit the Pi directly # instead of going through Cloudflare for *.zakobar.com) diff --git a/modules/services/aim.nix b/modules/services/aim.nix index 207481c..d7015bb 100644 --- a/modules/services/aim.nix +++ b/modules/services/aim.nix @@ -103,6 +103,8 @@ in # ------------------------------------------------------------------------- # Storage directory # ------------------------------------------------------------------------- + homey.storage.upholds = [ "podman-aim-server.service" "podman-aim-ui.service" ]; + homey.storage.extraDirs = [ { path = "aim"; mode = "0755"; } ]; diff --git a/modules/services/attic.nix b/modules/services/attic.nix index 15d906f..41bf7fa 100644 --- a/modules/services/attic.nix +++ b/modules/services/attic.nix @@ -123,6 +123,8 @@ in # ----------------------------------------------------------------------- # Storage directories (not backed up — no backup.extraPaths entry) # ----------------------------------------------------------------------- + homey.storage.upholds = [ "podman-attic.service" ]; + homey.storage.extraDirs = [ { path = "attic"; } { path = "attic/cache"; mode = "0755"; } diff --git a/modules/services/authelia.nix b/modules/services/authelia.nix index 7b6cd7a..ca4b30a 100644 --- a/modules/services/authelia.nix +++ b/modules/services/authelia.nix @@ -252,6 +252,8 @@ in # ----------------------------------------------------------------------- # Storage directories # ----------------------------------------------------------------------- + homey.storage.upholds = [ "podman-authelia.service" ]; + homey.storage.extraDirs = [ { path = "authelia"; } { path = "authelia/config"; } diff --git a/modules/services/emulatorjs.nix b/modules/services/emulatorjs.nix index d4cdcdb..0b81b2f 100644 --- a/modules/services/emulatorjs.nix +++ b/modules/services/emulatorjs.nix @@ -108,6 +108,8 @@ in # ------------------------------------------------------------------------- # Storage directories (owned by the service user) # ------------------------------------------------------------------------- + homey.storage.upholds = [ "emulatorjs.service" ]; + homey.storage.extraDirs = [ { path = "emulatorjs"; user = "emulatorjs"; group = "emulatorjs"; } { path = "emulatorjs/saves"; user = "emulatorjs"; group = "emulatorjs"; } diff --git a/modules/services/gitea.nix b/modules/services/gitea.nix index c15fcbd..6487a53 100644 --- a/modules/services/gitea.nix +++ b/modules/services/gitea.nix @@ -211,6 +211,8 @@ in # ----------------------------------------------------------------------- # Storage directories (UID 1000 = Gitea's internal user) # ----------------------------------------------------------------------- + homey.storage.upholds = [ "podman-gitea.service" ]; + homey.storage.extraDirs = [ { path = "gitea"; user = "1000"; group = "1000"; } { path = "gitea/data"; user = "1000"; group = "1000"; } diff --git a/modules/services/home-assistant.nix b/modules/services/home-assistant.nix index 4c34e81..cb46bf9 100644 --- a/modules/services/home-assistant.nix +++ b/modules/services/home-assistant.nix @@ -157,6 +157,8 @@ in # ------------------------------------------------------------------------- # Storage directories # ------------------------------------------------------------------------- + homey.storage.upholds = [ "podman-home-assistant.service" ]; + homey.storage.extraDirs = [ { path = "home-assistant"; } { path = "home-assistant/config"; mode = "0755"; } diff --git a/modules/services/jellyfin.nix b/modules/services/jellyfin.nix index 0c3a963..df1a754 100644 --- a/modules/services/jellyfin.nix +++ b/modules/services/jellyfin.nix @@ -145,6 +145,8 @@ EOF # ----------------------------------------------------------------------- # Storage directories # ----------------------------------------------------------------------- + homey.storage.upholds = [ "podman-jellyfin.service" ]; + homey.storage.extraDirs = [ { path = "jellyfin"; } { path = "jellyfin/config"; } diff --git a/modules/services/mealie.nix b/modules/services/mealie.nix index 53013eb..f4742dd 100644 --- a/modules/services/mealie.nix +++ b/modules/services/mealie.nix @@ -113,6 +113,8 @@ in # ----------------------------------------------------------------------- # Storage directories # ----------------------------------------------------------------------- + homey.storage.upholds = [ "podman-mealie.service" ]; + homey.storage.extraDirs = [ { path = "mealie"; } { path = "mealie/data"; mode = "0755"; } diff --git a/modules/services/mlflow.nix b/modules/services/mlflow.nix index 2886fdb..b87d066 100644 --- a/modules/services/mlflow.nix +++ b/modules/services/mlflow.nix @@ -118,6 +118,8 @@ in # ------------------------------------------------------------------------- # Storage directories on external HD # ------------------------------------------------------------------------- + homey.storage.upholds = [ "podman-mlflow.service" ]; + homey.storage.extraDirs = [ { path = "mlflow"; } { path = "mlflow/data"; mode = "0750"; } diff --git a/modules/services/nextcloud.nix b/modules/services/nextcloud.nix index c398a6e..c893ed3 100644 --- a/modules/services/nextcloud.nix +++ b/modules/services/nextcloud.nix @@ -49,6 +49,19 @@ let MaxConnectionsPerChild 500 ''; + + # PHP 8.4's JIT miscompiles OC\Config\UserConfig::getValueBool() on this + # image, corrupting the type of the value passed to strtolower() and + # throwing "Argument #1 ($string) must be of type string, string given" + # on every request that bootstraps a user session (confirmed by disabling + # JIT live on 2026-10-08: the crash stopped immediately). Not an app or + # data bug — getTypedValue()'s `: string` return type and + # decryptSensitiveValue()'s `string &$value` by-ref param both independently + # guarantee the value is already a real string under strict_types=1 by the + # time strtolower() rejects it. + phpDisableJitConfig = pkgs.writeText "zzz-disable-jit.ini" '' + opcache.jit=0 + ''; in { options.homey.nextcloud = { @@ -158,6 +171,8 @@ in "${nextcloudCustomConfig}:/var/www/html/config/zakobar.config.php:ro" # Apache MPM limits (caps concurrent PHP processes / disk writers) "${apacheMpmConfig}:/etc/apache2/mods-available/mpm_prefork.conf:ro" + # Disable JIT — see phpDisableJitConfig comment above for why. + "${phpDisableJitConfig}:/usr/local/etc/php/conf.d/zzz-disable-jit.ini:ro" ]; extraOptions = [ @@ -210,6 +225,8 @@ in # UID 33 = www-data in the Nextcloud container # UID 999 = postgres — must own the db dir (creates files directly in it) # ----------------------------------------------------------------------- + homey.storage.upholds = [ "podman-nextcloud-postgres.service" "podman-nextcloud.service" ]; + homey.storage.extraDirs = [ { path = "nextcloud"; } { path = "nextcloud/html"; user = "33"; group = "33"; } diff --git a/modules/services/ntfy.nix b/modules/services/ntfy.nix index 172fa5b..1977cc9 100644 --- a/modules/services/ntfy.nix +++ b/modules/services/ntfy.nix @@ -198,6 +198,8 @@ in # ----------------------------------------------------------------------- # Storage directories (owned by the ntfy-sh system user) # ----------------------------------------------------------------------- + homey.storage.upholds = [ "ntfy-sh.service" ]; + homey.storage.extraDirs = [ { path = "ntfy"; user = "ntfy-sh"; group = "ntfy-sh"; } { path = "ntfy/attachments"; user = "ntfy-sh"; group = "ntfy-sh"; } diff --git a/modules/services/openldap.nix b/modules/services/openldap.nix index fe04a2d..975ac94 100644 --- a/modules/services/openldap.nix +++ b/modules/services/openldap.nix @@ -117,6 +117,8 @@ in # ----------------------------------------------------------------------- # Storage directories # ----------------------------------------------------------------------- + homey.storage.upholds = [ "podman-openldap.service" ]; + homey.storage.extraDirs = [ { path = "openldap"; } { path = "openldap/etc-ldap-slapd.d"; } diff --git a/modules/services/paperless.nix b/modules/services/paperless.nix index addba6f..ed99809 100644 --- a/modules/services/paperless.nix +++ b/modules/services/paperless.nix @@ -152,6 +152,8 @@ in # ----------------------------------------------------------------------- # Storage directories (UID 1000 = USERMAP_UID in container) # ----------------------------------------------------------------------- + homey.storage.upholds = [ "podman-paperless-redis.service" "podman-paperless.service" ]; + homey.storage.extraDirs = [ { path = "paperless"; } { path = "paperless/data"; user = "1000"; group = "1000"; } diff --git a/modules/services/stirling-pdf.nix b/modules/services/stirling-pdf.nix index ce776b2..b63d28e 100644 --- a/modules/services/stirling-pdf.nix +++ b/modules/services/stirling-pdf.nix @@ -72,6 +72,8 @@ in # ----------------------------------------------------------------------- # Storage directories # ----------------------------------------------------------------------- + homey.storage.upholds = [ "podman-stirling-pdf.service" ]; + homey.storage.extraDirs = [ { path = "stirling-pdf"; } { path = "stirling-pdf/configs"; mode = "0755"; } diff --git a/modules/services/transmission.nix b/modules/services/transmission.nix index ade3b72..cf9a754 100644 --- a/modules/services/transmission.nix +++ b/modules/services/transmission.nix @@ -87,6 +87,8 @@ in # ----------------------------------------------------------------------- # Storage directories # ----------------------------------------------------------------------- + homey.storage.upholds = [ "podman-transmission.service" ]; + homey.storage.extraDirs = [ { path = "transmission"; user = "1000"; group = "1000"; mode = "0755"; } { path = "transmission/config"; user = "1000"; group = "1000"; mode = "0755"; } diff --git a/modules/services/uptime-kuma.nix b/modules/services/uptime-kuma.nix index 4d7c462..e0b9047 100644 --- a/modules/services/uptime-kuma.nix +++ b/modules/services/uptime-kuma.nix @@ -305,6 +305,8 @@ in # ----------------------------------------------------------------------- # Storage directories # ----------------------------------------------------------------------- + homey.storage.upholds = [ "podman-uptime-kuma.service" ]; + homey.storage.extraDirs = [ { path = "uptime-kuma"; } ]; diff --git a/modules/storage.nix b/modules/storage.nix index d11e9c1..ec8d434 100644 --- a/modules/storage.nix +++ b/modules/storage.nix @@ -79,6 +79,28 @@ in default = []; description = "Per-service directories to create under mountPoint. Each service module contributes its own entries."; }; + + upholds = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = []; + description = '' + Systemd unit names (e.g. "podman-openldap.service") that must come + back up on their own after the external drive remounts. + + These units already have Requires=mnt-data.mount, so they correctly + stop when the drive disappears. The problem is the reverse direction: + a plain Requires= dependency does NOT restart when the required unit + becomes available again — systemd treats a Requires-propagated stop + the same as a deliberate "systemctl stop", so Restart= policies on + the unit itself never fire either. Verified empirically on 2026-10-02 + after a USB disconnect/remount left 11 services dead for ~4 hours. + + The fix is a dedicated keepalive unit bound to the mount (below) that + Upholds= every unit listed here — Upholds continuously (re)starts its + targets for as long as the upholding unit is active, which is exactly + the "come back when the mount comes back" behavior Requires= lacks. + ''; + }; }; config = lib.mkIf (cfg.device != "") { @@ -95,6 +117,23 @@ in ]; }; + # See the `upholds` option doc above for why this unit exists. + systemd.services.storage-keepalive = + let + mountUnit = "${lib.replaceStrings [ "/" ] [ "-" ] (lib.removePrefix "/" cfg.mountPoint)}.mount"; + in { + description = "Keeps storage-dependent services running whenever ${cfg.mountPoint} is mounted"; + bindsTo = [ mountUnit ]; + after = [ mountUnit ]; + wantedBy = [ mountUnit ]; + unitConfig.Upholds = cfg.upholds; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + ExecStart = "${pkgs.coreutils}/bin/true"; + }; + }; + # Mount point root + shared infrastructure dirs (restic cache, shared media). # Per-service dirs are contributed via homey.storage.extraDirs by each # service module, so adding a new service only requires editing that module.