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:
@@ -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)
|
||||||
|
|||||||
@@ -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"; }
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -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"; }
|
||||||
|
|||||||
@@ -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"; }
|
||||||
|
|||||||
@@ -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"; }
|
||||||
|
|||||||
@@ -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"; }
|
||||||
|
|||||||
@@ -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"; }
|
||||||
|
|||||||
@@ -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"; }
|
||||||
|
|||||||
@@ -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"; }
|
||||||
|
|||||||
@@ -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"; }
|
||||||
|
|||||||
@@ -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"; }
|
||||||
|
|||||||
@@ -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"; }
|
||||||
|
|||||||
@@ -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"; }
|
||||||
|
|||||||
@@ -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"; }
|
||||||
|
|||||||
@@ -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"; }
|
||||||
|
|||||||
@@ -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"; }
|
||||||
|
|||||||
@@ -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"; }
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user