Back up existing files before every service overwrites them

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.<timestamp> 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).
This commit is contained in:
Claude
2026-08-26 17:26:17 +00:00
parent 493ee30916
commit bec9228c55
61 changed files with 486 additions and 5 deletions
+26
View File
@@ -802,6 +802,32 @@ write_readme() {
chown "$ACTUAL_USER:$ACTUAL_USER" "$dir/README.md" 2>/dev/null || true
}
# Copies FILE to FILE.bak.<timestamp> 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.
+7
View File
@@ -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
+8
View File
@@ -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}
+7
View File
@@ -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
+7
View File
@@ -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
+7
View File
@@ -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}
+7
View File
@@ -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
+7
View File
@@ -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
+9
View File
@@ -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
+6
View File
@@ -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
+7
View File
@@ -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
+7
View File
@@ -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}
+7
View File
@@ -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
+7
View File
@@ -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
+7
View File
@@ -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
+7
View File
@@ -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
+7
View File
@@ -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
+7
View File
@@ -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
+6
View File
@@ -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
+67 -5
View File
@@ -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
+7
View File
@@ -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)}
+7
View File
@@ -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)}
+7
View File
@@ -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
+7
View File
@@ -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}'
+1
View File
@@ -274,6 +274,7 @@ networks:
"
fi
backup_if_exists docker-compose.yml
cat > docker-compose.yml << HOMEASSISTANT_COMPOSE
name: homeassistant
+7
View File
@@ -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
+8
View File
@@ -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
#
+8
View File
@@ -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)
+7
View File
@@ -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
+7
View File
@@ -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
+6
View File
@@ -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
+7
View File
@@ -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
+7
View File
@@ -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")
+7
View File
@@ -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
+6
View File
@@ -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
+7
View File
@@ -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
+7
View File
@@ -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
+7
View File
@@ -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.
+7
View File
@@ -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
+2
View File
@@ -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}
+7
View File
@@ -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
+7
View File
@@ -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
+7
View File
@@ -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
+7
View File
@@ -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
+7
View File
@@ -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).
+6
View File
@@ -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
+1
View File
@@ -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}"
+7
View File
@@ -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
+6
View File
@@ -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"
+7
View File
@@ -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
+7
View File
@@ -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
+7
View File
@@ -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
+7
View File
@@ -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
+6
View File
@@ -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
+7
View File
@@ -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
+7
View File
@@ -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
# =========================
+7
View File
@@ -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
+7
View File
@@ -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
+6
View File
@@ -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
+7
View File
@@ -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}
+7
View File
@@ -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