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.
156 lines
5.5 KiB
Bash
156 lines
5.5 KiB
Bash
#!/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
|