#!/usr/bin/env bash # # Dezky host backup — Restic to a Hetzner Storage Box (primary), then a # dedup-aware `restic copy` to a second Storage Box in Helsinki (DR). # # Runs as root (must read stalwart- and root-owned data). HOME is pointed at # /opt/dezky-backup so ssh uses the dedicated backup key + config (Storage Box # is SSH/SFTP on port 23). Triggered daily by dezky-backup.timer. # # Requires restic >= 0.14 (for `copy --from-repo`). set -euo pipefail RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m' info() { echo -e "${BLUE}[INFO]${NC} $*"; } ok() { echo -e "${GREEN}[OK]${NC} $*"; } warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } BACKUP_HOME="/opt/dezky-backup" ENV_FILE="${ENV_FILE:-$BACKUP_HOME/restic.env}" if [[ ! -f "$ENV_FILE" ]]; then error "Missing $ENV_FILE — run restic/install.sh first." exit 1 fi # shellcheck disable=SC1090 source "$ENV_FILE" : "${RESTIC_PASSWORD:?RESTIC_PASSWORD required}" : "${BACKUP_PRIMARY_REPO:?BACKUP_PRIMARY_REPO required}" : "${BACKUP_PATHS:?BACKUP_PATHS required}" : "${BACKUP_RETENTION:=--keep-daily 7 --keep-weekly 4 --keep-monthly 6}" # ssh (spawned by restic) reads $HOME/.ssh/config — wildcard for *.your-storagebox.de export HOME="$BACKUP_HOME" export RESTIC_PASSWORD # For `copy`: both repos share the same password. export RESTIC_FROM_PASSWORD="$RESTIC_PASSWORD" # Optional dead-man's-switch (e.g. healthchecks.io). Pinged /start, success, /fail. HC="${BACKUP_HEALTHCHECK_URL:-}" ping_hc() { [[ -n "$HC" ]] && curl -fsS -m 10 --retry 3 "${HC}${1:-}" >/dev/null 2>&1 || true; } fail() { error "$1"; ping_hc "/fail"; exit 1; } ping_hc "/start" # Exclude obvious churn/noise from the PVC tree EXCLUDES=(--exclude-caches --exclude '*/lost+found' --exclude '*.tmp') # ── 1) Back up to the primary Storage Box ────────────────────────────────── info "Backing up to primary: $BACKUP_PRIMARY_REPO" # shellcheck disable=SC2086 restic -r "$BACKUP_PRIMARY_REPO" backup $BACKUP_PATHS \ "${EXCLUDES[@]}" \ --tag dezky --tag host \ --host dezky-node1 \ || fail "Primary backup failed." ok "Primary backup done." # ── 2) Retention on primary ──────────────────────────────────────────────── info "Applying retention on primary..." # shellcheck disable=SC2086 restic -r "$BACKUP_PRIMARY_REPO" forget $BACKUP_RETENTION --prune \ || warn "Primary forget/prune reported an issue (backup itself is safe)." # ── 3) Light integrity check on primary ──────────────────────────────────── restic -r "$BACKUP_PRIMARY_REPO" check || warn "restic check flagged the primary repo — investigate." # ── 4) Mirror to the Helsinki DR box (dedup-aware copy) ───────────────────── if [[ -n "${BACKUP_DR_REPO:-}" ]]; then info "Copying snapshots to DR: $BACKUP_DR_REPO" restic -r "$BACKUP_DR_REPO" copy --from-repo "$BACKUP_PRIMARY_REPO" \ || fail "DR copy failed." # shellcheck disable=SC2086 restic -r "$BACKUP_DR_REPO" forget $BACKUP_RETENTION --prune \ || warn "DR forget/prune reported an issue." ok "DR mirror done." else warn "BACKUP_DR_REPO not set — skipping off-site mirror (set it for real DR)." fi ok "Backup cycle complete." ping_hc "" # success ping (bare URL)