fix(beszel): apply BESZEL_DOMAIN passed to a re-run

Same defect as webfinger, and the typo case makes it concrete. A first deploy
with monitroing.example.com never gets an ACME certificate. The operator spots
it and re-runs with the correct hostname -- .env still holds the typo, so
Caddy still has only that site block, still fails ACME, and the endpoint still
does not work, while the script exits 0 printing
"URL: https://monitoring.example.com".

The two sources disagree and nothing compares them: validation greps .env for
non-empty (a typo passes), while the confirmation prompt and the DEPLOYED
banner expand this shell's variable. Interactively the prompted value is not
exported, so compose produces a byte-identical config and does not even
recreate the container -- nothing happens at all.

Fixed with the same idiom as webfinger and d654299.

Verified: a corrected BESZEL_DOMAIN lands in .env and is logged; a re-run
passing nothing leaves .env untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-16 14:21:35 -05:00
co-authored by Claude Opus 5
parent 237aa913f0
commit 79db4d1e29
+65 -2
View File
@@ -7,7 +7,9 @@
# What this does:
# 1. Installs docker + docker-cli-compose if missing.
# 2. Lays down docker-compose.yml, Caddyfile, .env.example in $STACK_DIR.
# 3. Generates .env on first run; existing .env is never overwritten.
# 3. Generates .env on first run. On a re-run an existing .env is kept,
# except for values passed explicitly that run, which are written
# through -- .env is what compose interpolates into the container.
# 4. Prompts for required values not preset (BESZEL_DOMAIN, ACME_EMAIL).
# 5. Opens TCP 80/443 in UFW if active.
# 6. Pulls images, brings the stack up, waits for healthchecks.
@@ -32,6 +34,18 @@ set -euo pipefail
: "${FORCE:=0}"
: "${SKIP_PROMPTS:=0}" # non-interactive: require values via env, no prompts
[[ "$SKIP_PROMPTS" == "1" ]] && FORCE=1
ENV_FILE="$STACK_DIR/.env"
# Both values reach Caddy only by compose interpolating .env, so .env has to absorb
# what this run passed or the run deploys the old values while reporting the
# new ones. Which keys arrived in the ENVIRONMENT must be recorded BEFORE the
# ":=" defaults below, which make an unset variable look like an empty one.
RUNTIME_KEYS=(BESZEL_DOMAIN ACME_EMAIL)
KEYS_FROM_ENV=()
for _k in "${RUNTIME_KEYS[@]}"; do
if [[ -n "${!_k+x}" ]]; then KEYS_FROM_ENV+=("$_k"); fi
done
: "${BESZEL_DOMAIN:=}"
: "${ACME_EMAIL:=}"
@@ -142,6 +156,24 @@ prompt() {
fi
}
# On a re-run the deployed values live in .env. Load them into any key NOT
# passed this run, so the prompts below do not force the operator to retype
# everything (and so SKIP_PROMPTS=1 does not die demanding values .env has).
if [[ -f "$ENV_FILE" ]]; then
_preloaded=()
for _k in "${RUNTIME_KEYS[@]}"; do
if [[ " ${KEYS_FROM_ENV[*]-} " == *" ${_k} "* ]]; then continue; fi
_v=$(sed -n "s/^${_k}=//p" "$ENV_FILE" | tail -n1)
if [[ -z "$_v" ]]; then continue; fi
printf -v "$_k" '%s' "$_v"
_preloaded+=("$_k")
done
if (( ${#_preloaded[@]} > 0 )); then
log "Reusing ${ENV_FILE} values for: ${_preloaded[*]}"
log " (pass VAR=... to change one; it is then written back to .env)"
fi
fi
prompt BESZEL_DOMAIN "Public hostname for beszel (e.g. monitoring.example.com)"
prompt ACME_EMAIL "Let's Encrypt email"
@@ -163,6 +195,28 @@ install -d -m 0750 "$STACK_DIR"
install -m 0640 "$SCRIPT_DIR/docker-compose.yml" "$STACK_DIR/docker-compose.yml"
install -m 0640 "$SCRIPT_DIR/Caddyfile" "$STACK_DIR/Caddyfile"
set_env() { # <KEY> <value>: update KEY in .env, or append if absent
# The value goes through the ENVIRONMENT, never interpolated into a sed
# script. Interpolating it corrupts any value containing & (sed expands it to
# the whole match) and aborts the run on one containing the s||| delimiter --
# which is reachable for an OIDC issuer URL or a redirect query string.
local key="$1" val="$2" tmp
if [[ ! -f "$ENV_FILE" ]]; then
printf '%s=%s
' "$key" "$val" >> "$ENV_FILE"
return 0
fi
tmp="$(mktemp)"
_SE_KEY="$key" _SE_VAL="$val" awk '
BEGIN { k = ENVIRON["_SE_KEY"]; v = ENVIRON["_SE_VAL"]; seen = 0 }
!seen && index($0, k "=") == 1 { print k "=" v; seen = 1; next }
{ print }
END { if (!seen) print k "=" v }
' "$ENV_FILE" > "$tmp"
cat "$tmp" > "$ENV_FILE" # rewrite in place: keeps the original mode/owner
rm -f "$tmp"
}
ENV_FILE="$STACK_DIR/.env"
if [[ ! -f "$ENV_FILE" ]]; then
log "Seeding $ENV_FILE..."
@@ -172,7 +226,16 @@ if [[ ! -f "$ENV_FILE" ]]; then
-e "s|^ACME_EMAIL=.*|ACME_EMAIL=${ACME_EMAIL}|" \
"$ENV_FILE"
else
log ".env exists; leaving it alone."
# Keep the file, but absorb what was passed this run: compose reads .env,
# so letting it drift means deploying the old values while the banner
# prints the new ones. Keys not passed are left untouched.
log ".env exists; keeping it (only values passed this run are updated)."
for _k in ${KEYS_FROM_ENV[@]+"${KEYS_FROM_ENV[@]}"}; do
_cur=$(sed -n "s/^${_k}=//p" "$ENV_FILE" | tail -n1)
if [[ "$_cur" == "${!_k}" ]]; then continue; fi
set_env "$_k" "${!_k}"
log " ${_k}: ${_cur:-<unset>} -> ${!_k:-<empty>}"
done
fi
# Validate