#!/usr/bin/env bash # backup.sh — snapshot the fleet-manager durable state to timestamped tarballs. # # Two docker named volumes hold everything that isn't reproducible from the image: # - fleet-data : inventory.json + admins.json + enroll.key (CRITICAL — losing # admins.json/enroll.key locks every admin out of the UI) # - influx-data : the metrics time-series (history; regenerates going forward) # # Runs a throwaway alpine container to tar each volume read-only into $DEST. Safe # to run live. Schedule it (systemd timer / cron); copy $DEST offsite. # # Usage: # ./backup.sh [dest-dir] # default dest: ./backups # COMPOSE_PROJECT_NAME=foo ./backup.sh # if you run compose under another project # # Restore (manager stopped): # docker run --rm -v _fleet-data:/dest -v "$PWD/backups":/src alpine \ # sh -c 'rm -rf /dest/* && tar xzf /src/fleet-data-YYYYmmdd-HHMMSS.tar.gz -C /dest' set -euo pipefail DEST=${1:-./backups} PROJECT=${COMPOSE_PROJECT_NAME:-fleet-manager} # compose derives volume names as _ STAMP=$(date -u +%Y%m%d-%H%M%S) VOLUMES=${BACKUP_VOLUMES:-"fleet-data influx-data"} mkdir -p "$DEST" DEST_ABS=$(cd "$DEST" && pwd) for key in $VOLUMES; do vol="${PROJECT}_${key}" if ! docker volume inspect "$vol" >/dev/null 2>&1; then echo "backup: SKIP $vol (no such volume — wrong COMPOSE_PROJECT_NAME?)" continue fi out="${key}-${STAMP}.tar.gz" echo "backup: $vol -> $DEST_ABS/$out" docker run --rm \ -v "${vol}:/src:ro" \ -v "${DEST_ABS}:/backup" \ alpine tar czf "/backup/${out}" -C /src . done # Retention: keep the newest $KEEP of each volume's tarballs (0 = keep all). KEEP=${BACKUP_KEEP:-14} if [ "$KEEP" -gt 0 ]; then for key in $VOLUMES; do # ls -t for mtime order; our filenames are safe (key + UTC timestamp). # shellcheck disable=SC2012 ls -1t "$DEST_ABS/${key}-"*.tar.gz 2>/dev/null | tail -n +"$((KEEP + 1))" | while read -r old; do echo "backup: prune $old" rm -f "$old" done done fi echo "backup: done ($STAMP) — copy $DEST_ABS offsite."