Files
Varaverk/Rsync/Manual-Rsync.md
T
Gmer4Lfe bf3e7cc2c4 Storage-mode awareness pass + doc update for System_Essentials through Partnership
All state/data file paths in scripts and PHP now resolve via STATE_DIR / DATA_DIR /
PERSISTENT_CONF_CACHE instead of hardcoded /boot/config/ or /tmp/ paths, so the
ecosystem works in both internal and appdata storage modes.

PHP layer (watchdog.php, partnership.php, fallback.php, monitor.php, snapshot.php,
config.php): all state reads switched to STATE_DIR constant; remote state reads use
the new vv_remote_state_cmd() helper which resolves the remote's SCRIPTS_DIR via
their varaverk.cfg before building the path.

conf_sync.sh: fixed SCRIPTS_ROOT → SCRIPTS_DIR bug on MY_CONF path; added
_remote_scripts_dir() to resolve partner's SCRIPTS_DIR before SCP pull.

fallback.php page: added controls card (PARTNERSHIP_ENABLED, FALLBACK_ENABLED,
FALLBACK_RSYNC_ENABLED toggles), status grid, and settings card.

README and Manual updated for System_Essentials, Watchdogs, Fallback, Rsync,
Media, Monitors, Orchestrators, Partnership: added new scripts (conf_sync,
conf_cache_save/restore, conf_cache_watchdog, play_state_sync, start_webhook_listener,
upgrade_webhook_handler), corrected all stale /boot/config/ state file paths to
$STATE_DIR/$DATA_DIR, noted webgui/php_fpm/mover/user_scripts scripts moved to
Plugin/unraid/System_Essentials, fixed start_webhook_listener.sh header (Node.js,
not PHP -S).
2026-06-19 19:32:39 -04:00

710 lines
22 KiB
Markdown

# ━━━━━ RSYNC — Manual ━━━━━
Config reference, procedures, operational workflows.
For overview see README-Rsync.md. For per-script detail see the rsync.sh header.
This document also serves as the **initial setup guide** for standing up the
two-server ecosystem from scratch.
---
## ━━━ WHAT YOU'RE BUILDING ━━━
```
HOST1 (unRAID-Gmer4Lfe) HOST2 (unRAID-Jayred365)
────────────────────── ──────────────────────
Downloads to: Downloads to:
/mnt/user/Movies /mnt/user/Anime_Movies
/mnt/user/Tv_Shows /mnt/user/Anime_Shows
/mnt/user/Music
Both servers' arrs track ALL content across both servers.
It does not matter who downloaded what or when.
Daily 3-phase window (1am):
Phase 1 — arr_sync.sh (runs first):
All arrs on all nodes reconcile libraries using external IDs
(TMDB, TVDB, MusicBrainz). Union model — any node that tracks
an item, all nodes get it. After this phase both servers' arrs
know about all content regardless of who downloaded it.
Phase 2 — rsync (bidirectional, no --delete on media shares):
HOST1 pushes its shares ──────► HOST2 receives files
HOST1 receives files ◄────── HOST2 pushes its shares
Files arrive already tracked by the remote arr (arr_sync ran first).
No --delete — media files only spread. Arr cleanup handles deletions.
Phase 3 — arr cleanup:
sonarr_cleanup / radarr_cleanup / lidarr_cleanup query the live arr
API and remove any files no longer tracked. Emby notified after.
Note: profiled syncs (arrs_stack, emby, critical-data…) use their own
PROFILE_RSYNC_OPTS which include --delete. Only the no-profile media
shares use DEFAULT_RSYNC_OPTS (spread only).
Weekly clean sync (2:30am Sunday, containers stopped):
Emby — full clean mirror ◄──────► Emby
Critical-Data ──────► Auth stack (HOST1 → HOST2)
Every 30 minutes — dirty sync:
Emby watch states ──────► HOST2 stays current on playback
```
---
## ━━━ PREREQUISITES ━━━
Required on both servers before starting:
```
unRAID 7.x
Community Applications plugin — search "Community Applications" in unRAID plugins
User Scripts plugin — install via Community Applications
Tailscale plugin — install via Community Applications
Terminal access — unRAID UI → Tools → Terminal, or SSH
```
Optional but recommended:
```
Gitea (Docker container on HOST1) — self-hosted git for the script repository
Working Emby installation — for transcode management and fallback
```
---
## ━━━ STEP 1 — TAILSCALE ━━━
Tailscale provides the encrypted mesh network between servers. Scripts resolve the
remote server's IP via Tailscale at runtime — no hardcoded IPs, no VPN configuration,
no open ports. All server-to-server communication goes through Tailscale.
### Install on Both Servers
```
1. Open Apps in the unRAID UI
2. Search "Tailscale" — install the plugin
3. Settings → Tailscale → Connect
4. Authenticate with your Tailscale account (browser opens on your machine)
5. Verify both servers appear: https://login.tailscale.com/admin/machines
```
### Verify Connectivity
```bash
# From HOST1 — should return HOST2's 100.x.x.x Tailscale IP:
tailscale ip -4 unRAID-Jayred365
# Test actual connectivity:
tailscale ping unRAID-Jayred365
```
> **Critical:** The hostnames in `master.conf` (`HOST1` and `HOST2`) must match the
> Tailscale machine names **exactly** — case sensitive. All remote IP resolution goes
> through `tailscale ip -4 HOSTNAME`. A name mismatch causes every remote operation to
> fail at the IP resolution step.
---
## ━━━ STEP 2 — ENABLE SSH ━━━
unRAID 7.x has SSH disabled by default. Enable it on both servers.
```
Settings → Management Access → Secure Shell
SSH: Enabled
SSH port: 22
Apply
```
SSH is only exposed on your local network and Tailscale interface. No ports are
opened to the public internet.
---
## ━━━ STEP 3 — SSH KEYS ━━━
Two sets of keys needed: rsync automation keys (server-to-server) and a Gitea
access key (for script repository pulls).
### 3a — Rsync Automation Keys (ssh_setup.sh)
Run on **each server**. `ssh_setup.sh` generates the key, copies it to the remote,
and updates `host*.conf` with the key path automatically.
```bash
# On HOST1 — after the repo is cloned:
bash /boot/config/plugins/varaverk/Partnership/ssh_setup.sh
# On HOST2:
bash /boot/config/plugins/varaverk/Partnership/ssh_setup.sh
```
Key naming convention: hostname lowercased, `unraid-` prefix stripped.
```
unRAID-Gmer4Lfe → /root/.ssh/gmer4lfe_rsync_automation
unRAID-Jayred365 → /root/.ssh/jayred365_rsync_automation
```
`host*.conf` is updated automatically with `HOST*_SSH_KEY` pointing to the
generated key. Run `--status` to verify:
```bash
bash /boot/config/plugins/varaverk/Partnership/ssh_setup.sh --status
```
> **`ssh_setup.sh` is idempotent** — safe to re-run. Use `--force` to regenerate
> a key (e.g., after a security incident) and re-copy it to the remote.
### 3b — Gitea SSH Key (manual)
```bash
# On BOTH servers:
ssh-keygen -t ed25519 -f /root/.ssh/unraid_gitea -C "unraid-gitea" -N ""
# Print the public key to add to Gitea:
cat /root/.ssh/unraid_gitea.pub
# In Gitea: Settings → SSH / GPG Keys → Add Key → paste above
```
---
## ━━━ STEP 4 — CLONE THE REPOSITORY ━━━
```bash
# Create target directory:
mkdir -p /boot/config/plugins/varaverk
# Clone:
GIT_SSH_COMMAND="ssh -i /root/.ssh/unraid_gitea" \
git clone git@YOUR_GITEA_HOST:Varaverk/varaverk.git \
/boot/config/plugins/varaverk
# Make scripts executable:
find /boot/config/plugins/varaverk -name "*.sh" -exec chmod +x {} \;
```
Expected structure after clone:
```
master.conf ← all shared configuration
host1.conf ← HOST1-specific configuration
host2.conf ← HOST2-specific configuration
common.sh ← shared library
load_config.sh ← config loader
Orchestrators/
Rsync/
Fallback/
Docker_Essentials/
...
```
---
## ━━━ STEP 5 — CONFIGURE MASTER.CONF ━━━
```bash
nano /boot/config/plugins/varaverk/master.conf
```
### Host Identity
```bash
# These must match Tailscale machine names exactly — case sensitive.
HOST1="unRAID-Gmer4Lfe" # REQUIRED
HOST2="unRAID-Jayred365" # REQUIRED
# SSH keys — each server uses its own key to authenticate to the other:
HOST1_SSH_KEY="/root/.ssh/Gmer4Lfe-rsync-key"
HOST2_SSH_KEY="/root/.ssh/Jayred365-rsync-key"
```
### Git Repository
```bash
GITEA_CONTAINER="Gitea"
GITEA_REPO_PATH="Varaverk/varaverk.git"
TARGET_DIR="/boot/config/plugins/varaverk"
GITEA_SSH_KEY="/root/.ssh/unraid_gitea"
SSH_PORT=221
```
### Daily Sync Shares
```bash
# host1.conf
# Shares this server downloads to — pushed to the other server nightly.
# arr_sync.sh runs before rsync in the daily window, so the receiving
# server's arrs already track incoming files when they arrive.
# Never put the same path in both lists.
HOST1_DAILY_SYNC_SHARES=(
"/mnt/user/Movies"
"/mnt/user/Tv_Shows"
"/mnt/user/Music"
"/mnt/user/Kids_Movies"
"/mnt/user/Kids_Tv_Shows"
"/mnt/user/Sports"
"/mnt/user/stand-up_comedy"
)
# host2.conf
HOST2_DAILY_SYNC_SHARES=(
"/mnt/user/Anime_Shows"
"/mnt/user/Anime_Movies"
)
```
### Weekly Sync Shares
```bash
# master.conf
# Synced during the Sunday 2:30am window — containers stopped both sides.
WEEKLY_SYNC_SHARES=(
"/mnt/user/Media_Server/Emby"
"/mnt/user/appdata-Fallback/Critical-Data"
)
```
---
## ━━━ STEP 6 — MASTER_HOST*.CONF ━━━
`detect_hosts()` reads which server is running and aliases `HOST*_` prefixed vars
to their unprefixed names. Scripts only ever reference the unprefixed name — they
work identically on both servers.
```bash
nano /boot/config/plugins/varaverk/host1.conf # on HOST1
nano /boot/config/plugins/varaverk/host2.conf # on HOST2
```
Every variable is documented in the conf files. Key values to set:
- SSH key paths
- DAILY_SYNC_SHARES
- EMBY_URL / EMBY_API_KEY
- CERT_MONITOR_DOMAINS
- SMART_IGNORE_DRIVES
- ZFS_REPORT_IGNORE_POOLS
---
## ━━━ RSYNC PROFILES ━━━
Profiles control per-share behavior. Profile key = directory basename lowercased.
Override with `--profile=name`.
### Profile Matching
```bash
# rsync.sh /mnt/user/appdata-Fallback/Arrs_Stack
# basename: Arrs_Stack → lowercased: arrs_stack → matches [arrs_stack] profile
#
# rsync.sh /mnt/user/Movies
# basename: Movies → no profile match → global defaults (no containers stopped)
#
# rsync.sh /mnt/user/appdata-Fallback/Critical-Data --profile=critical-data
# explicit override
```
### Current Profile Definitions
```bash
# master.conf
# ── arrs_stack ───────────────────────────────────────────────────────────────
# Arr databases — stopped for clean SQLite snapshot
PROFILES["arrs_stack_CRITICAL_CONTAINER_NAMES"]=(
"Sonarr" "Radarr" "Lidarr" "Prowlarr" "Bazarr" "Pinchflat"
)
# ── critical-data ─────────────────────────────────────────────────────────────
# Auth stack — stopped for clean database snapshot, Authelia delayed restart
PROFILES["critical-data_CRITICAL_CONTAINER_NAMES"]=(
"Mariadb-Authelia" "Redis-Authelia"
"NginxProxyManager" "Lldap-Gmer4Lfe"
)
PROFILES["critical-data_DELAYED_CONTAINERS"]=(
"Authelia" "Authelia-Secondary"
)
PROFILES["critical-data_CONTAINER_DELAY"]=30
# ── important-data ────────────────────────────────────────────────────────────
# NextCloud + Postgres — stopped for clean snapshot
PROFILES["important-data_CRITICAL_CONTAINER_NAMES"]=(
"Postgres-NextCloud"
)
PROFILES["important-data_DELAYED_CONTAINERS"]=("NextCloud")
# ── emby ──────────────────────────────────────────────────────────────────────
# Weekly full clean sync — both Emby instances stopped
PROFILES["emby_CRITICAL_CONTAINER_NAMES"]=("Emby")
PROFILES["emby_EXCLUDE_DIRS"]=(
"transcodes/" "logs/" "crash*" "cache/"
)
# ── emby-fallback ─────────────────────────────────────────────────────────────
# Every 30 minutes, Emby STAYS RUNNING — dirty sync of critical state only
PROFILES["emby-fallback_CRITICAL_CONTAINER_NAMES"]=() # nothing stops
PROFILES["emby-fallback_EXCLUDE_DIRS"]=(
"*.wal" "*.shm" # unsafe mid-write
"transcodes/" "logs/" "crash*" "cache/"
)
PROFILES["emby-fallback_REMOTE_RESTART_CONTAINERS"]=("Emby")
```
### Two Emby Profiles — Why Both Exist
**emby-fallback** (every 30 minutes, Emby stays running):
- Syncs: users.db, library.db, authentication.db, config/
- Skips: \*.wal, \*.shm, transcodes/, logs/, cache/
- Why: WAL files are written while Emby runs — copying them would produce a corrupt database on HOST2
- Result: HOST2 is always within 30 minutes of HOST1 on watch state and user activity
**emby** (Sunday 2:30am, both Emby instances stopped):
- Syncs: everything except transcodes, logs, cache, crash files
- Includes: metadata, plugins, full database state, all config
- Why: WAL is checkpointed on clean shutdown — safe to copy everything
- Result: HOST2 gets a gold-standard Emby state once per week
The two profiles work together. emby-fallback keeps HOST2 current for immediate fallback.
emby gives HOST2 full fidelity once per week. Neither alone is sufficient.
---
## ━━━ PERSONAL ENCRYPTED SHARES ━━━
Personal shares sync to the remote for offsite backup. ZFS encrypts at the dataset
level — the remote receives encrypted blocks and cannot read the content without your
passphrase or keyfile.
### Create an Encrypted ZFS Dataset
```bash
# In the unRAID UI:
# Main → click your ZFS pool name → + Dataset
# Name: Gmer4Lfe-Personal
# Encryption: Enabled
# Passphrase: [your passphrase]
# Write your passphrase down — if lost, data is completely unrecoverable
# Verify encryption is active before syncing:
zfs get encryption poolname/Gmer4Lfe-Personal
# Should show: encryption aes-256-gcm
```
### Auto-Unlock on Boot (Optional)
```bash
# Keyfile approach — more convenient, but the keyfile itself is a secret
dd if=/dev/urandom bs=32 count=1 | base64 > /root/.zfs-keys/personal.key
chmod 600 /root/.zfs-keys/personal.key
zfs change-key \
-o keylocation=file:///root/.zfs-keys/personal.key \
-o keyformat=raw \
poolname/Gmer4Lfe-Personal
# Add to array_started.sh or ramdisk_setup.sh:
zfs load-key poolname/Gmer4Lfe-Personal
zfs mount poolname/Gmer4Lfe-Personal
# Manual unlock alternative (most secure):
zfs load-key poolname/Gmer4Lfe-Personal # prompts for passphrase
zfs mount poolname/Gmer4Lfe-Personal
```
### Add to host1.conf
```bash
HOST1_PERSONAL_SHARES=(
"/mnt/user/Gmer4Lfe-Personal"
)
```
---
## ━━━ STEP 7 — SCHEDULER SETUP ━━━
All scheduling is managed by the Varaverk plugin — no User Scripts entries needed.
### Array start (Varaverk disks_mounted event hook)
The Varaverk plugin fires `array_started.sh` automatically via its `disks_mounted` event
hook. It's registered in `schedule.json`:
```json
"Orchestrators/array_started.sh": { "cron": "array_start", "enabled": true }
```
Launches everything in ARRAY_START_SCRIPTS: api_key_renew, inotify_tuning,
docker_syslog_filter, php_fpm_max_children, ramdisk_setup, docker_network_connect, fallback.
Watchdogs run separately via watchdog_orchestrator.sh (cron — every 15 minutes).
### Cron Schedule (Varaverk Scheduler)
```bash
*/7 * * * * transcode_management.sh
*/15 * * * * watchdog_orchestrator.sh
*/30 * * * * critical_sync_maintenance.sh
0 */4 * * * intermediate_sync_maintenance.sh
0 1 * * * daily_sync_maintenance.sh
0 7 * * 0 sunday_morning_coffee_report.sh
30 2 * * 0 weekly_sync_maintenance.sh
0 0 15 * * monthly_maintenance.sh
```
All entries are configured via the Varaverk scheduler UI or `schedule.json`.
Scripts live at `/boot/config/plugins/varaverk/`.
---
## ━━━ VERIFICATION ━━━
Test with `--dry-run` first — all pre-flight checks run, no changes made.
### Test a Single Profile Sync
```bash
bash /boot/config/plugins/varaverk/Rsync/rsync.sh \
/mnt/user/appdata-Fallback/Arrs_Stack --dry-run --log
```
Expected output (healthy):
```
━━━ Setup ━━━
Host: HOST1 (unRAID-Gmer4Lfe) → HOST2 (unRAID-Jayred365)
Remote IP: 100.x.x.x
Profile: arrs_stack
━━━ Pre-flight ━━━
Remote reachable
version parity — both on unRAID X.Y.Z
Remote Docker daemon responding
Remote rootfs: 12% (threshold: 75%)
Remote share exists and not empty
All pre-flight checks passed
```
### Test the Daily Orchestrator
```bash
bash /boot/config/plugins/varaverk/Orchestrators/daily_sync_maintenance.sh --dry-run
```
### Check Configuration Resolution
```bash
bash /boot/config/plugins/varaverk/Rsync/rsync.sh \
/mnt/user/appdata-Fallback/Arrs_Stack --status
```
---
## ━━━ INITIAL HOST2 SYNC ━━━
If HOST2 is being set up from scratch with empty shares:
```bash
# Create share directories on HOST2:
bash /boot/config/plugins/varaverk/Tools/recreate_shares.sh
# Initial push from HOST1 — first run may take several hours for large libraries:
bash /boot/config/plugins/varaverk/Orchestrators/daily_sync_maintenance.sh --log
```
The scheduled nightly sync will be incremental after the initial push.
---
## ━━━ NAMING CONSISTENCY REQUIREMENT ━━━
The ecosystem uses one codebase on both servers. This only works if containers and
shares have identical names on both servers. **This is not configurable — it is a
design requirement.**
```
Container names must match exactly on both servers:
"Emby" ← both HOST1 and HOST2
"NginxProxyManager" ← both HOST1 and HOST2
"Mariadb-Authelia" ← both HOST1 and HOST2
Share paths must match exactly on both servers:
/mnt/user/Movies ← both HOST1 and HOST2
/mnt/user/Tv_Shows ← both HOST1 and HOST2
```
If a container has a different name on one server: the script skips it silently.
You only notice when the container is not stopped during a sync that requires it.
If a share has a different path: rsync.sh aborts with "remote share missing."
Easier to catch — but still requires renaming the share to fix.
---
## ━━━ TROUBLESHOOTING ━━━
### SSH Connection Refused / Timeout
```bash
# Verify SSH is enabled on the remote:
# Settings → Management Access → Secure Shell → Enabled
# Verify Tailscale is connected:
tailscale ip -4 unRAID-Jayred365
# Test SSH manually (key path from --status output):
ssh -i /root/.ssh/gmer4lfe_rsync_automation \
root@$(tailscale ip -4 unRAID-Jayred365) "hostname"
# If password prompted: key not authorised — re-run ssh_setup.sh
# Re-run setup (idempotent, re-copies key to remote):
bash /boot/config/plugins/varaverk/Partnership/ssh_setup.sh
```
### Pre-flight Aborts on Remote Rootfs
```bash
# Check current usage on remote:
ssh -i /root/.ssh/Gmer4Lfe-rsync-key root@[HOST2-ip] "df /"
# Common cause: array not started, drives not mounted
```
### Remote Share Missing
```bash
# Verify the share exists on HOST2:
ssh -i /root/.ssh/Gmer4Lfe-rsync-key root@[HOST2-ip] "ls /mnt/user/"
# If missing: create the share on HOST2, then run initial sync
```
### Containers Not Stopping / Starting
```bash
# Verify container names match Docker exactly — case sensitive:
docker ps --format "{{.Names}}"
# Test remote:
ssh -i /root/.ssh/Gmer4Lfe-rsync-key root@[HOST2-ip] "docker ps --format '{{.Names}}'"
```
### Profile Not Matching
```bash
# Profile key = directory basename lowercased
# /mnt/user/appdata-Fallback/Arrs_Stack → key: arrs_stack
# Override explicitly:
bash rsync.sh /mnt/user/appdata-Fallback/My_Stuff --profile=arrs_stack
# Verify what profile resolved:
bash rsync.sh /mnt/user/appdata-Fallback/Arrs_Stack --status
```
---
## ━━━ FULL CONFIGURATION REFERENCE ━━━
### master.conf
```bash
# Rsync engine
RSYNC_ENABLED=true
DEFAULT_RSYNC_OPTS="-az --no-perms --no-owner --no-group --inplace"
# No --delete in DEFAULT_RSYNC_OPTS — media share rsync is additive only.
# arr_sync.sh keeps all arr databases in union — either server can download anything.
# arr cleanup handles deletions (orphans only). Profiles use --delete explicitly.
BW_LIMIT=0 # KB/s, 0 = unlimited
RETRY_COUNT=3
SLEEP=60 # seconds between retries
ROOTFS_WARN_PCT=75 # abort if remote rootfs above this %
# Shared with Monitors/
BANDWIDTH_LOG="$DATA_DIR/bandwidth_history.db"
BANDWIDTH_LOG_RETENTION=90
BANDWIDTH_WARN_GB=50
# Profile definitions (see RSYNC PROFILES section above)
declare -A PROFILES
declare -A PROFILE_BW_LIMIT
declare -A PROFILE_RETRY_COUNT
declare -A PROFILE_SLEEP
declare -A PROFILE_CONTAINER_DELAY
declare -A PROFILE_CRITICAL_CONTAINER_NAMES
declare -A PROFILE_DELAYED_CONTAINERS
declare -A PROFILE_EXCLUDE_DIRS
declare -A PROFILE_REMOTE_RESTART_CONTAINERS
```
### host*.conf
```bash
# Daily sync shares — one list per server (mutually exclusive)
HOST1_DAILY_SYNC_SHARES=(...)
HOST2_DAILY_SYNC_SHARES=(...)
# Personal encrypted shares
HOST1_PERSONAL_SHARES=(...)
# SSH key for this server to authenticate to the remote
# Set automatically by ssh_setup.sh — do not edit manually
HOST1_SSH_KEY="/root/.ssh/gmer4lfe_rsync_automation"
HOST2_SSH_KEY="/root/.ssh/jayred365_rsync_automation"
```
---
## ━━━ OUTPUT TIERS ━━━
Without `--log`, each run shows: section headers (Pre-flight, Transfer, Stop/Start
Containers), the transfer identity block (source, remote, profile, identity), a
progress indicator while rsync is running, and a summary block with duration,
bytes transferred, and status. Warnings and errors are always visible.
With `--log`, every decision is shown — pre-flight check results, profile resolution,
per-container stop/start state, retry details, and bandwidth log confirmation.
Use for first-run validation or when investigating unexpected behavior.
---
## ━━━ FLAG REFERENCE ━━━
All flags work on `rsync.sh` and all orchestrators.
### --dry-run
Runs all pre-flight checks. Shows what rsync would transfer. No transfer, no container
stops, no bandwidth log entry. Safe to run at any time.
```bash
rsync.sh /mnt/user/Movies --dry-run
rsync.sh /mnt/user/appdata-Fallback/Arrs_Stack --dry-run --log
daily_sync_maintenance.sh --dry-run
```
### --status
Shows resolved configuration — profile, remote identity, all vars that would be
used — then exits. No pre-flight checks, no rsync. Use to verify configuration
loaded correctly.
```bash
rsync.sh /mnt/user/appdata-Fallback/Arrs_Stack --status
```
### --log
Verbose output throughout. Every decision, every container operation, every rsync
progress line. Use for first-time runs or when investigating issues.
### --profile=name
Override profile selection. Bypasses basename inference. Use when the directory
name doesn't match any profile key, or when testing a specific profile.
```bash
rsync.sh /mnt/user/appdata-Fallback/Critical-Data --profile=critical-data
rsync.sh /mnt/user/Media_Server/Emby --profile=emby-fallback
```