From 27ad5176262721926c373aac0ca1f935e66f8b30 Mon Sep 17 00:00:00 2001 From: librelad Date: Sat, 23 May 2026 15:34:17 +0100 Subject: [PATCH] feat(backup): per-app strategy override (advanced, context-aware) Adds CFG__BACKUP_STRATEGY (default auto) so an app's backup strategy can be overridden from its Advanced config tab, taking precedence over the global default. Added to the 10 live-capable apps, so the dropdown's 'live' option only appears where it actually works. - backupResolveStrategy now checks the per-app override before the global value. - backupAppLiveCapable / backupAppStrategyOptions expose capability + the valid option set; predicate helpers hardened with explicit returns so they behave identically with or without shell errexit. - BACKUP_STRATEGY field mapping (select, advanced) renders the dropdown. Co-Authored-By: Claude Opus 4.7 Signed-off-by: librelad --- containers/authelia/authelia.config | 1 + containers/bookstack/bookstack.config | 1 + containers/headscale/headscale.config | 1 + containers/invidious/invidious.config | 1 + containers/linkding/linkding.config | 1 + containers/mastodon/mastodon.config | 1 + containers/nextcloud/nextcloud.config | 1 + containers/owncloud/owncloud.config | 1 + containers/trilium/trilium.config | 1 + containers/vaultwarden/vaultwarden.config | 1 + scripts/backup/db/backup_db.sh | 51 ++++++++++++++++--- .../webui_create_app_field_mappings.sh | 14 +++++ 12 files changed, 67 insertions(+), 8 deletions(-) diff --git a/containers/authelia/authelia.config b/containers/authelia/authelia.config index 3969798..65a2c1a 100755 --- a/containers/authelia/authelia.config +++ b/containers/authelia/authelia.config @@ -17,6 +17,7 @@ CFG_AUTHELIA_APP_NAME=authelia CFG_AUTHELIA_REQUIRES="domain,traefik" CFG_AUTHELIA_BACKUP=true +CFG_AUTHELIA_BACKUP_STRATEGY=auto CFG_AUTHELIA_COMPOSE_FILE=default CFG_AUTHELIA_HEALTHCHECK=true CFG_AUTHELIA_AUTHELIA=false diff --git a/containers/bookstack/bookstack.config b/containers/bookstack/bookstack.config index 79d6258..1bf4dbb 100644 --- a/containers/bookstack/bookstack.config +++ b/containers/bookstack/bookstack.config @@ -13,6 +13,7 @@ # CFG_BOOKSTACK_APP_NAME=bookstack CFG_BOOKSTACK_BACKUP=true +CFG_BOOKSTACK_BACKUP_STRATEGY=auto CFG_BOOKSTACK_COMPOSE_FILE=default CFG_BOOKSTACK_HEALTHCHECK=true CFG_BOOKSTACK_AUTHELIA=false diff --git a/containers/headscale/headscale.config b/containers/headscale/headscale.config index 40fef03..5c82bf8 100755 --- a/containers/headscale/headscale.config +++ b/containers/headscale/headscale.config @@ -11,6 +11,7 @@ # CFG_HEADSCALE_APP_NAME=headscale CFG_HEADSCALE_BACKUP=true +CFG_HEADSCALE_BACKUP_STRATEGY=auto CFG_HEADSCALE_COMPOSE_FILE=default CFG_HEADSCALE_HEALTHCHECK=true CFG_HEADSCALE_BASIC_AUTH_PASS=RANDOMIZEDPASSWORD1 diff --git a/containers/invidious/invidious.config b/containers/invidious/invidious.config index 168b4da..2698c6d 100755 --- a/containers/invidious/invidious.config +++ b/containers/invidious/invidious.config @@ -11,6 +11,7 @@ # CFG_INVIDIOUS_APP_NAME=invidious CFG_INVIDIOUS_BACKUP=false +CFG_INVIDIOUS_BACKUP_STRATEGY=auto CFG_INVIDIOUS_COMPOSE_FILE=default CFG_INVIDIOUS_HEALTHCHECK=false CFG_INVIDIOUS_AUTHELIA=false diff --git a/containers/linkding/linkding.config b/containers/linkding/linkding.config index 6789ae0..8685308 100755 --- a/containers/linkding/linkding.config +++ b/containers/linkding/linkding.config @@ -11,6 +11,7 @@ # CFG_LINKDING_APP_NAME=linkding CFG_LINKDING_BACKUP=true +CFG_LINKDING_BACKUP_STRATEGY=auto CFG_LINKDING_COMPOSE_FILE=default CFG_LINKDING_HEALTHCHECK=true CFG_LINKDING_AUTHELIA=false diff --git a/containers/mastodon/mastodon.config b/containers/mastodon/mastodon.config index d7c1388..8b8ac03 100755 --- a/containers/mastodon/mastodon.config +++ b/containers/mastodon/mastodon.config @@ -11,6 +11,7 @@ # CFG_MASTODON_APP_NAME=mastodon CFG_MASTODON_BACKUP=true +CFG_MASTODON_BACKUP_STRATEGY=auto CFG_MASTODON_COMPOSE_FILE=default CFG_MASTODON_HEALTHCHECK=true CFG_MASTODON_AUTHELIA=false diff --git a/containers/nextcloud/nextcloud.config b/containers/nextcloud/nextcloud.config index c326af0..83f22f2 100755 --- a/containers/nextcloud/nextcloud.config +++ b/containers/nextcloud/nextcloud.config @@ -15,6 +15,7 @@ # CFG_NEXTCLOUD_APP_NAME=nextcloud CFG_NEXTCLOUD_BACKUP=true +CFG_NEXTCLOUD_BACKUP_STRATEGY=auto CFG_NEXTCLOUD_COMPOSE_FILE=default CFG_NEXTCLOUD_HEALTHCHECK=true CFG_NEXTCLOUD_AUTHELIA=false diff --git a/containers/owncloud/owncloud.config b/containers/owncloud/owncloud.config index be6ffa6..4410f04 100755 --- a/containers/owncloud/owncloud.config +++ b/containers/owncloud/owncloud.config @@ -11,6 +11,7 @@ # CFG_OWNCLOUD_APP_NAME=owncloud CFG_OWNCLOUD_BACKUP=true +CFG_OWNCLOUD_BACKUP_STRATEGY=auto CFG_OWNCLOUD_COMPOSE_FILE=default CFG_OWNCLOUD_HEALTHCHECK=true CFG_OWNCLOUD_AUTHELIA=false diff --git a/containers/trilium/trilium.config b/containers/trilium/trilium.config index 5427cfd..2f428f7 100755 --- a/containers/trilium/trilium.config +++ b/containers/trilium/trilium.config @@ -11,6 +11,7 @@ # CFG_TRILIUM_APP_NAME=trilium CFG_TRILIUM_BACKUP=true +CFG_TRILIUM_BACKUP_STRATEGY=auto CFG_TRILIUM_COMPOSE_FILE=default CFG_TRILIUM_HEALTHCHECK=true CFG_TRILIUM_AUTHELIA=false diff --git a/containers/vaultwarden/vaultwarden.config b/containers/vaultwarden/vaultwarden.config index af6ec79..2e63312 100755 --- a/containers/vaultwarden/vaultwarden.config +++ b/containers/vaultwarden/vaultwarden.config @@ -12,6 +12,7 @@ # CFG_VAULTWARDEN_APP_NAME=vaultwarden CFG_VAULTWARDEN_BACKUP=true +CFG_VAULTWARDEN_BACKUP_STRATEGY=auto CFG_VAULTWARDEN_COMPOSE_FILE=default CFG_VAULTWARDEN_HEALTHCHECK=false CFG_VAULTWARDEN_AUTHELIA=false diff --git a/scripts/backup/db/backup_db.sh b/scripts/backup/db/backup_db.sh index 336cf4c..35a8a9b 100644 --- a/scripts/backup/db/backup_db.sh +++ b/scripts/backup/db/backup_db.sh @@ -55,7 +55,8 @@ backupDbDescriptors() backupDbHasDescriptors() { local app="$1" - [[ -n "$(backupDbDescriptors "$app")" ]] + if [[ -n "$(backupDbDescriptors "$app")" ]]; then return 0; fi + return 1 } # True when the app carries `libreportal.backup.live: "true"` — i.e. its data is @@ -65,26 +66,60 @@ backupAppIsLiveSafe() local app="$1" local compose="$containers_dir$app/docker-compose.yml" [[ -f "$compose" ]] || return 1 - grep -qE '^[[:space:]]*libreportal\.backup\.live[[:space:]]*:[[:space:]]*["'\'']?true' "$compose" 2>/dev/null + if grep -qE '^[[:space:]]*libreportal\.backup\.live[[:space:]]*:[[:space:]]*["'\'']?true' "$compose" 2>/dev/null; then + return 0 + fi + return 1 } -# Resolve the effective strategy for one app. Explicit settings are honoured as -# power-user overrides; the default "auto" goes live only where we can guarantee -# consistency (a dumpable database, or an app blessed live-safe) and otherwise -# falls back to the always-safe stop-snapshot-start. +# An app can be backed up live without downtime when we can make it consistent: +# it has a dumpable database, or it is explicitly blessed live-safe. +backupAppLiveCapable() +{ + local app="$1" + if backupDbHasDescriptors "$app"; then return 0; fi + if backupAppIsLiveSafe "$app"; then return 0; fi + return 1 +} + +# Strategy options valid for one app, in .config "[a:b|c:d]" syntax. live is +# offered only where the app can actually do it, so the UI never shows a choice +# that would just fall back to stop. +backupAppStrategyOptions() +{ + local app="$1" + local opts="auto:Automatic — recommended|stop-snapshot-start:Stop → snapshot → start|pause-snapshot-unpause:Pause → snapshot → unpause" + if backupAppLiveCapable "$app"; then + opts="$opts|live:Live — no downtime" + fi + echo "$opts" + return 0 +} + +# Resolve the effective strategy for one app. Order of precedence: +# 1. per-app override CFG__BACKUP_STRATEGY (advanced, defaults to auto) +# 2. global default CFG_BACKUP_STRATEGY (auto) +# An explicit stop/pause/live is honoured as-is; "auto" goes live only where the +# app is live-capable and otherwise uses the always-safe stop-snapshot-start. backupResolveStrategy() { local app="$1" - local s="${CFG_BACKUP_STRATEGY:-auto}" + local override_key="CFG_${app^^}_BACKUP_STRATEGY" + local s="${!override_key}" + if [[ -z "$s" || "$s" == "auto" ]]; then + s="${CFG_BACKUP_STRATEGY:-auto}" + fi + case "$s" in live|pause-snapshot-unpause|stop-snapshot-start) echo "$s"; return 0 ;; esac - if backupDbHasDescriptors "$app" || backupAppIsLiveSafe "$app"; then + if backupAppLiveCapable "$app"; then echo "live" else echo "stop-snapshot-start" fi + return 0 } # Deterministic dump filename for a descriptor — backup writes it, restore reads diff --git a/scripts/webui/data/generators/categories/webui_create_app_field_mappings.sh b/scripts/webui/data/generators/categories/webui_create_app_field_mappings.sh index 4ac0565..4025aa6 100755 --- a/scripts/webui/data/generators/categories/webui_create_app_field_mappings.sh +++ b/scripts/webui/data/generators/categories/webui_create_app_field_mappings.sh @@ -159,6 +159,20 @@ PORTEOF "tooltip": "Enable automatic backups for this application", "default": false }, + "BACKUP_STRATEGY": { + "category": "advanced", + "label": "Backup Strategy", + "type": "select", + "tooltip": "How this app is quiesced before its backup snapshot. Automatic picks the safest zero-downtime method available (a live, consistent database dump for this app), and falls back to stopping the app if a live dump ever fails. Only shown for apps that can be backed up live.", + "advanced": true, + "options": [ + {"value": "auto", "label": "Automatic (recommended)"}, + {"value": "stop-snapshot-start", "label": "Stop → snapshot → start"}, + {"value": "pause-snapshot-unpause", "label": "Pause → snapshot → unpause"}, + {"value": "live", "label": "Live — no downtime"} + ], + "default": "auto" + }, "MONITORING": { "category": "features", "label": "Export metrics to Grafana",