Backups and restore
Automatic backups
coachd snapshots its database at start and then daily, keeping the newest 7 in /data/backups (coachd-<UTC timestamp>.db plus a .json manifest with the SHA-256 of the database and every FIT file). The BackupStale alert fires when the newest is over 48 h old. For a snapshot now:
docker compose exec coachd /coachd backupNot in a snapshot: deploy/secrets/ and .env (keep them in a password manager), a Garmin key renewed on the page, and nonce.key (recreated on start; links in flight stop working).
Restore
docker compose stop coachd
docker compose run --rm --no-deps coachd restore /data/backups/coachd-<ts>.db
docker compose start coachdrestore refuses, before touching anything, while coachd runs, and for a corrupt or foreign snapshot, one from a newer schema, one that doesn’t match its manifest, or a demo snapshot over real data (or the reverse). Don’t run other coachd commands during a restore. The previous database moves to /data/pre-restore-<ts>/; delete it once you’re satisfied.
Kept across a restore: every revocation (agent tokens, passkeys, watches) and every used approval link, so nothing revoked works again and no approval link works twice. Sign-in sessions and, in built-in mode, app connections are cleared: sign in and reconnect your apps.
Rolled back to the snapshot: proposals and their states (a workout approved later is pending again), notes, the goal, and tokens, passkeys and watches created later (create or re-pair them).
If the previous database can’t be read, the restore warns and goes ahead: check token list and device list, and revoke again anything revoked after the snapshot.
Undo a restore
The coachd image has no shell, so use a throwaway container to move the previous files back:
docker compose stop coachd
T=tailscale/tailscale:v1.104.2@sha256:b11583ad2e93e4671128538c88cf11f460444a5d7e449311dd1933a400ab849d
docker run --rm --network none -v coachd_coachd-data:/data --entrypoint sh "$T" -c \
'cd /data && rm -f coachd.db coachd.db-wal coachd.db-shm && mv pre-restore-<ts>/coachd.db* .'
docker compose start coachdOff-site with restic
restic/restic.sh runs a pinned restic image over the volume, read-only, and copies /data/backups and /data/fit.
Create
restic.envindeploy/(mode0600):RESTIC_REPOSITORY=...plus your provider’s credentials (S3, B2, SFTP, …).Store the repository password:
put_secret restic_password 0:0(helper). Losing it loses the backups: keep a copy in your password manager.Initialise and run it:
./restic/restic.sh init ./restic/restic.sh backup # also prunes: 7 daily, 4 weekly, 12 monthly ./restic/restic.sh snapshotsSchedule it, from
deploy/, for example in root’s crontab:30 3 * * * cd /path/to/coachd/deploy && ./restic/restic.sh backup >> /var/log/coachd-restic.log 2>&1 30 4 * * 0 cd /path/to/coachd/deploy && ./restic/restic.sh check >> /var/log/coachd-restic.log 2>&1
Disaster recovery
On a new or wiped host. Don’t start coachd until the last step: on an empty volume it would write a backup of an empty database, newer than every real one.
Create the volume
With the stack files, secrets/ and .env in place:
docker compose run --rm --no-deps coachd versionThis creates the coachd_coachd-data volume without starting coachd.
Fetch and copy the latest restic snapshot
./restic/restic.sh restore-latest /srv/coachd-restore # an existing directory
docker run --rm --network none -v coachd_coachd-data:/data \
-v /srv/coachd-restore/data:/restore:ro --entrypoint sh "$T" -c \
'cp -a /restore/fit /restore/backups /data/ && chown -R 65532:65532 /data/fit /data/backups'T is the throwaway image from Undo a restore.
Restore the newest snapshot and start
List /data/backups (docker run --rm -v coachd_coachd-data:/data:ro --entrypoint ls "$T" -l /data/backups), delete any snapshot written after the disaster, then:
docker compose run --rm --no-deps coachd restore /data/backups/coachd-<ts>.db
docker compose up -d