Add storage keepalive (upholds) across services, Nextcloud cron, disable Nextcloud PHP JIT

Requires= doesn't restart a service once its required mount comes back after
a USB disconnect/remount, so a dedicated storage-keepalive unit now Upholds=
every storage-dependent service to bring it back automatically. Also adds the
missing Nextcloud cron.php timer (background jobs were never triggered), and
disables opcache.jit in the Nextcloud container — PHP 8.4's JIT was
miscompiling OC\Config\UserConfig::getValueBool(), crashing every
session-bootstrapping request with a bogus strtolower() TypeError.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Aner Zakobar
2026-10-08 09:47:20 +03:00
parent da020a8dfe
commit c3115026e9
18 changed files with 110 additions and 0 deletions
+24
View File
@@ -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 # Local DNS overrides (optional — makes LAN clients hit the Pi directly
# instead of going through Cloudflare for *.zakobar.com) # instead of going through Cloudflare for *.zakobar.com)
+2
View File
@@ -103,6 +103,8 @@ in
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# Storage directory # Storage directory
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
homey.storage.upholds = [ "podman-aim-server.service" "podman-aim-ui.service" ];
homey.storage.extraDirs = [ homey.storage.extraDirs = [
{ path = "aim"; mode = "0755"; } { path = "aim"; mode = "0755"; }
]; ];
+2
View File
@@ -123,6 +123,8 @@ in
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
# Storage directories (not backed up — no backup.extraPaths entry) # Storage directories (not backed up — no backup.extraPaths entry)
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
homey.storage.upholds = [ "podman-attic.service" ];
homey.storage.extraDirs = [ homey.storage.extraDirs = [
{ path = "attic"; } { path = "attic"; }
{ path = "attic/cache"; mode = "0755"; } { path = "attic/cache"; mode = "0755"; }
+2
View File
@@ -252,6 +252,8 @@ in
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
# Storage directories # Storage directories
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
homey.storage.upholds = [ "podman-authelia.service" ];
homey.storage.extraDirs = [ homey.storage.extraDirs = [
{ path = "authelia"; } { path = "authelia"; }
{ path = "authelia/config"; } { path = "authelia/config"; }
+2
View File
@@ -108,6 +108,8 @@ in
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# Storage directories (owned by the service user) # Storage directories (owned by the service user)
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
homey.storage.upholds = [ "emulatorjs.service" ];
homey.storage.extraDirs = [ homey.storage.extraDirs = [
{ path = "emulatorjs"; user = "emulatorjs"; group = "emulatorjs"; } { path = "emulatorjs"; user = "emulatorjs"; group = "emulatorjs"; }
{ path = "emulatorjs/saves"; user = "emulatorjs"; group = "emulatorjs"; } { path = "emulatorjs/saves"; user = "emulatorjs"; group = "emulatorjs"; }
+2
View File
@@ -211,6 +211,8 @@ in
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
# Storage directories (UID 1000 = Gitea's internal user) # Storage directories (UID 1000 = Gitea's internal user)
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
homey.storage.upholds = [ "podman-gitea.service" ];
homey.storage.extraDirs = [ homey.storage.extraDirs = [
{ path = "gitea"; user = "1000"; group = "1000"; } { path = "gitea"; user = "1000"; group = "1000"; }
{ path = "gitea/data"; user = "1000"; group = "1000"; } { path = "gitea/data"; user = "1000"; group = "1000"; }
+2
View File
@@ -157,6 +157,8 @@ in
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# Storage directories # Storage directories
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
homey.storage.upholds = [ "podman-home-assistant.service" ];
homey.storage.extraDirs = [ homey.storage.extraDirs = [
{ path = "home-assistant"; } { path = "home-assistant"; }
{ path = "home-assistant/config"; mode = "0755"; } { path = "home-assistant/config"; mode = "0755"; }
+2
View File
@@ -145,6 +145,8 @@ EOF
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
# Storage directories # Storage directories
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
homey.storage.upholds = [ "podman-jellyfin.service" ];
homey.storage.extraDirs = [ homey.storage.extraDirs = [
{ path = "jellyfin"; } { path = "jellyfin"; }
{ path = "jellyfin/config"; } { path = "jellyfin/config"; }
+2
View File
@@ -113,6 +113,8 @@ in
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
# Storage directories # Storage directories
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
homey.storage.upholds = [ "podman-mealie.service" ];
homey.storage.extraDirs = [ homey.storage.extraDirs = [
{ path = "mealie"; } { path = "mealie"; }
{ path = "mealie/data"; mode = "0755"; } { path = "mealie/data"; mode = "0755"; }
+2
View File
@@ -118,6 +118,8 @@ in
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# Storage directories on external HD # Storage directories on external HD
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
homey.storage.upholds = [ "podman-mlflow.service" ];
homey.storage.extraDirs = [ homey.storage.extraDirs = [
{ path = "mlflow"; } { path = "mlflow"; }
{ path = "mlflow/data"; mode = "0750"; } { path = "mlflow/data"; mode = "0750"; }
+17
View File
@@ -49,6 +49,19 @@ let
MaxConnectionsPerChild 500 MaxConnectionsPerChild 500
</IfModule> </IfModule>
''; '';
# 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 in
{ {
options.homey.nextcloud = { options.homey.nextcloud = {
@@ -158,6 +171,8 @@ in
"${nextcloudCustomConfig}:/var/www/html/config/zakobar.config.php:ro" "${nextcloudCustomConfig}:/var/www/html/config/zakobar.config.php:ro"
# Apache MPM limits (caps concurrent PHP processes / disk writers) # Apache MPM limits (caps concurrent PHP processes / disk writers)
"${apacheMpmConfig}:/etc/apache2/mods-available/mpm_prefork.conf:ro" "${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 = [ extraOptions = [
@@ -210,6 +225,8 @@ in
# UID 33 = www-data in the Nextcloud container # UID 33 = www-data in the Nextcloud container
# UID 999 = postgres — must own the db dir (creates files directly in it) # 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 = [ homey.storage.extraDirs = [
{ path = "nextcloud"; } { path = "nextcloud"; }
{ path = "nextcloud/html"; user = "33"; group = "33"; } { path = "nextcloud/html"; user = "33"; group = "33"; }
+2
View File
@@ -198,6 +198,8 @@ in
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
# Storage directories (owned by the ntfy-sh system user) # Storage directories (owned by the ntfy-sh system user)
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
homey.storage.upholds = [ "ntfy-sh.service" ];
homey.storage.extraDirs = [ homey.storage.extraDirs = [
{ path = "ntfy"; user = "ntfy-sh"; group = "ntfy-sh"; } { path = "ntfy"; user = "ntfy-sh"; group = "ntfy-sh"; }
{ path = "ntfy/attachments"; user = "ntfy-sh"; group = "ntfy-sh"; } { path = "ntfy/attachments"; user = "ntfy-sh"; group = "ntfy-sh"; }
+2
View File
@@ -117,6 +117,8 @@ in
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
# Storage directories # Storage directories
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
homey.storage.upholds = [ "podman-openldap.service" ];
homey.storage.extraDirs = [ homey.storage.extraDirs = [
{ path = "openldap"; } { path = "openldap"; }
{ path = "openldap/etc-ldap-slapd.d"; } { path = "openldap/etc-ldap-slapd.d"; }
+2
View File
@@ -152,6 +152,8 @@ in
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
# Storage directories (UID 1000 = USERMAP_UID in container) # Storage directories (UID 1000 = USERMAP_UID in container)
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
homey.storage.upholds = [ "podman-paperless-redis.service" "podman-paperless.service" ];
homey.storage.extraDirs = [ homey.storage.extraDirs = [
{ path = "paperless"; } { path = "paperless"; }
{ path = "paperless/data"; user = "1000"; group = "1000"; } { path = "paperless/data"; user = "1000"; group = "1000"; }
+2
View File
@@ -72,6 +72,8 @@ in
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
# Storage directories # Storage directories
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
homey.storage.upholds = [ "podman-stirling-pdf.service" ];
homey.storage.extraDirs = [ homey.storage.extraDirs = [
{ path = "stirling-pdf"; } { path = "stirling-pdf"; }
{ path = "stirling-pdf/configs"; mode = "0755"; } { path = "stirling-pdf/configs"; mode = "0755"; }
+2
View File
@@ -87,6 +87,8 @@ in
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
# Storage directories # Storage directories
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
homey.storage.upholds = [ "podman-transmission.service" ];
homey.storage.extraDirs = [ homey.storage.extraDirs = [
{ path = "transmission"; user = "1000"; group = "1000"; mode = "0755"; } { path = "transmission"; user = "1000"; group = "1000"; mode = "0755"; }
{ path = "transmission/config"; user = "1000"; group = "1000"; mode = "0755"; } { path = "transmission/config"; user = "1000"; group = "1000"; mode = "0755"; }
+2
View File
@@ -305,6 +305,8 @@ in
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
# Storage directories # Storage directories
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
homey.storage.upholds = [ "podman-uptime-kuma.service" ];
homey.storage.extraDirs = [ homey.storage.extraDirs = [
{ path = "uptime-kuma"; } { path = "uptime-kuma"; }
]; ];
+39
View File
@@ -79,6 +79,28 @@ in
default = []; default = [];
description = "Per-service directories to create under mountPoint. Each service module contributes its own entries."; 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 != "") { 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). # Mount point root + shared infrastructure dirs (restic cache, shared media).
# Per-service dirs are contributed via homey.storage.extraDirs by each # Per-service dirs are contributed via homey.storage.extraDirs by each
# service module, so adding a new service only requires editing that module. # service module, so adding a new service only requires editing that module.