From bec9228c55d4f577939d9b2ef83b75faa5b5c4ff Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 26 Aug 2026 17:26:17 +0000 Subject: [PATCH] Back up existing files before every service overwrites them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Confirmed live: install_frigate()'s fresh-install path overwrote a working, hand-crafted docker-compose.yml (Frigate + mosquitto + frigate-notify) with zero backup, because that file's shape didn't match what frigate.sh's own "existing install" detection knew how to recognize. Every service's own detection is a judgment call about what counts as "already installed" and can miss a real setup built outside this repo's conventions. lib/common.sh gains backup_if_exists(FILE) — copies FILE to FILE.bak. if it exists, no-ops otherwise (including DRY_RUN). Applied before every service's own `cat > docker-compose.yml`/`cat > .env` write across all 60 services that do one (115 call sites), plus a matching standalone-mode stub added to every service's own bootstrap block, same convention already used for port_in_use/find_free_port. This doesn't replace a service's own update/fresh-reinstall detection — it's the safety net underneath it, so a wrong detection costs a .bak file to restore from instead of the original silently disappearing. Also fixes the actual gap that surfaced this: services/frigate.sh's Authelia offer only checked for Authelia installed locally on Frigate's own box, which is never true for a dedicated NVR box with no local Caddy either (the common shape — Caddy lives elsewhere, snippet-generation mode already handles that). Now offers Authelia protection unconditionally and, when Authelia isn't local, asks whether it lives on the same machine as Caddy (still "import authelia", since that's local to wherever Caddy ends up) or on a genuinely separate third machine (the explicit header-pinned forward_auth form, per CLAUDE.md's "forward_auth to a remote Authelia" note, needed because a bare authelia:9091 shortcut only works one hop). --- lib/common.sh | 26 +++++++++++++ services/actualbudget.sh | 7 ++++ services/ai-gpu.sh | 8 ++++ services/archivebox.sh | 7 ++++ services/arm.sh | 7 ++++ services/asterisk.sh | 7 ++++ services/audiobookshelf.sh | 7 ++++ services/authelia.sh | 7 ++++ services/beszel.sh | 9 +++++ services/caddy.sh | 6 +++ services/calibre-web.sh | 7 ++++ services/changedetection.sh | 7 ++++ services/ddclient.sh | 7 ++++ services/drum-rhythm-game.sh | 7 ++++ services/emby.sh | 7 ++++ services/filebrowser.sh | 7 ++++ services/fmd.sh | 7 ++++ services/frigate-audio.sh | 7 ++++ services/frigate-notify.sh | 6 +++ services/frigate.sh | 72 +++++++++++++++++++++++++++++++++--- services/garage-webui.sh | 7 ++++ services/garage.sh | 7 ++++ services/gatus.sh | 7 ++++ services/gitea.sh | 7 ++++ services/homeassistant.sh | 1 + services/homebox.sh | 7 ++++ services/immich.sh | 8 ++++ services/iopaint.sh | 8 ++++ services/jellyfin.sh | 7 ++++ services/joplin.sh | 7 ++++ services/js99er.sh | 6 +++ services/koha.sh | 7 ++++ services/kyber-server.sh | 7 ++++ services/lyrion.sh | 7 ++++ services/magicmirror.sh | 6 +++ services/mail-archiver.sh | 7 ++++ services/mattermost.sh | 7 ++++ services/mealie.sh | 7 ++++ services/meshcentral.sh | 7 ++++ services/minecraft.sh | 2 + services/n8n.sh | 7 ++++ services/nextcloud.sh | 7 ++++ services/ntfy.sh | 7 ++++ services/onlyoffice.sh | 7 ++++ services/pihole.sh | 7 ++++ services/portainer.sh | 6 +++ services/pstn-trunk.sh | 1 + services/rustdesk.sh | 7 ++++ services/silent-send.sh | 6 +++ services/stirling-pdf.sh | 7 ++++ services/syncthing.sh | 7 ++++ services/traccar.sh | 7 ++++ services/unifi.sh | 7 ++++ services/uptimekuma.sh | 6 +++ services/vaultwarden.sh | 7 ++++ services/watchtower.sh | 7 ++++ services/watchyourlan.sh | 7 ++++ services/wg-easy.sh | 7 ++++ services/wolf-pair.sh | 6 +++ services/wolf.sh | 7 ++++ services/wordpress.sh | 7 ++++ 61 files changed, 486 insertions(+), 5 deletions(-) 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..0cc3ec9 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}}" @@ -311,6 +316,7 @@ networks: local HB_PEPPER HB_PEPPER="$(generate_password 48)" + backup_if_exists docker-compose.yml cat > docker-compose.yml << HB_COMPOSE name: $CONTAINER @@ -331,6 +337,7 @@ services: ${_CADDY_NET_BLOCK}${_CADDY_NET_SECTION} HB_COMPOSE + backup_if_exists .env cat > .env << HB_ENV CADDY_NET=$SITE_CADDY_NET 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