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).
Two independent, requested changes:
- services/pihole.sh: new standalone service, Pi-hole v6 (the image moved
entirely to a TOML-based /etc/pihole config — the old WEBPASSWORD env var
and separate /etc/dnsmasq.d volume are both gone; uses
FTLCONF_webserver_api_password and FTLCONF_dns_listeningMode=ALL
instead). Deliberately not wired into wg-easy or any other VPN — a device
has to be pointed at it manually (per-device or via router DHCP). DNS
itself (53/tcp+udp) is never scanned/moved since shifting it off the
standard port would defeat the point; a port_in_use check warns instead
of blocking, since the common case (systemd-resolved on 127.0.0.53 only)
doesn't actually collide with Pi-hole binding the host's real interfaces.
Web admin UI is Caddy-fronted like everything else in this repo. Added to
the README services table.
- services/wg-easy.sh: default VPN/web ports moved from 51820/51821 to
51830/51831. Netbird's own WireGuard listener also defaults to exactly
51820 — installing both on one box means wg-easy's existing scan-and-move
logic would silently shift its port every time, which is harder to
predict/document than just not starting on the collision in the first
place. The scan itself is unchanged and still moves both ports further if
even the new default is taken.
Tested pihole.sh's full standalone install flow (no-Caddy and
Caddy-present-locally cases) against a mocked environment, validating both
generated docker-compose.yml files with `docker compose config`, and
confirmed the reinstall-mode gate correctly no-ops on a second run in
unattended mode.