--env-file only feeds the containers; a plain shell script never saw
BACKUP_DIR, so it silently fell back to ./backups while the Hub itself used
the real value from .env. Found in production: postgres's /backups bind
mount was still pinned to a directory orphaned by an earlier git-archive
swap (mv gti-fiscal gti-fiscal_old && mv gti-fiscal_new gti-fiscal — Linux
bind mounts follow the original dentry, not the path string), so every
backup.sh run had been silently landing dumps nobody could find at the
live path. Fixed by pointing BACKUP_DIR at a stable absolute path outside
the deploy tree and recreating postgres once to pick it up; verified with
a real ./backup.sh run in production.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
backup.sh now also streams the fiscal-hub container's release binaries
(dist tarball, never in Postgres) into BACKUP_DIR alongside the pg_dump,
with the same 14-backup retention; restore_releases.sh is the companion
restore. Verified with a real restore smoke test in production: the
restored dump's agent_releases row and the restored tarball's exe both
match the published sha256 exactly.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Found by running deploy/hub/backup.sh on the production VM:
- With core.autocrlf=true, `git archive` on the Windows release machine
shipped the .sh files with CRLF and `sh` aborted on line 4
("set: Illegal option -"): backup, restore and seed never ran.
.gitattributes now forces *.sh to LF.
- Docker creates the bind-mounted backups/ directory as root, so the
host-side retention `rm` would fail once the 15th dump existed.
Retention now runs inside the postgres container.
tests/test_deploy_scripts.py covers both.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>