Highlights
💲 Literal $ in config values no longer corrupts backups
gobackup loads its config by running os.ExpandEnv over the entire file before parsing it — so any $ in a value (most painfully a password like m9qq!$7v!s^$!UU) was silently eaten as a variable reference (→ m9qq!v!s^UU) and auth failed. YAML quoting couldn't help (expansion is pre-parse, on raw bytes) and os.ExpandEnv has no escape character.
The supervisor now keeps a $ only when it begins a real reference — $NAME or ${NAME} where NAME is [a-zA-Z][a-zA-Z0-9_]+ — and rewrites every other $ to ${GB_DOLLAR}, a sentinel it injects into the engine's env as a literal $. gobackup expands ${GB_DOLLAR} back to $ in a single pass, so the value round-trips exactly.
gobackup.databases.main.password: "m9qq!$7v!s^$!UU" # just works — no $$ gymnastics- Real references still expand:
$DB_PASSWORDand${DB_PASSWORD}are preserved and expanded by gobackup. - Everything else is literal:
$7,$!, a trailing$,$$, and even a too-short name like$A/${A}are kept verbatim. - Requires the supervisor to manage the engine (Docker socket +
gobackup-docker.component=gobackup), since it injects the sentinel. Label-only mode instead logs a warning naming each affected value. - Limit: a secret that genuinely contains a reference-shaped sequence (
p$word,Se${cret}) is treated as a reference — use a*_env/*_filecredential label for those.
Verified end-to-end (spike/e2e-dollar/): a Postgres password full of $ is dumped successfully — pg_dump authenticates, proving the round-trip.
🔁 Fix: single config write per reconcile
Previously render() wrote a raw config that Phase 2 then rewrote with transformed volume paths — flip-flopping the writer's dedup (spurious changed every pass) and briefly exposing untransformed, unmounted paths to the engine. Path transforms and $-escaping now run first and the config is written exactly once. Regression guard added.
Multi-arch image ghcr.io/ekho/gobackup-docker tags 2.3.0, 2.3, 2, latest.