一键导入
backup-ops
Expert knowledge for cluster backup and restore operations. Use when configuring backups, triggering manual jobs, or restoring data.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Expert knowledge for cluster backup and restore operations. Use when configuring backups, triggering manual jobs, or restoring data.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Operating the local coding agent — qwen3-coder via opencode, Claude-orchestrated, spec-driven. Use when running/troubleshooting `oc`, the ralph-qwen loop, or the laptop↔Beelink-qwen creds.
Expert knowledge for media applications (Jellyfin, Immich, Jellyseerr). Use when managing media storage, NFS mounts, or application-specific configurations.
Expert knowledge for Pi-hole and Unbound DNS operations. Use when configuring DNS, troubleshooting resolution issues, modifying adlists, or understanding the DNS data flow.
Expert knowledge for cluster observability. Use when configuring Prometheus/Grafana, Alertmanager, Uptime Kuma, or the Homepage dashboard.
Expert knowledge for Tailscale VPN operations. Use when configuring remote access, exit nodes, ACL policies, or troubleshooting VPN connectivity.
Expert knowledge for the Family Board kiosk dashboard (board.lab.mtgibbs.dev) — a framework-light PWA that renders the n8n intake feed. Use when designing or changing the board UI, adding fixtures, or wiring the feed proxy. The deployable workspace is clusters/pi-k3s/family-board/.
| name | backup-ops |
| description | Expert knowledge for cluster backup and restore operations. Use when configuring backups, triggering manual jobs, or restoring data. |
| allowed-tools | Bash, Read, Grep, Glob, Edit, Write |
| Operation | MCP Tool |
|---|---|
| Backup status (schedules, last runs) | get_backup_status |
| Trigger manual backup | trigger_backup(namespace, cronjob) |
| CronJob details (spec, volumes, history) | get_cronjob_details(namespace, cronjob) |
| Job logs (check output) | get_job_logs(namespace, job) |
All backups target the QNAP NAS (storage.lab.mtgibbs.dev → 192.168.1.61) as user cluster-backup using rsync over SSH.
In-cluster jobs run weekly on Sundays, staggered to avoid overlap. The Beelink job runs nightly (it lives off-cluster — see below).
Destination base: /share/cluster/backups/
Two backup planes. Jobs #1–#5 are Kubernetes CronJobs in the
backup-jobsnamespace — visible to the MCP tools and watched by theBackupCronJobStalealert. The Beelink AI-stack backup is NOT a cluster CronJob (its data is off-cluster); it's a systemd-timed Compose container ON the Beelink, watched by a separate metric-based alert. The MCPget_backup_status/trigger_backuptools do not see it.
pvc-backup — Master Node PVCspi-k3suptime-kuma_uptime-kuma-data, uptime-kuma_autokuma-data, pihole_pihole-etc, pihole_pihole-dnsmasq, monitoring_kube-prometheus-grafana, jellyfin_jellyfin-config/share/cluster/backups/{date}/{pvc_name}/worker2-backup — Worker 2 PVCspi5-worker-2media_sabnzbd-config, media_bazarr-config, n8n_n8n-data/share/cluster/backups/{date}/worker2/{pvc_name}/postgres-backup — Immich Databasepg_dump custom format (compression level 9)/share/cluster/backups/{date}/postgres/immich-db-password (from 1Password via ExternalSecret)media-backup — Worker 1 Media Stack PVCspi5-worker-1media_prowlarr-config, media_sonarr-config, media_radarr-config, media_qbittorrent-config, media_jellyseerr-config, media_lazylibrarian-config, media_calibre-web-config, media_readarr-config, media_lidarr-config, romm_romm-config/share/cluster/backups/{date}/media/{pvc_name}/git-mirror-backup — GitHub Repository Mirrorspi-k3smtgibbs (bare mirror clones)/share/cluster/backups/git-mirrors/github-mirror-token (from 1Password via ExternalSecret)mariadb-backup — RomM Databaseromm db on romm-mariadb.romm.svc.cluster.local, as the romm app user)mariadb-dump --single-transaction --routines --triggers (consistent online InnoDB dump), gzipped/share/cluster/backups/{date}/romm/romm-mariadb-{date}.sql.gzromm-db-password (from 1Password romm/db-password via ExternalSecret)romm-config PVC rides media-backup (pi5-worker-1), romm-redis-data is regenerable cache (not backed up), ROMs/saves/assets live on the QNAP NFS share directly (/share/cluster/games).The Beelink (beelink-ai, 192.168.1.70) holds stateful data that lives outside
the cluster, so it's backed up by a systemd-timed Compose container ON the Beelink
— NOT by a backup-jobs CronJob. Managed in the beelink-ansible repo
(playbooks/50-ai-stack.yml + files/beelink-backup.sh), not in this one.
OnCalendar=03:30 + up to 15 min jitter (beelink-backup.timer)beelink-backup Compose service, profile backup (not started by docker compose up); fired by beelink-backup.service (docker compose --profile backup run --rm)pg_dump (custom format, compress 9) over the ai-internal Docker network as the SELECT-only backup_ro role (no DB port is exposed; the role cannot mutate anything)/srv/openwebui (adults' OWUI), /srv/dewey-data (kids' OWUI), /srv/pipelines-data, /srv/ops-pipelines-data — tar -czf from :ro bind mounts/share/cluster/backups/{date}/beelink/ (litellm-{date}.dump + four *-{date}.tar.gz)beelink/ snapshots (prunes only the beelink/ subdir, never the date dirs)backup_ro password = op://pi-cluster/litellm-postgres-backup-ro/password (passed as the litellm_backup_ro_password extra-var); QNAP key reuses op://pi-cluster/synology_backup/private key via the QNAP_BACKUP_SSH_KEY env var (multiline → env, not an inline extra-var)beelink_backup_last_success_timestamp_seconds to the node_exporter textfile collector (/textfile/beelink_backup.prom). The BeelinkBackupStale alert (>36h) + BeelinkBackupMetricMissing (absent ≥30h) in clusters/pi-k3s/monitoring/prometheusrule-beelink.yaml watch it — this is the Beelink analogue of BackupCronJobStale, which can't see a non-cluster job.backup-jobsbackup-ssh-key (from 1Password, synology_backup/private key)instrumentisto/rsync-ssh:alpine (all jobs)clusters/pi-k3s/backup-jobs/All PVC backup jobs use the same approach:
/var/lib/rancher/k3s/storage as /storage (read-only)pvc-*_{namespace}_{pvcname}Adding a new PVC to backups: Determine which node the PVC lives on, then add {namespace}_{pvcname} to the PVCS variable in the corresponding cronjob manifest.
MCP (preferred):
trigger_backup(namespace="backup-jobs", cronjob="pvc-backup")
trigger_backup(namespace="backup-jobs", cronjob="media-backup")
trigger_backup(namespace="backup-jobs", cronjob="worker2-backup")
trigger_backup(namespace="backup-jobs", cronjob="postgres-backup")
trigger_backup(namespace="backup-jobs", cronjob="git-mirror-backup")
trigger_backup(namespace="backup-jobs", cronjob="mariadb-backup")
kubectl fallback (cluster-ops):
kubectl create job --from=cronjob/pvc-backup manual-pvc-backup -n backup-jobs
kubectl create job --from=cronjob/media-backup manual-media-backup -n backup-jobs
kubectl create job --from=cronjob/worker2-backup manual-worker2-backup -n backup-jobs
kubectl create job --from=cronjob/postgres-backup manual-postgres-backup -n backup-jobs
kubectl create job --from=cronjob/git-mirror-backup manual-git-mirror -n backup-jobs
MCP (preferred):
get_backup_status # Overview of all jobs
get_job_logs(namespace="backup-jobs", job="<job-name>") # Specific job output
kubectl fallback (cluster-ops):
kubectl get jobs -n backup-jobs --sort-by=.metadata.creationTimestamp
kubectl logs job/<job-name> -n backup-jobs
Run on beelink-ai directly, or remotely with ansible inference -m shell -a "..."
from the beelink-ansible repo.
# Trigger a backup now (oneshot)
sudo systemctl start beelink-backup.service
# Status / last result / next scheduled run
journalctl -u beelink-backup.service -n 50 --no-pager
systemctl list-timers beelink-backup.timer --no-pager
# Confirm the freshness metric is being served (what the alert watches)
curl -s localhost:9100/metrics | grep beelink_backup
On the Beelink. Backups are at cluster-backup@storage.lab.mtgibbs.dev:/share/cluster/backups/{date}/beelink/.
# copy the dump to the postgres container and restore as the litellm superuser
docker exec -i postgres pg_restore -U litellm -d litellm --clean --if-exists < litellm-{date}.dump
cd /opt/ai-stack && docker compose stop open-webui
tar -xzf openwebui-{date}.tar.gz -C /srv/openwebui
docker compose start open-webui
(same shape for dewey-data→open-webui-dewey, pipelines-data→pipelines, ops-pipelines-data→pipelines-ops)Recovery = Flux redeploys the apps (empty PVCs) + restore each config PVC from the QNAP.
Uses clusters/pi-k3s/backup-jobs/restore-job.template.yaml — a Job that mounts the
target PVC (K3s auto-provisions the local-path volume on the right node, so no
pvc-<uuid> path hunting) and rsyncs the backup back in.
Proven 2026-06-14 via a scratch-PVC dry-run: sonarr's backup restored with
PRAGMA integrity_check: okand real config (7 indexers, 2 download clients, 1 root folder), production untouched.
⚠️ The chown to
1029:100is MANDATORY. The QNAP squashes backup file ownership to the backup user (uid 1001) on write, but the apps run as PUID 1029 / PGID 100. The restore Job re-chowns; skip it and the restored app can't read its own config. (All media apps share 1029:100 — verify withkubectl exec deploy/<app> -- ls -ln /configif unsure.)
Per-app steps:
ssh cluster-backup@storage.lab.mtgibbs.dev "ls -dt /share/cluster/backups/*/ | head"kubectl -n media scale deploy/<app> --replicas=0sed -e 's/__APP__/sonarr/' -e 's/__PVC__/sonarr-config/' \
-e 's/__NODE__/pi5-worker-1/' -e 's#__SRC__#media/media_sonarr-config#' \
-e 's/__DATE__/2026-06-14/' \
clusters/pi-k3s/backup-jobs/restore-job.template.yaml | kubectl apply -f -
kubectl -n media logs -f job/restore-sonarr # has an empty-source guard
kubectl -n media scale deploy/<app> --replicas=1 && kubectl -n media delete job restore-<app>*-config API key still works).Node + source map (local-path PVCs are node-pinned):
| Node | __SRC__ prefix | apps |
|---|---|---|
pi5-worker-1 | media/media_<app>-config | sonarr radarr prowlarr qbittorrent jellyseerr lazylibrarian calibre-web readarr lidarr |
pi5-worker-2 | worker2/media_<app>-config | sabnzbd bazarr |
API-key note: restoring the PVC keeps the app's API key matching 1Password
(<app>.lab.mtgibbs.dev/api-key), so homepage / MCP / import-resolver keep working. A
from-empty rebuild (no restore) mints a NEW key — re-harvest it into 1Password.
Caveats: single NAS copy, rsync --delete mirror, ~4 weekly snapshots, no offsite.
NFS media library (movies/tv/music/books) lives on the QNAP and is not in these config
backups. The restore-job.template.yaml is intentionally excluded from kustomization
(Flux never auto-runs it).
These live on pi-k3s (via pvc-backup). Same idea, but they're not media-namespace:
identify the backup, scale the workload down, rsync the dated dir back to the node's
/var/lib/rancher/k3s/storage/pvc-*_<ns>_<name>/, fix ownership, scale up.
pg_restore -U immich -d immich -v /path/to/backup.dumpkubectl -n romm scale deploy/romm --replicas=0scp cluster-backup@storage.lab.mtgibbs.dev:/share/cluster/backups/{date}/romm/romm-mariadb-{date}.sql.gz /tmp/gunzip -c /tmp/romm-mariadb-{date}.sql.gz | \
kubectl -n romm exec -i deploy/romm-mariadb -- \
sh -c 'mariadb -u romm -p"$MARIADB_PASSWORD" romm'
romm-config PVC holds the auth assets — restore it from media/romm_romm-config via the media restore template if it's also lost).