diff --git a/CLAUDE.md b/CLAUDE.md index b852a9d..c078b97 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -493,6 +493,7 @@ right (Portainer, ntfy), not general familiarity with the product: | Service | Native OIDC? | Notes | |---|---|---| | `mealie` | Yes — wired up | Pure env vars (`OIDC_AUTH_ENABLED`, `OIDC_CLIENT_ID/SECRET`, `OIDC_CONFIGURATION_URL`), see `_mealie_offer_authelia_oidc()`. Redirect URI is `/login`. Needs a `--forwarded-allow-ips` entrypoint override when Caddy-fronted, or the generated redirect URI comes out `http://` even when actually served over `https://` — see the function's own comment. | +| `homebox` | Yes — wired up | Pure env vars (`HBOX_OIDC_ENABLED`, `HBOX_OIDC_ISSUER_URL`, `HBOX_OIDC_CLIENT_ID/SECRET`, `HBOX_OIDC_SCOPE`), see `_homebox_offer_authelia_oidc()`. Confirmed against homebox.software's own OIDC docs and authelia.com's Homebox integration page — needs PKCE (unlike Mealie/ActualBudget). Redirect path is `/api/v1/users/login/oidc/callback`; issuer URL is reportedly sensitive to a trailing slash (a real upstream bug), so it's written from this repo's own portal-URL value as-is, never with one appended. The stock compose template didn't have `env_file: .env` (vars were listed individually in `environment:` instead) — added to the template, and patched onto any pre-existing install's compose file the first time this offer runs, or the written `.env` additions would silently never reach the container. `HBOX_OPTIONS_ALLOW_LOCAL_LOGIN=false`/`HBOX_OIDC_AUTO_REDIRECT=true` are real, documented env vars for fully replacing local login, offered as a separate step gated behind the same "have you tested the button first" confirmation as Mealie/Beszel. | | `actualbudget` | Yes — wired up | Pure env vars (`ACTUAL_OPENID_DISCOVERY_URL`, `ACTUAL_OPENID_CLIENT_ID/SECRET`, `ACTUAL_OPENID_SERVER_HOSTNAME`), see `_actualbudget_offer_authelia_oidc()`. Redirect path `/openid/callback` (matches the existing preset in `_authelia_add_oidc_client()`'s menu). First OIDC login becomes the server owner if none is set yet — Actual's own behavior. | | `immich` | Yes — wired up | Real OAuth2/OIDC settings under Administration → Settings, backed by `GET`/`PUT /api/system-config` — confirmed the exact JSON field names against Immich's own `config-file.md` and source directly (the `oauth` sub-object: `enabled`/`issuerUrl`/`clientId`/`clientSecret`/`scope`/`buttonText`, etc.), not guessed. See `_immich_offer_authelia_oidc()`. GET/PUT exchange the *whole* config object (no partial-patch endpoint), so it round-trips everything else — storage template, library settings — completely unchanged; the same shape already proven by `import-photos.sh`'s own storage-template step in this file. Needs an admin API key, which doesn't exist until the user creates their account on first web visit — this offer runs from both the fresh-install path (usually a no-op that first time) and the "update" rerun path, which is the realistic way most people finish this. | | `audiobookshelf` | Yes — wired up (Authelia side only) | Checked against audiobookshelf.org's own OIDC docs: config is UI-only (Settings → Authentication), no env var or config API — so `_audiobookshelf_offer_authelia_oidc()` registers the Authelia client (needs PKCE, confirmed via authelia.com's own integration page for it) and prints the exact individual-endpoint values to paste in, since Audiobookshelf wants those rather than a discovery URL. Three redirect URIs: web callback, mobile-redirect, and the `audiobookshelf://oauth` app-scheme callback. | diff --git a/lib/common.sh b/lib/common.sh index ea9340a..3389064 100644 --- a/lib/common.sh +++ b/lib/common.sh @@ -802,6 +802,32 @@ write_readme() { chown "$ACTUAL_USER:$ACTUAL_USER" "$dir/README.md" 2>/dev/null || true } +# Copies FILE to FILE.bak. if it already exists, right before a +# caller is about to overwrite it with a fresh `cat > FILE` heredoc. No-ops +# in DRY_RUN and silently no-ops if FILE doesn't exist yet (first install, +# nothing to save) — safe to call unconditionally right before every such +# write, fresh install or not. +# +# Confirmed live: install_frigate()'s fresh-install path overwrote a +# working, hand-crafted multi-container docker-compose.yml (Frigate + +# mosquitto + frigate-notify) with zero backup, because that file's shape +# didn't match what the service's own "existing install" detection knew +# how to recognize. Every service's own detection logic is a judgment call +# about what counts as "already installed" and can miss a real setup built +# outside this repo's own conventions — this exists as the safety net +# underneath that judgment call, not a replacement for it: call it right +# before any `cat > FILE` that could clobber something a user already has, +# so a wrong detection costs a `.bak` file to restore from instead of the +# original silently disappearing. +backup_if_exists() { + local file="$1" + [ "$DRY_RUN" = true ] && return 0 + [ -f "$file" ] || return 0 + local backup="${file}.bak.$(date +%Y%m%d-%H%M%S)" + cp -p "$file" "$backup" 2>/dev/null \ + && log_info "Backed up existing $(basename "$file") to $(basename "$backup")" +} + # ── Host port collision avoidance (shared by every service that publishes a # fixed host port) ──────────────────────────────────────────────────────────── # With 70+ services in this repo, several ship the same default port (e.g. diff --git a/services/actualbudget.sh b/services/actualbudget.sh index c54accd..e0cdeaf 100644 --- a/services/actualbudget.sh +++ b/services/actualbudget.sh @@ -180,6 +180,11 @@ CBLOCK mkdir -p "$_dir" cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } fi ACTUAL_USER="${ACTUAL_USER:-${SUDO_USER:-$USER}}" @@ -388,6 +393,7 @@ networks: " fi + backup_if_exists docker-compose.yml cat > docker-compose.yml << AB_COMPOSE name: $CONTAINER @@ -405,6 +411,7 @@ services: ${_CADDY_NET_BLOCK}${_CADDY_NET_SECTION} AB_COMPOSE + backup_if_exists .env cat > .env << AB_ENV TZ=$TZ_VAL CADDY_NET=$SITE_CADDY_NET diff --git a/services/ai-gpu.sh b/services/ai-gpu.sh index 769c58a..daa3a29 100644 --- a/services/ai-gpu.sh +++ b/services/ai-gpu.sh @@ -170,6 +170,11 @@ CBLOCK [[ "${DRY_RUN:-false}" == "true" ]] && return 0 cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } generate_password() { local _len="${1:-32}" @@ -368,6 +373,7 @@ install_ai-gpu() { sed -i "s|America/New_York|$TZ_VAL|g" {} \; fi + backup_if_exists "$IMAGE_GEN_DIR/.env" cat > "$IMAGE_GEN_DIR/.env" << IMGENV # InvokeAI — image generation TZ=${TZ_VAL} @@ -407,6 +413,7 @@ IMGENV local WEBUI_SECRET WEBUI_SECRET="$(generate_password 32)" + backup_if_exists "$LLM_DIR/.env" cat > "$LLM_DIR/.env" << LLMENV # Ollama + Open WebUI + SearXNG TZ=${TZ_VAL} @@ -441,6 +448,7 @@ LLMENV fi fi + backup_if_exists "$PORTAL_DIR/.env" cat > "$PORTAL_DIR/.env" << PORTALENV # AI Portal — GPU stack swap controller TZ=${TZ_VAL} diff --git a/services/archivebox.sh b/services/archivebox.sh index 1d85b3b..f4bc9b1 100644 --- a/services/archivebox.sh +++ b/services/archivebox.sh @@ -174,6 +174,11 @@ CBLOCK mkdir -p "$_dir" cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } fi ACTUAL_USER="${ACTUAL_USER:-${SUDO_USER:-$USER}}" @@ -240,6 +245,7 @@ networks: " fi + backup_if_exists docker-compose.yml cat > docker-compose.yml << ABCOMPOSE name: archivebox @@ -262,6 +268,7 @@ services: ${_CADDY_NET_BLOCK}${_CADDY_NET_SECTION} ABCOMPOSE + backup_if_exists .env cat > .env << ABENV CADDY_NET=$SITE_CADDY_NET ABENV diff --git a/services/arm.sh b/services/arm.sh index faf6b92..a9654eb 100644 --- a/services/arm.sh +++ b/services/arm.sh @@ -188,6 +188,11 @@ CBLOCK mkdir -p "$_dir" cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } fi # Globals — ACTUAL_USER/ACTUAL_HOME must come before DOCKER_DIR @@ -278,6 +283,7 @@ networks: " fi + backup_if_exists docker-compose.yml cat > docker-compose.yml << ARM_COMPOSE name: arm @@ -306,6 +312,7 @@ services: ${_CADDY_NET_BLOCK}${_CADDY_NET_SECTION} ARM_COMPOSE + backup_if_exists .env cat > .env << ARM_ENV ARM_OUTPUT=$ARM_OUTPUT CADDY_NET=$SITE_CADDY_NET diff --git a/services/asterisk.sh b/services/asterisk.sh index 36b88f5..d1f1a22 100644 --- a/services/asterisk.sh +++ b/services/asterisk.sh @@ -255,6 +255,11 @@ CBLOCK [[ "${DRY_RUN:-false}" == "true" ]] && return 0 cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } generate_password() { local _len="${1:-32}" @@ -1588,6 +1593,7 @@ _asterisk_write_compose() { " [[ "$USE_EMBEDDED_COTURN" != true ]] && _COTURN_DEPENDS="" && _COTURN_SERVICE="" + backup_if_exists docker-compose.yml cat > docker-compose.yml << EOF name: PROJECT_NAME_PLACEHOLDER @@ -2727,6 +2733,7 @@ install_asterisk() { # WireGuard/Tailscale) on a subnet this box isn't directly attached to." fi + backup_if_exists .env cat > .env << ENV # ── Domain ──────────────────────────────────────────────────── # ${_domain_comment} diff --git a/services/audiobookshelf.sh b/services/audiobookshelf.sh index f385064..da531f3 100644 --- a/services/audiobookshelf.sh +++ b/services/audiobookshelf.sh @@ -180,6 +180,11 @@ CBLOCK mkdir -p "$_dir" cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } fi ACTUAL_USER="${ACTUAL_USER:-${SUDO_USER:-$USER}}" @@ -379,6 +384,7 @@ networks: " fi + backup_if_exists docker-compose.yml cat > docker-compose.yml << ABS_COMPOSE name: $CONTAINER @@ -400,6 +406,7 @@ services: ${_CADDY_NET_BLOCK}${_CADDY_NET_SECTION} ABS_COMPOSE + backup_if_exists .env cat > .env << ABS_ENV AUDIOBOOKS_PATH=$AUDIOBOOKS_PATH PODCASTS_PATH=./podcasts diff --git a/services/authelia.sh b/services/authelia.sh index 70bba65..9405896 100644 --- a/services/authelia.sh +++ b/services/authelia.sh @@ -186,6 +186,11 @@ CBLOCK mkdir -p "$_dir" cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } fi # Globals — ACTUAL_USER/ACTUAL_HOME must come before DOCKER_DIR @@ -408,6 +413,7 @@ install_authelia() { cd "$AUTHELIA_DIR" || return 1 # ── .env ───────────────────────────────────────────────────────────────── + backup_if_exists .env cat > .env << AUTHELIA_ENV MY_DOMAIN=${AUTHELIA_DOMAIN} SMTP_USER=${AUTHELIA_SMTP_USER} @@ -416,6 +422,7 @@ TZ=${AUTHELIA_TZ} AUTHELIA_ENV # ── docker-compose.yml (quoted heredoc: ${SMTP_USER} resolved by compose/.env) ── + backup_if_exists docker-compose.yml cat > docker-compose.yml << 'AUTHELIA_COMPOSE' name: authelia diff --git a/services/beszel.sh b/services/beszel.sh index 50b487e..0e9bd86 100644 --- a/services/beszel.sh +++ b/services/beszel.sh @@ -120,6 +120,11 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then local _companion; _companion="$(dirname "${BASH_SOURCE[0]}")/beszel.md" [ -f "$_companion" ] && cat "$_companion" >> "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } fi ACTUAL_USER="${ACTUAL_USER:-${SUDO_USER:-$USER}}" @@ -458,6 +463,7 @@ networks: # profile recognizes. security_opt: apparmor:unconfined below is # Beszel's own documented fix (beszel.dev/guide/systemd#apparmor-error) # — confirmed live, this exact error on a real box. + backup_if_exists docker-compose.yml cat > docker-compose.yml << BESZEL_COMPOSE name: beszel @@ -504,6 +510,7 @@ BESZEL_COMPOSE # README). Not threaded through automatically here because Caddy setup # (below) happens after this file is written, same ordering every # other service in this repo uses for its own Caddy prompt. + backup_if_exists .env cat > .env << BESZEL_ENV TZ=${SITE_TZ:-$(cat /etc/timezone 2>/dev/null || echo UTC)} CADDY_NET=$SITE_CADDY_NET @@ -678,6 +685,7 @@ install_beszel-agent() { # and there's no caddy_net to conditionally join since this box never # runs a web UI of its own. See that function's own comment for why the # systemd/dbus/sensor mounts below matter (Services/Temp columns). + backup_if_exists docker-compose.yml cat > docker-compose.yml << AGENT_COMPOSE name: beszel-agent @@ -703,6 +711,7 @@ services: - /sys/class/thermal:/sys/class/thermal:ro AGENT_COMPOSE + backup_if_exists .env cat > .env << AGENT_ENV TZ=${SITE_TZ:-$(cat /etc/timezone 2>/dev/null || echo UTC)} HUB_URL=$HUB_URL diff --git a/services/caddy.sh b/services/caddy.sh index d50c810..ae8eab4 100644 --- a/services/caddy.sh +++ b/services/caddy.sh @@ -192,6 +192,11 @@ CBLOCK mkdir -p "$_dir" cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } fi # Globals — ACTUAL_USER/ACTUAL_HOME must come before DOCKER_DIR @@ -279,6 +284,7 @@ install_caddy() { cd "$CADDY_DIR" || return 1 + backup_if_exists docker-compose.yml cat > docker-compose.yml << 'CADDY_COMPOSE' name: caddy diff --git a/services/calibre-web.sh b/services/calibre-web.sh index 3555698..7da6e6e 100644 --- a/services/calibre-web.sh +++ b/services/calibre-web.sh @@ -177,6 +177,11 @@ CBLOCK mkdir -p "$_dir" cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } fi ACTUAL_USER="${ACTUAL_USER:-${SUDO_USER:-$USER}}" @@ -242,6 +247,7 @@ networks: " fi + backup_if_exists docker-compose.yml cat > docker-compose.yml << CW_COMPOSE name: calibre-web @@ -264,6 +270,7 @@ services: ${_CADDY_NET_BLOCK}${_CADDY_NET_SECTION} CW_COMPOSE + backup_if_exists .env cat > .env << CW_ENV CADDY_NET=$SITE_CADDY_NET CW_ENV diff --git a/services/changedetection.sh b/services/changedetection.sh index 49189aa..627e4c7 100644 --- a/services/changedetection.sh +++ b/services/changedetection.sh @@ -177,6 +177,11 @@ CBLOCK mkdir -p "$_dir" cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } fi ACTUAL_USER="${ACTUAL_USER:-${SUDO_USER:-$USER}}" @@ -243,6 +248,7 @@ networks: " fi + backup_if_exists docker-compose.yml cat > docker-compose.yml << CD_COMPOSE name: changedetection @@ -272,6 +278,7 @@ ${_CADDY_NET_BLOCK} depends_on: ${_CADDY_NET_BLOCK}${_CADDY_NET_SECTION} CD_COMPOSE + backup_if_exists .env cat > .env << CD_ENV # Changedetection.io environment — edit before starting if needed BASE_URL=https://changes.${SITE_DOMAIN} diff --git a/services/ddclient.sh b/services/ddclient.sh index d86d490..bbe29fd 100644 --- a/services/ddclient.sh +++ b/services/ddclient.sh @@ -174,6 +174,11 @@ CBLOCK mkdir -p "$_dir" cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } fi # Globals — ACTUAL_USER/ACTUAL_HOME must come before DOCKER_DIR @@ -212,6 +217,7 @@ install_ddclient() { local TZ_VAL; TZ_VAL="${SITE_TZ:-$(cat /etc/timezone 2>/dev/null || echo UTC)}" + backup_if_exists docker-compose.yml cat > docker-compose.yml << 'DDCLIENT_COMPOSE' name: ddclient @@ -229,6 +235,7 @@ services: - ./config:/config DDCLIENT_COMPOSE + backup_if_exists .env cat > .env << DDCLIENT_ENV TZ=$TZ_VAL DDCLIENT_ENV diff --git a/services/drum-rhythm-game.sh b/services/drum-rhythm-game.sh index 5b12892..3f48142 100644 --- a/services/drum-rhythm-game.sh +++ b/services/drum-rhythm-game.sh @@ -178,6 +178,11 @@ CBLOCK mkdir -p "$_dir" cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } fi ACTUAL_USER="${ACTUAL_USER:-${SUDO_USER:-$USER}}" @@ -477,6 +482,7 @@ networks: " fi + backup_if_exists docker-compose.yml cat > docker-compose.yml << DRUM_COMPOSE name: drum-rhythm-game @@ -492,6 +498,7 @@ services: ${_CADDY_NET_BLOCK}${_CADDY_NET_SECTION} DRUM_COMPOSE + backup_if_exists .env cat > .env << DRUM_ENV CADDY_NET=${SITE_CADDY_NET} DRUM_ENV diff --git a/services/emby.sh b/services/emby.sh index 028f4a2..8363e65 100644 --- a/services/emby.sh +++ b/services/emby.sh @@ -189,6 +189,11 @@ CBLOCK mkdir -p "$_dir" cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } fi # Globals — ACTUAL_USER/ACTUAL_HOME must come before DOCKER_DIR @@ -352,6 +357,7 @@ networks: " fi + backup_if_exists docker-compose.yml cat > docker-compose.yml << EMBY_COMPOSE name: $CONTAINER @@ -377,6 +383,7 @@ services: ${_CADDY_NET_BLOCK}${_CADDY_NET_SECTION} EMBY_COMPOSE + backup_if_exists .env cat > .env << EMBY_ENV MEDIA_PATH=$MEDIA_PATH CADDY_NET=$SITE_CADDY_NET diff --git a/services/filebrowser.sh b/services/filebrowser.sh index 35f4663..7e86453 100644 --- a/services/filebrowser.sh +++ b/services/filebrowser.sh @@ -177,6 +177,11 @@ CBLOCK mkdir -p "$_dir" cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } fi ACTUAL_USER="${ACTUAL_USER:-${SUDO_USER:-$USER}}" @@ -315,6 +320,7 @@ networks: " fi + backup_if_exists docker-compose.yml cat > docker-compose.yml << FB_COMPOSE name: $CONTAINER @@ -334,6 +340,7 @@ services: ${_CADDY_NET_BLOCK}${_CADDY_NET_SECTION} FB_COMPOSE + backup_if_exists .env cat > .env << FB_ENV CADDY_NET=$SITE_CADDY_NET FB_ENV diff --git a/services/fmd.sh b/services/fmd.sh index a911bdb..664e1da 100644 --- a/services/fmd.sh +++ b/services/fmd.sh @@ -199,6 +199,11 @@ CBLOCK mkdir -p "$_dir" cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } fi # Globals — ACTUAL_USER/ACTUAL_HOME must come before DOCKER_DIR @@ -339,6 +344,7 @@ networks: " fi + backup_if_exists docker-compose.yml cat > docker-compose.yml << FMD_COMPOSE name: $CONTAINER @@ -357,6 +363,7 @@ services: ${_CADDY_NET_BLOCK}${_CADDY_NET_SECTION} FMD_COMPOSE + backup_if_exists .env cat > .env << FMD_ENV FMD_REGISTRATIONTOKEN=$FMD_TOKEN CADDY_NET=$SITE_CADDY_NET diff --git a/services/frigate-audio.sh b/services/frigate-audio.sh index cfa969f..e54e95b 100644 --- a/services/frigate-audio.sh +++ b/services/frigate-audio.sh @@ -207,6 +207,11 @@ CBLOCK mkdir -p "$_dir" cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } fi # Globals — ACTUAL_USER/ACTUAL_HOME must come before DOCKER_DIR @@ -365,6 +370,7 @@ install_frigate-audio() { # ── .env ────────────────────────────────────────────────────────────────── log_info "Writing .env..." + backup_if_exists "$DIR/.env" cat > "$DIR/.env" << ENVEOF # Frigate audio stack — generated by setup.sh # DO NOT commit this file — it contains credentials. @@ -432,6 +438,7 @@ networks: " fi + backup_if_exists "$DIR/docker-compose.yml" cat > "$DIR/docker-compose.yml" << 'COMPOSEEOF' # Frigate NVR + Mosquitto MQTT + frigate-notify # Generated by ubuntu-post-install setup.sh diff --git a/services/frigate-notify.sh b/services/frigate-notify.sh index 9aec65e..9ff6dff 100644 --- a/services/frigate-notify.sh +++ b/services/frigate-notify.sh @@ -174,6 +174,11 @@ CBLOCK mkdir -p "$_dir" cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } fi # Globals — ACTUAL_USER/ACTUAL_HOME must come before DOCKER_DIR @@ -234,6 +239,7 @@ networks: " fi + backup_if_exists docker-compose.yml cat > docker-compose.yml << FN_COMPOSE name: frigate-notify diff --git a/services/frigate.sh b/services/frigate.sh index c4763ca..464d52d 100644 --- a/services/frigate.sh +++ b/services/frigate.sh @@ -202,6 +202,11 @@ CBLOCK mkdir -p "$_dir" cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } fi # Globals — ACTUAL_USER/ACTUAL_HOME must come before DOCKER_DIR @@ -626,6 +631,7 @@ networks: " fi + backup_if_exists docker-compose.yml cat > docker-compose.yml << FRIGATE_COMPOSE name: frigate @@ -673,16 +679,71 @@ FRIGATE_COMPOSE # reverse_proxy block) stops that trust from being spoofed by a # request that reaches Frigate's published host port directly, # bypassing Caddy/Authelia entirely. + # Whether to even OFFER this can't just check "$DOCKER_DIR/authelia" + # locally — Frigate's own box very often has no local Caddy at all + # (configure_caddy_for_service falls back to writing a snippet for + # a remote Caddy machine to pick up, confirmed live: this is the + # normal shape for a dedicated NVR box), in which case Authelia, if + # it exists anywhere, lives on THAT remote Caddy machine instead — + # a box this script has no filesystem access to inspect. Default to + # "y" only when local Authelia is actually confirmed; otherwise still + # offer it (default "n") and sort out local-vs-remote below once the + # admin says yes, rather than silently refusing to ask at all. local FRIGATE_USE_AUTHELIA="n" FRIGATE_PROXY_SECRET="" AUTH_CONFIG_BLOCK="" - if [ -d "$DOCKER_DIR/authelia" ]; then - echo "" - prompt_yn "Protect Frigate with Authelia SSO (disables Frigate's own login)? (y/n):" "y" FRIGATE_USE_AUTHELIA - fi + local _frigate_local_authelia="n" + [ -d "$DOCKER_DIR/authelia" ] && _frigate_local_authelia="y" + echo "" + prompt_yn "Protect Frigate with Authelia SSO (disables Frigate's own login)? (y/n):" "$_frigate_local_authelia" FRIGATE_USE_AUTHELIA if [[ "$FRIGATE_USE_AUTHELIA" =~ ^[Yy]$ ]]; then FRIGATE_PROXY_SECRET="${ENV_MAP[FRIGATE_PROXY_AUTH_SECRET]:-$(generate_password 32)}" + + # Local Authelia snippet (import authelia) only actually exists + # in the Caddyfile it's imported into if Authelia is on THAT + # same machine. When it's on this box, that's this box's own + # Caddy — safe to assume. When Caddy itself turns out to be + # remote (below), "local" instead means "local to wherever + # Caddy is", which this script can't see — so ask, rather than + # silently emit an import that would fail Caddy's own reload + # with "file to import not found" on that other machine. + local _frigate_auth_block=" import authelia" + if [ "$_frigate_local_authelia" != "y" ]; then + echo "" + log_info "No local Authelia on this box — Caddy for Frigate may end up on a" + log_info "different machine (decided next)." + local _authelia_with_caddy="" + prompt_yn " Does Authelia run on that SAME machine as Caddy? (y/n):" "y" _authelia_with_caddy + if [[ ! "$_authelia_with_caddy" =~ ^[Yy]$ ]]; then + # Genuinely cross-machine: Authelia is a third box, + # different from both this one and wherever Caddy ends + # up. Needs the explicit header-pinned forward_auth form + # — see CLAUDE.md's "forward_auth to a remote Authelia" + # note for why the bare "authelia:9091" shortcut can't + # be used here and X-Forwarded-Host must be pinned + # explicitly (a second Caddy hop otherwise silently + # evaluates every domain as if it were auth's own + # portal domain — confirmed live, a real incident this + # exact snippet shape was written to prevent). + local _remote_authelia_domain="" + prompt_text " Authelia's own portal domain (e.g. authelia.example.com):" "" _remote_authelia_domain + if [ -n "$_remote_authelia_domain" ]; then + _frigate_auth_block=" forward_auth https://${_remote_authelia_domain} { + uri /api/authz/forward-auth + copy_headers Remote-User Remote-Groups Remote-Name Remote-Email + header_up X-Forwarded-Method {method} + header_up X-Forwarded-Proto {scheme} + header_up X-Forwarded-Host {host} + header_up X-Forwarded-Uri {uri} + }" + else + log_warning "No domain entered — falling back to 'import authelia', which will fail" + log_warning "Caddy's reload unless Authelia is actually local to that Caddy machine." + fi + fi + fi + configure_caddy_for_service "Frigate" "frigate:5000" "frigate" \ - " import authelia" \ + "$_frigate_auth_block" \ " header_up X-Proxy-Secret ${FRIGATE_PROXY_SECRET}" if [ "${CADDY_SERVICE_CONFIGURED:-false}" = true ]; then AUTH_CONFIG_BLOCK="auth: @@ -780,6 +841,7 @@ snapshots: FRIGATE_CONFIG fi + backup_if_exists .env cat > .env << FRIGATE_ENV FRIGATE_MEDIA=$FRIGATE_MEDIA CADDY_NET=$SITE_CADDY_NET diff --git a/services/garage-webui.sh b/services/garage-webui.sh index 8079ee0..bd33646 100644 --- a/services/garage-webui.sh +++ b/services/garage-webui.sh @@ -92,6 +92,11 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then mkdir -p "$_dir" cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } fi ACTUAL_USER="${ACTUAL_USER:-${SUDO_USER:-$USER}}" @@ -199,6 +204,7 @@ install_garage-webui() { ensure_docker_dir_ownership "$DIR" cd "$DIR" || return 1 + backup_if_exists docker-compose.yml cat > docker-compose.yml << COMPOSE name: garage-webui @@ -219,6 +225,7 @@ services: - "${WEB_PORT}:3909" COMPOSE + backup_if_exists .env cat > .env << ENV TZ=${SITE_TZ:-$(cat /etc/timezone 2>/dev/null || echo UTC)} diff --git a/services/garage.sh b/services/garage.sh index 6fb7e88..0d9f6e5 100644 --- a/services/garage.sh +++ b/services/garage.sh @@ -108,6 +108,11 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then mkdir -p "$_dir" cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } fi # Globals — ACTUAL_USER/ACTUAL_HOME must come before DOCKER_DIR @@ -277,6 +282,7 @@ api_bind_addr = "[::]:${ADMIN_PORT}" admin_token = "${ADMIN_TOKEN}" TOML + backup_if_exists docker-compose.yml cat > docker-compose.yml << COMPOSE name: garage @@ -297,6 +303,7 @@ services: - "${ADMIN_PORT}:${ADMIN_PORT}" COMPOSE + backup_if_exists .env cat > .env << ENV TZ=${SITE_TZ:-$(cat /etc/timezone 2>/dev/null || echo UTC)} diff --git a/services/gatus.sh b/services/gatus.sh index 0d10068..221c5e5 100644 --- a/services/gatus.sh +++ b/services/gatus.sh @@ -187,6 +187,11 @@ CBLOCK mkdir -p "$_dir" cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } fi # Globals — ACTUAL_USER/ACTUAL_HOME must come before DOCKER_DIR @@ -405,6 +410,7 @@ networks: " fi + backup_if_exists docker-compose.yml cat > docker-compose.yml << GATUS_COMPOSE name: gatus @@ -423,6 +429,7 @@ services: ${_CADDY_NET_BLOCK}${_CADDY_NET_SECTION} GATUS_COMPOSE + backup_if_exists .env cat > .env << GATUS_ENV TZ=$TZ_VAL CADDY_NET=$SITE_CADDY_NET diff --git a/services/gitea.sh b/services/gitea.sh index 57bd147..c89ad11 100644 --- a/services/gitea.sh +++ b/services/gitea.sh @@ -104,6 +104,11 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then mkdir -p "$_dir" cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } fi ACTUAL_USER="${ACTUAL_USER:-${SUDO_USER:-$USER}}" @@ -585,6 +590,7 @@ install_gitea() { [[ "$WEB_PORT" != 3001 ]] && log_info "Port 3001 was taken — Gitea's web UI will use ${WEB_PORT}." [[ "$SSH_PORT" != 2222 ]] && log_info "Port 2222 was taken — Gitea's SSH clone port will use ${SSH_PORT}." + backup_if_exists docker-compose.yml cat > docker-compose.yml << EOF name: gitea services: @@ -699,6 +705,7 @@ EOF cp -f "$SYNC_SRC" "$DIR/gitea-github-sync.sh" chmod +x "$DIR/gitea-github-sync.sh" + backup_if_exists "$DIR/.env" cat > "$DIR/.env" << ENV # Written by services/gitea.sh — re-run that (update mode) to change any of this. GITEA_URL='http://localhost:${WEB_PORT}' diff --git a/services/homeassistant.sh b/services/homeassistant.sh index 4c454c2..95c919d 100644 --- a/services/homeassistant.sh +++ b/services/homeassistant.sh @@ -274,6 +274,7 @@ networks: " fi + backup_if_exists docker-compose.yml cat > docker-compose.yml << HOMEASSISTANT_COMPOSE name: homeassistant diff --git a/services/homebox.sh b/services/homebox.sh index 3fe95c1..ca1204d 100644 --- a/services/homebox.sh +++ b/services/homebox.sh @@ -177,6 +177,11 @@ CBLOCK mkdir -p "$_dir" cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } fi ACTUAL_USER="${ACTUAL_USER:-${SUDO_USER:-$USER}}" @@ -196,6 +201,124 @@ fi register_service homebox utilities "Home inventory and asset management (Homebox)" 7745 +# Offers to wire Homebox's own native OIDC support to Authelia — real +# server-side automation via env vars, not paste-in instructions, same +# shape as Mealie/ActualBudget. Confirmed against homebox.software's own +# OIDC docs and authelia.com's Homebox integration page directly: PKCE is +# required, redirect path is /api/v1/users/login/oidc/callback, and the +# issuer URL is sensitive to a trailing slash (a real reported bug) — the +# portal URL this repo already stores never has one, so left as-is here. +# +# Args: DIR CONTAINER +_homebox_offer_authelia_oidc() { + local DIR="$1" CONTAINER="$2" + + declare -F _authelia_provision_oidc_client >/dev/null 2>&1 || return 0 + [ -d "$DOCKER_DIR/authelia" ] || return 0 + + echo "" + local USE_SSO="" + prompt_yn " Add \"Sign in with Authelia\" (OpenID Connect) to Homebox? (y/n):" "n" USE_SSO + [[ "$USE_SSO" =~ ^[Yy]$ ]] || return 0 + + if grep -q '^HBOX_OIDC_ENABLED=' "$DIR/.env" 2>/dev/null; then + echo "" + log_info "Authelia SSO is already configured for Homebox (HBOX_OIDC_* already set in $DIR/.env)." + local RECONFIGURE="" + prompt_yn " Reconfigure it (registers a fresh Authelia client + secret)? (y/n):" "n" RECONFIGURE + [[ "$RECONFIGURE" =~ ^[Yy]$ ]] || return 0 + sed -i '/^HBOX_OIDC_/d; /^HBOX_OPTIONS_TRUST_PROXY=/d' "$DIR/.env" + fi + + # Existing installs from before this offer existed won't have env_file + # picked up their .env's OIDC additions otherwise — this repo's own + # compose template gained it above; a pre-existing compose file needs + # the same one-line patch to actually load what's about to be written. + if ! grep -q '^\s*env_file: \.env\s*$' "$DIR/docker-compose.yml" 2>/dev/null; then + sed -i "/^ hostname: /a\\ env_file: .env" "$DIR/docker-compose.yml" + fi + + local APP_DOMAIN + APP_DOMAIN="$(_authelia_pick_domain "Domain Homebox is reachable at (number or domain)")" + if [ -z "$APP_DOMAIN" ]; then + log_warning "No domain entered — skipping SSO setup." + return 0 + fi + + local _2fa="" AUTH_POLICY="two_factor" + prompt_yn " Require two-factor for Homebox logins via Authelia too? (y/n):" "y" _2fa + [[ "$_2fa" =~ ^[Yy]$ ]] || AUTH_POLICY="one_factor" + + if ! _authelia_provision_oidc_client "Homebox" "homebox" "$AUTH_POLICY" "y" "y" \ + "https://${APP_DOMAIN}/api/v1/users/login/oidc/callback"; then + log_warning "Couldn't register Homebox as an OIDC client in Authelia — skipping SSO setup." + return 0 + fi + + cat >> "$DIR/.env" << ENV + +# Written by services/homebox.sh's Authelia SSO step — adds "Sign in with +# Authelia" alongside local login; local accounts keep working unchanged. +HBOX_OIDC_ENABLED=true +HBOX_OIDC_ISSUER_URL=${OIDC_AUTHELIA_PORTAL_URL} +HBOX_OIDC_CLIENT_ID=homebox +HBOX_OIDC_CLIENT_SECRET=${OIDC_CLIENT_SECRET_PLAIN} +HBOX_OIDC_SCOPE=openid profile email groups +HBOX_OPTIONS_TRUST_PROXY=true +ENV + chown "$ACTUAL_USER:$ACTUAL_USER" "$DIR/.env" 2>/dev/null || true + + (cd "$DIR" && docker compose up -d) \ + && log_success "\"Sign in with Authelia\" added to Homebox — local login still works too." \ + || log_warning "Restart failed — check: docker compose -f $DIR/docker-compose.yml logs" + + declare -F _authelia_scope_access >/dev/null 2>&1 && _authelia_scope_access "homebox" "$APP_DOMAIN" + + echo "" + log_info "Test the \"Login with Authelia\" button on Homebox's own login page before" + log_info "disabling local login — re-run 'sudo ./setup.sh homebox' (choose update) once" + log_info "you've confirmed it works, and you'll be offered that as a separate step." +} + +# Split out from _homebox_offer_authelia_oidc so disabling local login is +# never offered in the same breath as first setting SSO up — same +# reasoning as Mealie/Beszel's equivalent split (confirmed live on Beszel: +# saying yes before actually testing the button leaves both login paths +# broken at once). Only reached from a later "update" rerun once OIDC is +# already configured and the admin declines to reconfigure. +_homebox_offer_disable_local_login() { + local DIR="$1" + grep -q '^HBOX_OPTIONS_ALLOW_LOCAL_LOGIN=false' "$DIR/.env" 2>/dev/null && return 0 + + echo "" + local _tested="" + prompt_yn " Have you ALREADY logged into Homebox successfully using the Authelia button (not just enabled it)? (y/n):" "n" _tested + if [[ ! "$_tested" =~ ^[Yy]$ ]]; then + log_info "Skipped. Test the Authelia login button first, then re-run 'sudo ./setup.sh homebox' (choose update) to come back to this." + return 0 + fi + + local _disable_local="" + prompt_yn " Also disable Homebox's own local login, so Authelia is the only way in? (y/n):" "n" _disable_local + [[ "$_disable_local" =~ ^[Yy]$ ]] || return 0 + + log_warning "Anyone without an Authelia account (only a local Homebox one) will no longer be able to log in." + log_info "Reversible any time: set HBOX_OPTIONS_ALLOW_LOCAL_LOGIN back to true in $DIR/.env and 'docker compose up -d'." + local _auto_redirect="" + prompt_yn " Skip Homebox's login page entirely and jump straight to Authelia? (y/n):" "y" _auto_redirect + + sed -i '/^HBOX_OPTIONS_ALLOW_LOCAL_LOGIN=/d; /^HBOX_OIDC_AUTO_REDIRECT=/d' "$DIR/.env" + { + echo "HBOX_OPTIONS_ALLOW_LOCAL_LOGIN=false" + [[ "$_auto_redirect" =~ ^[Yy]$ ]] && echo "HBOX_OIDC_AUTO_REDIRECT=true" + } >> "$DIR/.env" + chown "$ACTUAL_USER:$ACTUAL_USER" "$DIR/.env" 2>/dev/null || true + + (cd "$DIR" && docker compose up -d) \ + && log_success "Local login is now disabled — Authelia is the only way into Homebox." \ + || log_warning "Restart failed — check: docker compose -f $DIR/docker-compose.yml logs" +} + install_homebox() { require_docker || return 1 log_info "Installing Homebox..." @@ -256,6 +379,8 @@ install_homebox() { ( cd "$HB_DIR" && docker compose pull && docker compose up -d ) \ && log_success "Homebox image refreshed" \ || log_warning "Refresh failed — check: docker compose -f $HB_DIR/docker-compose.yml logs" + _homebox_offer_authelia_oidc "$HB_DIR" "$CONTAINER" + _homebox_offer_disable_local_login "$HB_DIR" return 0 ;; cancel) @@ -311,6 +436,7 @@ networks: local HB_PEPPER HB_PEPPER="$(generate_password 48)" + backup_if_exists docker-compose.yml cat > docker-compose.yml << HB_COMPOSE name: $CONTAINER @@ -320,6 +446,7 @@ services: container_name: $CONTAINER hostname: $CONTAINER restart: unless-stopped + env_file: .env environment: - HBOX_LOG_LEVEL=info - HBOX_WEB_MAX_UPLOAD_SIZE=10 @@ -331,6 +458,7 @@ services: ${_CADDY_NET_BLOCK}${_CADDY_NET_SECTION} HB_COMPOSE + backup_if_exists .env cat > .env << HB_ENV CADDY_NET=$SITE_CADDY_NET @@ -346,6 +474,8 @@ HB_ENV configure_caddy_for_service "Homebox${INSTANCE_SUFFIX:+ ($INSTANCE_SUFFIX)}" "${CONTAINER}:7745" "homebox${INSTANCE_SUFFIX:+-$INSTANCE_SUFFIX}" + _homebox_offer_authelia_oidc "$HB_DIR" "$CONTAINER" + write_readme "$HB_DIR" << MD # Homebox${INSTANCE_SUFFIX:+ — $INSTANCE_SUFFIX} diff --git a/services/immich.sh b/services/immich.sh index a31943a..c6140af 100644 --- a/services/immich.sh +++ b/services/immich.sh @@ -183,6 +183,11 @@ CBLOCK mkdir -p "$_dir" cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } fi ACTUAL_USER="${ACTUAL_USER:-${SUDO_USER:-$USER}}" @@ -606,6 +611,7 @@ networks: [ -n "$EXTERNAL_LIBRARY" ] && _EXTERNAL_VOLUME_LINE=" - \${EXTERNAL_LIBRARY}:/usr/src/app/external:ro " + backup_if_exists docker-compose.yml cat > docker-compose.yml << IMMICH_COMPOSE name: $PROJECT @@ -686,6 +692,7 @@ S3_SECRET_ACCESS_KEY=$S3_SECRET_ACCESS_KEY fi if [ "$IMMICH_STRATEGY" = "2" ]; then + backup_if_exists .env cat > .env << IMMICH_ENV # IMMICH CONFIGURATION — External Library Mode # @@ -713,6 +720,7 @@ TZ=$TZ_VAL CADDY_NET=$SITE_CADDY_NET IMMICH_ENV else + backup_if_exists .env cat > .env << IMMICH_ENV # IMMICH CONFIGURATION — Unified Library # diff --git a/services/iopaint.sh b/services/iopaint.sh index 9a458bf..24cbc52 100644 --- a/services/iopaint.sh +++ b/services/iopaint.sh @@ -185,6 +185,11 @@ CBLOCK [[ "${DRY_RUN:-false}" == "true" ]] && return 0 cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } fi ACTUAL_USER="${ACTUAL_USER:-${SUDO_USER:-$USER}}" @@ -314,6 +319,7 @@ networks: fi if [[ "$USE_GPU" =~ ^[Yy]$ ]]; then + backup_if_exists docker-compose.yml cat > docker-compose.yml << IOPAINT_GPU name: iopaint @@ -346,6 +352,7 @@ services: ${_CADDY_NET_BLOCK}${_CADDY_NET_SECTION} IOPAINT_GPU else + backup_if_exists docker-compose.yml cat > docker-compose.yml << IOPAINT_CPU name: iopaint @@ -373,6 +380,7 @@ IOPAINT_CPU fi # ── .env ───────────────────────────────────────────────────────────────── + backup_if_exists .env cat > .env << IOPAINT_ENV # IOPaint — change MODEL and restart to switch (no need to edit docker-compose.yml) diff --git a/services/jellyfin.sh b/services/jellyfin.sh index 7cea4f7..557e8b2 100644 --- a/services/jellyfin.sh +++ b/services/jellyfin.sh @@ -182,6 +182,11 @@ CBLOCK mkdir -p "$_dir" cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } fi ACTUAL_USER="${ACTUAL_USER:-${SUDO_USER:-$USER}}" @@ -347,6 +352,7 @@ networks: ' fi + backup_if_exists docker-compose.yml cat > docker-compose.yml << JELLYFIN_COMPOSE name: $CONTAINER @@ -368,6 +374,7 @@ $HWACCEL_BLOCK ${_DISCOVERY_PORTS}${_CADDY_NET_BLOCK}${_CADDY_NET_SECTION} JELLYFIN_COMPOSE + backup_if_exists .env cat > .env << JELLYFIN_ENV MEDIA_PATH=$MEDIA_PATH CADDY_NET=$SITE_CADDY_NET diff --git a/services/joplin.sh b/services/joplin.sh index df63b11..c3d554e 100644 --- a/services/joplin.sh +++ b/services/joplin.sh @@ -177,6 +177,11 @@ CBLOCK mkdir -p "$_dir" cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } fi ACTUAL_USER="${ACTUAL_USER:-${SUDO_USER:-$USER}}" @@ -314,6 +319,7 @@ networks: " fi + backup_if_exists docker-compose.yml cat > docker-compose.yml << JOPLIN_COMPOSE name: $CONTAINER @@ -340,6 +346,7 @@ ${_CADDY_NET_BLOCK} ${_CADDY_NET_BLOCK}${_CADDY_NET_SECTION} JOPLIN_COMPOSE + backup_if_exists .env cat > .env << JOPLIN_ENV # Joplin Server configuration APP_PORT=22300 diff --git a/services/js99er.sh b/services/js99er.sh index ccfbad9..0b7bbc6 100644 --- a/services/js99er.sh +++ b/services/js99er.sh @@ -174,6 +174,11 @@ CBLOCK mkdir -p "$_dir" cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } fi # Globals — ACTUAL_USER/ACTUAL_HOME must come before DOCKER_DIR @@ -459,6 +464,7 @@ networks: " fi + backup_if_exists "$JS99ER_DIR/docker-compose.yml" cat > "$JS99ER_DIR/docker-compose.yml" << COMPOSE name: js99er diff --git a/services/koha.sh b/services/koha.sh index 591926c..5fd2741 100644 --- a/services/koha.sh +++ b/services/koha.sh @@ -188,6 +188,11 @@ CBLOCK mkdir -p "$_dir" cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } fi ACTUAL_USER="${ACTUAL_USER:-${SUDO_USER:-$USER}}" @@ -410,6 +415,7 @@ install_koha() { fi # ── docker-compose.yml ──────────────────────────────────────────────────── + backup_if_exists docker-compose.yml cat > docker-compose.yml << KOHA_COMPOSE name: koha @@ -481,6 +487,7 @@ ${_CADDY_NET_SECTION} KOHA_COMPOSE # ── config-main.env ─────────────────────────────────────────────────────── + backup_if_exists config-main.env cat > config-main.env << KOHA_ENV # Koha ILS configuration — generated at install time MYSQL_SERVER=koha-db diff --git a/services/kyber-server.sh b/services/kyber-server.sh index 5efec20..d086489 100644 --- a/services/kyber-server.sh +++ b/services/kyber-server.sh @@ -388,6 +388,7 @@ for a in data.get('assets', []): mkdir -p "$DIR" ensure_docker_dir_ownership "$DIR" + backup_if_exists "$DIR/docker-compose.yml" cat > "$DIR/docker-compose.yml" << EOF name: kyber-server services: @@ -411,6 +412,7 @@ EOF # Write .env with restricted permissions # Values are single-quoted so special characters ($, !, &, etc.) are safe. # Exception: single quotes inside a value would still break — avoid them. + backup_if_exists "$DIR/.env" cat > "$DIR/.env" << EOF MAXIMA_CREDENTIALS='${EA_EMAIL}:${EA_PASSWORD}' KYBER_TOKEN='${KYBER_TOKEN}' @@ -558,6 +560,11 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then mkdir -p "$_dir" cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } ACTUAL_USER="${SUDO_USER:-$USER}" ACTUAL_HOME=$(eval echo "~$ACTUAL_USER") diff --git a/services/lyrion.sh b/services/lyrion.sh index 5b41823..814750d 100644 --- a/services/lyrion.sh +++ b/services/lyrion.sh @@ -188,6 +188,11 @@ CBLOCK mkdir -p "$_dir" cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } fi # Globals — ACTUAL_USER/ACTUAL_HOME must come before DOCKER_DIR @@ -353,6 +358,7 @@ install_lyrion() { _HTTP_PORT_INTERNAL="9000" fi + backup_if_exists docker-compose.yml cat > docker-compose.yml << LYRION_COMPOSE name: $CONTAINER @@ -375,6 +381,7 @@ ${_NETWORK_BLOCK}${_PORTS_BLOCK} environment: LYRION_COMPOSE + backup_if_exists .env cat > .env << LYRION_ENV MUSIC_PATH=$MUSIC_PATH CADDY_NET=$SITE_CADDY_NET diff --git a/services/magicmirror.sh b/services/magicmirror.sh index 03150c4..15f0c70 100644 --- a/services/magicmirror.sh +++ b/services/magicmirror.sh @@ -174,6 +174,11 @@ CBLOCK mkdir -p "$_dir" cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } fi # Globals — ACTUAL_USER/ACTUAL_HOME must come before DOCKER_DIR @@ -254,6 +259,7 @@ networks: " fi + backup_if_exists docker-compose.yml cat > docker-compose.yml << MM_COMPOSE name: mm-$MM_PORT diff --git a/services/mail-archiver.sh b/services/mail-archiver.sh index 1101ec5..de00975 100644 --- a/services/mail-archiver.sh +++ b/services/mail-archiver.sh @@ -170,6 +170,11 @@ CBLOCK mkdir -p "$_dir" cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } fi # Globals — ACTUAL_USER/ACTUAL_HOME must come before DOCKER_DIR @@ -244,6 +249,7 @@ networks: " fi + backup_if_exists docker-compose.yml cat > docker-compose.yml << MA_COMPOSE name: mail-archiver @@ -279,6 +285,7 @@ ${_CADDY_NET_BLOCK} ${_CADDY_NET_SECTION} MA_COMPOSE + backup_if_exists .env cat > .env << MA_ENV # ── General ─────────────────────────────────────────────────────────────────── TZ=$TZ_VAL diff --git a/services/mattermost.sh b/services/mattermost.sh index d49edc7..74fe57f 100644 --- a/services/mattermost.sh +++ b/services/mattermost.sh @@ -213,6 +213,11 @@ CBLOCK [[ "${DRY_RUN:-false}" == "true" ]] && return 0 cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } generate_password() { local _len="${1:-32}" @@ -518,6 +523,7 @@ networks: " fi + backup_if_exists docker-compose.yml cat > docker-compose.yml << EOF name: ${PROJECT} @@ -580,6 +586,7 @@ ${_CADDY_NET_BLOCK} healthcheck: ${_CADDY_NET_BLOCK}${_COTURN_SERVICE}${_CADDY_NET_SECTION} EOF + backup_if_exists .env cat > .env << EOF TZ=$TZ_VAL CADDY_NET=$SITE_CADDY_NET diff --git a/services/mealie.sh b/services/mealie.sh index d64a91a..a4c7d59 100644 --- a/services/mealie.sh +++ b/services/mealie.sh @@ -180,6 +180,11 @@ CBLOCK mkdir -p "$_dir" cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } fi ACTUAL_USER="${ACTUAL_USER:-${SUDO_USER:-$USER}}" @@ -459,6 +464,7 @@ networks: " fi + backup_if_exists docker-compose.yml cat > docker-compose.yml << MEALIE_COMPOSE name: $CONTAINER @@ -483,6 +489,7 @@ services: ${_CADDY_NET_BLOCK}${_CADDY_NET_SECTION} MEALIE_COMPOSE + backup_if_exists .env cat > .env << MEALIE_ENV # Public URL Mealie is served on — used for email links and OAuth redirects. # Update if you change your domain or switch from HTTP to HTTPS. diff --git a/services/meshcentral.sh b/services/meshcentral.sh index 945f1f3..081a20d 100644 --- a/services/meshcentral.sh +++ b/services/meshcentral.sh @@ -188,6 +188,11 @@ CBLOCK mkdir -p "$_dir" cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } fi # Globals — ACTUAL_USER/ACTUAL_HOME must come before DOCKER_DIR @@ -323,6 +328,7 @@ networks: " fi + backup_if_exists docker-compose.yml cat > docker-compose.yml << MC_COMPOSE name: $CONTAINER @@ -350,6 +356,7 @@ services: ${_CADDY_NET_BLOCK}${_CADDY_NET_SECTION} MC_COMPOSE + backup_if_exists .env cat > .env << MC_ENV MC_HOSTNAME=$MC_HOSTNAME MC_REVERSE_PROXY=false diff --git a/services/minecraft.sh b/services/minecraft.sh index 2ab2f1f..0f9cd81 100644 --- a/services/minecraft.sh +++ b/services/minecraft.sh @@ -1519,6 +1519,7 @@ PREGENINFOEOF - ./config:/data/config" fi + backup_if_exists "$MC_DIR/docker-compose.yml" cat > "$MC_DIR/docker-compose.yml" << COMPOSEEOF name: ${MC_NAME} @@ -1902,6 +1903,7 @@ EXPOSE 80 CLIENTDOCKEREOF # Standalone compose for the client-mods page (its own folder). + backup_if_exists "$CLIENT_MODS_DIR/docker-compose.yml" cat > "$CLIENT_MODS_DIR/docker-compose.yml" << CMCOMPOSEEOF name: ${CM_NAME} diff --git a/services/n8n.sh b/services/n8n.sh index adc8c18..685d9fc 100644 --- a/services/n8n.sh +++ b/services/n8n.sh @@ -177,6 +177,11 @@ CBLOCK mkdir -p "$_dir" cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } fi ACTUAL_USER="${ACTUAL_USER:-${SUDO_USER:-$USER}}" @@ -242,6 +247,7 @@ networks: " fi + backup_if_exists docker-compose.yml cat > docker-compose.yml << N8N_COMPOSE name: n8n @@ -266,6 +272,7 @@ services: ${_CADDY_NET_BLOCK}${_CADDY_NET_SECTION} N8N_COMPOSE + backup_if_exists .env cat > .env << N8N_ENV # n8n environment — edit before starting if needed N8N_HOST=n8n diff --git a/services/nextcloud.sh b/services/nextcloud.sh index 0620a96..11fed1b 100644 --- a/services/nextcloud.sh +++ b/services/nextcloud.sh @@ -70,6 +70,11 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then mkdir -p "$_dir" cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } # Match common.sh's eval-based pattern so local vars in install_* are set correctly prompt_text() { @@ -260,6 +265,7 @@ networks: fi # ── docker-compose.yml ────────────────────────────────────────────────── + backup_if_exists docker-compose.yml cat > docker-compose.yml << NCCOMPOSE name: nextcloud services: @@ -290,6 +296,7 @@ ${_CADDY_NET_BLOCK}${_CADDY_NET_SECTION} NCCOMPOSE # ── .env ──────────────────────────────────────────────────────────────── + backup_if_exists .env cat > .env << NCENV TZ=$TZ_VAL CADDY_NET=$SITE_CADDY_NET diff --git a/services/ntfy.sh b/services/ntfy.sh index 8a95ba8..16558fd 100644 --- a/services/ntfy.sh +++ b/services/ntfy.sh @@ -184,6 +184,11 @@ CBLOCK mkdir -p "$_dir" cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } fi # Globals — ACTUAL_USER/ACTUAL_HOME must come before DOCKER_DIR @@ -309,6 +314,7 @@ networks: " fi + backup_if_exists docker-compose.yml cat > docker-compose.yml << NTFY_COMPOSE name: $CONTAINER @@ -329,6 +335,7 @@ services: ${_CADDY_NET_BLOCK}${_CADDY_NET_SECTION} NTFY_COMPOSE + backup_if_exists .env cat > .env << NTFY_ENV TZ=${SITE_TZ:-$(cat /etc/timezone 2>/dev/null || echo UTC)} CADDY_NET=$SITE_CADDY_NET diff --git a/services/onlyoffice.sh b/services/onlyoffice.sh index bed75bd..5c48343 100644 --- a/services/onlyoffice.sh +++ b/services/onlyoffice.sh @@ -70,6 +70,11 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then mkdir -p "$_dir" cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } # Match common.sh's eval-based pattern so local vars in install_* are set correctly prompt_text() { @@ -280,6 +285,7 @@ networks: fi # ── docker-compose.yml ────────────────────────────────────────────────── + backup_if_exists docker-compose.yml cat > docker-compose.yml << OOCOMPOSE name: onlyoffice services: @@ -299,6 +305,7 @@ ${_CADDY_NET_BLOCK}${_CADDY_NET_SECTION} OOCOMPOSE # ── .env ──────────────────────────────────────────────────────────────── + backup_if_exists .env cat > .env << OOENV CADDY_NET=$SITE_CADDY_NET # JWT authentication — keep JWT_SECRET private diff --git a/services/pihole.sh b/services/pihole.sh index 3673363..46cd28f 100644 --- a/services/pihole.sh +++ b/services/pihole.sh @@ -210,6 +210,11 @@ CBLOCK mkdir -p "$_dir" cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } fi # Globals — ACTUAL_USER/ACTUAL_HOME must come before DOCKER_DIR @@ -325,6 +330,7 @@ networks: # example — NET_ADMIN specifically is only needed if this instance is # ever used as a DHCP server too, which it isn't here, but the other two # (SYS_TIME, SYS_NICE) are part of that same documented baseline. + backup_if_exists docker-compose.yml cat > docker-compose.yml << PIHOLE_COMPOSE name: pihole @@ -351,6 +357,7 @@ services: ${_CADDY_NET_BLOCK}${_CADDY_NET_SECTION} PIHOLE_COMPOSE + backup_if_exists .env cat > .env << PIHOLE_ENV TZ=${SITE_TZ:-$(cat /etc/timezone 2>/dev/null || echo UTC)} # Admin web UI password (System Console / login screen). diff --git a/services/portainer.sh b/services/portainer.sh index 0920f6e..299c75f 100644 --- a/services/portainer.sh +++ b/services/portainer.sh @@ -177,6 +177,11 @@ CBLOCK mkdir -p "$_dir" cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } fi ACTUAL_USER="${ACTUAL_USER:-${SUDO_USER:-$USER}}" @@ -242,6 +247,7 @@ networks: " fi + backup_if_exists docker-compose.yml cat > docker-compose.yml << PORTAINER_COMPOSE name: portainer diff --git a/services/pstn-trunk.sh b/services/pstn-trunk.sh index 15ab16c..2bdd0fe 100644 --- a/services/pstn-trunk.sh +++ b/services/pstn-trunk.sh @@ -1746,6 +1746,7 @@ _pstn_apply_settings() { # Direct") — unquoted, bash's `source` would treat the second word of # any such value as a command to run ("Direct: command not found"), # confirmed live while testing the multi-IP change. + backup_if_exists "$EA_DIR/.pstn-trunk.env" cat > "$EA_DIR/.pstn-trunk.env" << ENV PROVIDER_NAME="${PROVIDER_NAME}" TRUNK_SERVER="${SERVER}" diff --git a/services/rustdesk.sh b/services/rustdesk.sh index 6915e46..f20b0c0 100644 --- a/services/rustdesk.sh +++ b/services/rustdesk.sh @@ -91,6 +91,11 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then mkdir -p "$_dir" cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } fi # Globals — ACTUAL_USER/ACTUAL_HOME must come before DOCKER_DIR @@ -230,6 +235,7 @@ install_rustdesk() { prompt_yn "Require encrypted connections only? (recommended) (y/n):" "y" _enc [ "$_enc" = "n" ] || [ "$_enc" = "N" ] && ENCRYPTED_ONLY="0" + backup_if_exists docker-compose.yml cat > docker-compose.yml << RD_COMPOSE name: $CONTAINER @@ -251,6 +257,7 @@ services: - ./rustdesk_data:/data RD_COMPOSE + backup_if_exists .env cat > .env << RD_ENV # ── General ─────────────────────────────────────────────────────────────────── TZ=$TZ_VAL diff --git a/services/silent-send.sh b/services/silent-send.sh index fb127a1..437626d 100644 --- a/services/silent-send.sh +++ b/services/silent-send.sh @@ -60,6 +60,11 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then mkdir -p "$_dir" cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } fi # Globals — ACTUAL_USER/ACTUAL_HOME must come before DOCKER_DIR @@ -176,6 +181,7 @@ EOF prompt_text " WEB_EXT_API_KEY (e.g. user:12345678:901), blank to skip:" "" FF_KEY prompt_text " WEB_EXT_API_SECRET, blank to skip:" "" FF_SECRET if [ -n "$FF_KEY" ] && [ -n "$FF_SECRET" ]; then + backup_if_exists "$SS_DIR/.env" cat > "$SS_DIR/.env" << ENVEOF WEB_EXT_API_KEY="$FF_KEY" WEB_EXT_API_SECRET="$FF_SECRET" diff --git a/services/stirling-pdf.sh b/services/stirling-pdf.sh index c7420b7..c82ebbf 100644 --- a/services/stirling-pdf.sh +++ b/services/stirling-pdf.sh @@ -177,6 +177,11 @@ CBLOCK mkdir -p "$_dir" cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } fi ACTUAL_USER="${ACTUAL_USER:-${SUDO_USER:-$USER}}" @@ -242,6 +247,7 @@ networks: " fi + backup_if_exists docker-compose.yml cat > docker-compose.yml << PDF_COMPOSE name: stirling-pdf @@ -261,6 +267,7 @@ services: ${_CADDY_NET_BLOCK}${_CADDY_NET_SECTION} PDF_COMPOSE + backup_if_exists .env cat > .env << PDF_ENV # Stirling PDF configuration diff --git a/services/syncthing.sh b/services/syncthing.sh index 5eda20c..5ee1136 100644 --- a/services/syncthing.sh +++ b/services/syncthing.sh @@ -147,6 +147,11 @@ CBLOCK mkdir -p "$_dir" cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } fi # Globals — ACTUAL_USER/ACTUAL_HOME must come before DOCKER_DIR @@ -225,6 +230,7 @@ networks: " fi + backup_if_exists docker-compose.yml cat > docker-compose.yml << EOF name: syncthing @@ -249,6 +255,7 @@ services: ${_CADDY_NET_BLOCK}${_CADDY_NET_SECTION} EOF + backup_if_exists .env cat > .env << ENV CADDY_NET=$SITE_CADDY_NET PUID=$PUID diff --git a/services/traccar.sh b/services/traccar.sh index 1f0fa70..9c216d7 100644 --- a/services/traccar.sh +++ b/services/traccar.sh @@ -180,6 +180,11 @@ CBLOCK mkdir -p "$_dir" cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } fi # Globals — ACTUAL_USER/ACTUAL_HOME must come before DOCKER_DIR @@ -474,6 +479,7 @@ networks: - \"${PROTO_MIN}-${PROTO_MAX}:${PROTO_MIN}-${PROTO_MAX}/udp\"" fi + backup_if_exists docker-compose.yml cat > docker-compose.yml << TRACCAR_COMPOSE name: $CONTAINER @@ -560,6 +566,7 @@ SMS_HTTP_PASSWORD=$SMS_HTTP_PASSWORD fi fi + backup_if_exists .env cat > .env << TRACCAR_ENV TZ=$TZ_VAL CADDY_NET=$SITE_CADDY_NET diff --git a/services/unifi.sh b/services/unifi.sh index 15a41ca..11b80df 100644 --- a/services/unifi.sh +++ b/services/unifi.sh @@ -89,6 +89,11 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then mkdir -p "$_dir" cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } fi # Globals — ACTUAL_USER/ACTUAL_HOME must come before DOCKER_DIR @@ -238,6 +243,7 @@ networks: fi # Unquoted heredoc; ${...} used for caddy_net vars; all Docker Compose vars escaped with \$ + backup_if_exists docker-compose.yml cat > docker-compose.yml << UNIFI_COMPOSE name: $PROJECT @@ -286,6 +292,7 @@ configs: db.getSiblingDB("\${MONGO_DBNAME}_stat").createUser({user: "\${MONGO_USER}", pwd: "\${MONGO_PASS}", roles: [{role: "\${MONGO_ROLE}", db: "\${MONGO_DBNAME}_stat"}]}); UNIFI_COMPOSE + backup_if_exists .env cat > .env << UNIFI_ENV # ── General ─────────────────────────────────────────────────────────────────── TZ=$TZ_VAL diff --git a/services/uptimekuma.sh b/services/uptimekuma.sh index 67f9e08..071b4cf 100644 --- a/services/uptimekuma.sh +++ b/services/uptimekuma.sh @@ -177,6 +177,11 @@ CBLOCK mkdir -p "$_dir" cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } fi # Globals — ACTUAL_USER/ACTUAL_HOME must come before DOCKER_DIR @@ -277,6 +282,7 @@ ${UPTIME_ENV_BLOCK} " fi + backup_if_exists docker-compose.yml cat > docker-compose.yml << UPTIME_COMPOSE name: uptime-kuma diff --git a/services/vaultwarden.sh b/services/vaultwarden.sh index 207e2b2..bcc4a59 100644 --- a/services/vaultwarden.sh +++ b/services/vaultwarden.sh @@ -194,6 +194,11 @@ CBLOCK mkdir -p "$_dir" cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } fi # Globals — ACTUAL_USER/ACTUAL_HOME must come before DOCKER_DIR @@ -420,6 +425,7 @@ networks: " fi + backup_if_exists docker-compose.yml cat > docker-compose.yml << VW_COMPOSE name: $CONTAINER @@ -437,6 +443,7 @@ services: ${_CADDY_NET_BLOCK}${_CADDY_NET_SECTION} VW_COMPOSE + backup_if_exists .env cat > .env << VW_ENV # ── General ─────────────────────────────────────────────────────────────────── TZ=$TZ_VAL diff --git a/services/watchtower.sh b/services/watchtower.sh index bd7be55..cc8290c 100644 --- a/services/watchtower.sh +++ b/services/watchtower.sh @@ -64,6 +64,11 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then mkdir -p "$_dir" cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } fi # Globals — ACTUAL_USER/ACTUAL_HOME must come before DOCKER_DIR @@ -151,6 +156,7 @@ networks: " fi + backup_if_exists docker-compose.yml cat > docker-compose.yml << WT_COMPOSE name: watchtower @@ -179,6 +185,7 @@ ${_CADDY_NET_BLOCK}${_CADDY_NET_SECTION} WT_COMPOSE # Create .env + backup_if_exists .env cat > .env << WT_ENV # Watchtower Configuration # ========================= diff --git a/services/watchyourlan.sh b/services/watchyourlan.sh index 94b767e..05cbc63 100644 --- a/services/watchyourlan.sh +++ b/services/watchyourlan.sh @@ -68,6 +68,11 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then mkdir -p "$_dir" cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } fi # Globals — ACTUAL_USER/ACTUAL_HOME must come before DOCKER_DIR @@ -126,6 +131,7 @@ install_watchyourlan() { prompt_text "GUI port [8840]:" "8840" GUI_PORT [ -z "$GUI_PORT" ] && GUI_PORT="8840" + backup_if_exists docker-compose.yml cat > docker-compose.yml << 'WYL_COMPOSE' name: watchyourlan @@ -141,6 +147,7 @@ services: - ./watchyourlan_data:/data WYL_COMPOSE + backup_if_exists .env cat > .env << WYL_ENV # ── General ─────────────────────────────────────────────────────────────────── TZ=$TZ_VAL diff --git a/services/wg-easy.sh b/services/wg-easy.sh index 846ff7a..fc9abab 100644 --- a/services/wg-easy.sh +++ b/services/wg-easy.sh @@ -199,6 +199,11 @@ CBLOCK mkdir -p "$_dir" cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } fi # Globals — ACTUAL_USER/ACTUAL_HOME must come before DOCKER_DIR @@ -302,6 +307,7 @@ networks: " fi + backup_if_exists docker-compose.yml cat > docker-compose.yml << WGEASY_COMPOSE name: wg-easy @@ -332,6 +338,7 @@ services: ${_CADDY_NET_BLOCK}${_CADDY_NET_SECTION} WGEASY_COMPOSE + backup_if_exists .env cat > .env << WGEASY_ENV WG_HOST=$WG_HOST # Plain-text password — used only if PASSWORD_HASH could not be generated above diff --git a/services/wolf-pair.sh b/services/wolf-pair.sh index a874b9a..a802d2f 100644 --- a/services/wolf-pair.sh +++ b/services/wolf-pair.sh @@ -169,6 +169,11 @@ CBLOCK mkdir -p "$_dir" cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } fi # Globals — ACTUAL_USER/ACTUAL_HOME must come before DOCKER_DIR @@ -404,6 +409,7 @@ DOCKERFILE # network_mode: host — server.py reaches Wolf at localhost:47989 directly. # Docker socket (ro) — server.py calls `docker logs wolf` to read secrets. log_info "Writing docker-compose.yml..." + backup_if_exists "$WOLFPAIR_DIR/docker-compose.yml" cat > "$WOLFPAIR_DIR/docker-compose.yml" << 'COMPOSE' name: wolf-pair diff --git a/services/wolf.sh b/services/wolf.sh index 45c2f07..463f95b 100644 --- a/services/wolf.sh +++ b/services/wolf.sh @@ -179,6 +179,11 @@ CBLOCK mkdir -p "$_dir" cat > "$_dir/README.md" } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } fi # Globals — ACTUAL_USER/ACTUAL_HOME must come before DOCKER_DIR @@ -875,6 +880,7 @@ UDEV # if the drive is remounted at a different path. IP/MAC/render-node are # baked in at install time because they're hardware-specific and not stored # in .env — use 'manage.sh update-network' to regenerate if they change. + backup_if_exists docker-compose.yml cat > docker-compose.yml << EOF name: wolf @@ -922,6 +928,7 @@ EOF log_success "docker-compose.yml created" # Save the game storage path so it's visible and editable later + backup_if_exists .env cat > .env << EOF # Wolf game/ROM storage root — edit this and run ./manage.sh update-storage to apply GAME_STORAGE_DIR=${GAME_STORAGE_DIR} diff --git a/services/wordpress.sh b/services/wordpress.sh index 7438e0f..45eecf5 100644 --- a/services/wordpress.sh +++ b/services/wordpress.sh @@ -207,6 +207,11 @@ CBLOCK cat > "$_dir/README.md" chown "$ACTUAL_USER:$ACTUAL_USER" "$_dir/README.md" 2>/dev/null || true } + backup_if_exists() { + local _file="$1" + [ -f "$_file" ] || return 0 + cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null + } fi ACTUAL_USER="${ACTUAL_USER:-${SUDO_USER:-$USER}}" @@ -361,6 +366,7 @@ PHPINI " fi + backup_if_exists docker-compose.yml cat > docker-compose.yml << WPCOMPOSE name: $CONTAINER @@ -398,6 +404,7 @@ networks: ${_CADDY_NET_SECTION} WPCOMPOSE + backup_if_exists .env cat > .env << WPENV TZ=$TZ_VAL CADDY_NET=$SITE_CADDY_NET