minecraft-server/scripts/restic-init.sh

157 lines
5.5 KiB
Bash
Raw Normal View History

backup: phase 1 + phase 2 scripts; daily script repaired and deployed Repairs the orphaned synapse-signing-key block at scripts/backup.sh lines 119-122 that was exiting the script under set -e before the Minecraft block could run, leaving 5 of the last 7 days without a world backup and zero usable snapshots after 7-day retention. Phase 1 (deployed today to /opt/docker/backup.sh on nullstone): - Repaired script — orphan block removed, MC arm wrapped so failures in one tar don't kill the run - tar exit code 1 ("file changed as we read it") now treated as success on the live MC world; spark profiler tmp file noise silenced via --ignore-failed-read --warning=no-file-changed - Plugin DBs (homestead, AuthMe, CoreProtect, LuckPerms) and configs now backed up alongside the world - Sentinel /opt/backups/.last-success stamped only when the world arm succeeds — gives outside monitors a single mtime to alert on - Manually verified end-to-end: 12G world tarball, 492M plugins, 279M dbs, 14 config files, sentinel updated. Pre-fix script saved at /opt/docker/backup.sh.bak-20260507-pre-phase1. Phase 2 (scripts in repo, deployment pending operator sudo): - scripts/restic-backup-playerdata.sh — Class A 5-min restic snapshots of playerdata/, stats/, advancements/, plugin DBs, LuckPerms; rcon save-all flush before snapshot; tag-scoped retention - scripts/restic-init.sh — one-time bootstrap (root-only) for /etc/mc-backup.{env,pw} + repo init at /home/user/restic/ - scripts/systemd/mc-backup-playerdata.{service,timer} — 5-min timer with hardening (ProtectSystem=strict, ReadOnlyPaths, etc) - docs/RUNBOOK-BACKUP-RESTORE.md updated with both phases' deployment steps and the operator-action checklist Off-host mirror to onyx (Phase 4) and class B/C/D world snapshots (Phase 3) are still TODO — see BACKUP-STRATEGY.md §11 phase plan.
2026-05-07 18:29:30 +01:00
#!/usr/bin/env bash
# scripts/restic-init.sh
#
# One-time bootstrap for the Phase 2 restic backup chain. Run this on
# nullstone as root (sudo) AFTER `apt install restic mcrcon`.
#
# What it does:
# 1. Generates /etc/mc-backup.pw (40-byte random restic password) if absent.
# 2. Writes /etc/mc-backup.env (consumed by restic-backup-playerdata.sh).
# 3. Initialises the local restic repo at /home/user/restic/mc-frequent.
# 4. Takes a baseline snapshot so the timer's first run is fast.
# 5. Optionally adds an SFTP-mirror block once onyx is provisioned.
#
# Idempotent — re-running is safe; existing files are preserved.
#
# Cross-ref: docs/BACKUP-STRATEGY.md §8.2, docs/RUNBOOK-BACKUP-RESTORE.md.
set -euo pipefail
umask 077
if [ "$(id -u)" -ne 0 ]; then
echo "FATAL: must run as root (sudo)." >&2
exit 2
fi
if ! command -v restic >/dev/null 2>&1; then
echo "FATAL: restic not installed. Run: apt install restic mcrcon" >&2
exit 3
fi
# Resolve target user — restic repo lives under their home so /opt
# disk pressure doesn't matter. nullstone: 142G free on /home.
TARGET_USER="${TARGET_USER:-user}"
if ! id "$TARGET_USER" >/dev/null 2>&1; then
echo "FATAL: user '$TARGET_USER' not found" >&2
exit 4
fi
TARGET_HOME=$(getent passwd "$TARGET_USER" | cut -d: -f6)
PW_FILE="/etc/mc-backup.pw"
ENV_FILE="/etc/mc-backup.env"
REPO_FREQUENT="${TARGET_HOME}/restic/mc-frequent"
REPO_WORLD="${TARGET_HOME}/restic/mc-world"
LOG_DIR="/var/log"
SENTINEL_DIR="/var/lib/mc-backup"
# 1. Password file
if [ ! -e "$PW_FILE" ]; then
head -c 40 /dev/urandom | base64 > "$PW_FILE"
chown root:root "$PW_FILE"
chmod 600 "$PW_FILE"
echo "Generated $PW_FILE (40 bytes random)."
else
echo "$PW_FILE already exists — keeping."
fi
# 2. Env file (only created if missing; user can edit afterwards).
if [ ! -e "$ENV_FILE" ]; then
cat > "$ENV_FILE" <<EOF
# /etc/mc-backup.env — consumed by restic-backup-playerdata.sh and the
# Class B/C/D world script (TBD). Edit as needed.
RESTIC_REPOSITORY_FREQUENT=$REPO_FREQUENT
RESTIC_REPOSITORY_WORLD=$REPO_WORLD
RESTIC_PASSWORD_FILE=$PW_FILE
MC_DATA=/opt/docker/minecraft
MC_BACKUP_LOG=$LOG_DIR/mc-backup.log
MC_BACKUP_FREQUENT_SENTINEL=$SENTINEL_DIR/last-success-frequent
MC_BACKUP_WORLD_SENTINEL=$SENTINEL_DIR/last-success-world
# RCON — used to flush MC saves before snapshotting. Pull from the live
# compose file (services.mc.environment.RCON_PASSWORD).
MC_RCON_HOST=127.0.0.1
MC_RCON_PORT=25575
MC_RCON_PASSWORD=*redacted*
# Off-host mirror destination (onyx via Tailscale). Empty = skip mirror.
TS_OFFHOST_USER=mc-backup
TS_OFFHOST_HOST=100.64.0.1
TS_OFFHOST_PATH=/backups/nullstone-mc-restic
# Alerting — fill in once ntfy.s8n.ru is up. Leave blank for now.
HEARTBEAT_URL=
ALERT_URL=
EOF
chown root:"$(id -gn "$TARGET_USER")" "$ENV_FILE"
chmod 640 "$ENV_FILE"
echo "Wrote $ENV_FILE (mode 640, group=$(id -gn "$TARGET_USER"))."
else
echo "$ENV_FILE already exists — keeping."
fi
# 3. Log + sentinel dirs (writable by target user).
mkdir -p "$SENTINEL_DIR"
chown "$TARGET_USER":"$(id -gn "$TARGET_USER")" "$SENTINEL_DIR"
chmod 750 "$SENTINEL_DIR"
touch "$LOG_DIR/mc-backup.log"
chown "$TARGET_USER":adm "$LOG_DIR/mc-backup.log" 2>/dev/null \
|| chown "$TARGET_USER":"$(id -gn "$TARGET_USER")" "$LOG_DIR/mc-backup.log"
chmod 640 "$LOG_DIR/mc-backup.log"
# 4. Repo init (idempotent — restic init exits non-zero if repo exists).
init_repo() {
local repo=$1
install -d -o "$TARGET_USER" -g "$(id -gn "$TARGET_USER")" -m 700 \
"$(dirname "$repo")" "$repo"
if RESTIC_PASSWORD_FILE="$PW_FILE" RESTIC_REPOSITORY="$repo" \
runuser -u "$TARGET_USER" -- restic snapshots >/dev/null 2>&1; then
echo "Repo $repo: already initialised."
else
RESTIC_PASSWORD_FILE="$PW_FILE" RESTIC_REPOSITORY="$repo" \
runuser -u "$TARGET_USER" -- restic init
echo "Repo $repo: initialised."
fi
}
init_repo "$REPO_FREQUENT"
init_repo "$REPO_WORLD"
# 5. Baseline snapshot of the frequent repo so the first timer run is fast.
echo "Taking baseline snapshot into $REPO_FREQUENT ..."
runuser -u "$TARGET_USER" -- env \
RESTIC_PASSWORD_FILE="$PW_FILE" \
RESTIC_REPOSITORY="$REPO_FREQUENT" \
restic backup \
--tag playerdata --tag baseline --host "$(hostname)" \
--exclude='*.lock' --exclude='*.tmp' \
/opt/docker/minecraft/world/playerdata \
/opt/docker/minecraft/world/stats \
/opt/docker/minecraft/world/advancements \
/opt/docker/minecraft/homestead_data.db \
/opt/docker/minecraft/plugins/AuthMe \
/opt/docker/minecraft/plugins/CoreProtect/database.db \
/opt/docker/minecraft/plugins/LuckPerms \
|| echo "Baseline snapshot returned non-zero — review output above."
cat <<'NEXT'
---------------------------------------------------------------
restic-init.sh complete.
Next steps:
1. Install systemd units:
install -m644 scripts/systemd/mc-backup-playerdata.service \
/etc/systemd/system/
install -m644 scripts/systemd/mc-backup-playerdata.timer \
/etc/systemd/system/
install -m755 scripts/restic-backup-playerdata.sh \
/usr/local/bin/
2. systemctl daemon-reload
3. systemctl enable --now mc-backup-playerdata.timer
4. Tail: journalctl -u mc-backup-playerdata.service -f
Onyx (off-host mirror) provisioning is a separate step — see
docs/RUNBOOK-BACKUP-RESTORE.md "Phase 2 deployment".
---------------------------------------------------------------
NEXT