diff --git a/Netgrimoire/Pocket/Deployment_Guide.md b/Netgrimoire/Pocket/Deployment_Guide.md new file mode 100644 index 0000000..8d4c413 --- /dev/null +++ b/Netgrimoire/Pocket/Deployment_Guide.md @@ -0,0 +1,1730 @@ +--- +title: Pocket Grimoire +description: Deployment Guide +published: true +date: 2026-02-20T04:41:35.122Z +tags: +editor: markdown +dateCreated: 2026-02-20T04:41:35.122Z +--- + +# Pocket Grimoire - Complete Deployment Guide + +**Portable, Encrypted, Offline-Capable Media Server and Documentation Reference** + +--- + +## Overview + +Pocket Grimoire is a portable companion to the Netgrimoire homelab, providing offline access to: +- Documentation and reference material (Wiki.js) +- Personal and family media libraries (Jellyfin) +- Photos, documents, and backups (encrypted vault) +- Automatic synchronization with Netgrimoire when connected + +**Design Philosophy:** +- Calm and predictable +- Encrypted at rest +- Offline-first operation +- Automatic synchronization +- One wall plug +- No cloud dependencies +- Minimal services (media + docs only, no gaming) + +--- + +## Hardware Inventory + +### Core Compute +- Raspberry Pi 4 (8GB) +- Passive heatsink case or low-noise fan case +- Official Raspberry Pi 4 Power Supply OR quality 3A USB-A to USB-C cable +- Spare MicroSD card (32GB+, for OS recovery) +- USB card reader (for flashing Pi images) + +### Storage (3 SSDs, 2 Active at a Time) +- **SSD #1 – VAULT** (1-2TB, encrypted, always connected) + - Wiki.js data, PostgreSQL, git repos, photos, documents, backups +- **SSD #2 – MEDIA-PERSONAL** (2TB+, encrypted) + - Your curated H.264/AAC movies and TV shows + - Connected for personal trips +- **SSD #3 – MEDIA-FAMILY** (2TB+, unencrypted) + - Family-friendly movies and TV shows (H.264/AAC) + - Connected for family visits + - Shareable/portable to other devices without Pocket Grimoire +- **USB drive – ISO/Rebuild** (64GB+, labeled, write-protected) +- **USB drive – Data Transfer** (128GB+, labeled) + +### Networking +- GL.iNet Beryl AX (GL-MT3000) travel router +- Short CAT5/6 Ethernet cable (6-12 inch, Pi ↔ Router) +- USB Ethernet adapter (backup/emergency) + +### Power +- Anker Prime 200W 6-Port GaN Charging Station (Model A2683) +- Short USB-A to USB-C cable (3A-rated, 6-12 inch, for Pi) +- Short USB-A to USB-A cable (6-12 inch, for Vault SSD) +- 2× USB-C to USB-C cables (6ft, 100W with E-Marker chip, for laptop/phone) + +### Media Players +- 2× Onn 4K streaming boxes with power supplies +- 2× HDMI cables +- Mini wireless keyboard (for Onn boxes and emergency Pi access) + +### Cables & Accessories +- Micro-HDMI to HDMI cable (Pi emergency console access) +- HDMI extender (if hotel TV ports are difficult to reach) + +### Organization & Emergency +- Carry case for complete kit +- Cable organizer pouch (separate from main case) +- Velcro cable ties (pack of 20) +- Labels for SSDs (VAULT, MEDIA-PERSONAL, MEDIA-FAMILY) +- Small flashlight or headlamp +- Small screwdriver or multitool (accessing hotel TV ports) + +--- + +## Power Configuration + +### Anker Prime A2683 Port Assignments + +``` +USB-C1 (retractable) → GL.iNet Beryl AX (12W) +USB-C2 (100W PD) → Laptop charging (65-90W) +USB-C3 → Phone charging (20-30W) +USB-C4 → Tablet/spare (optional) +USB-A1 (5V/3A) → Raspberry Pi 4 (15W) +USB-A2 (5V/3A) → Vault SSD (always connected, 5W) + +AC Outlet 1 → Spare +AC Outlet 2 → Spare +``` + +### Raspberry Pi USB Ports + +``` +USB 3.0 Port 1 → Media SSD (personal or family, rotated) +USB 3.0 Port 2 → Spare (emergency USB, data transfer) +USB 2.0 Port 1 → Spare (wireless keyboard dongle if needed) +USB 2.0 Port 2 → Spare +``` + +### Power Budget + +``` +Component Power Draw Running Total +───────────────────────────────────────────────── +Raspberry Pi 4 15W 15W +Beryl AX 12W 27W +Vault SSD 5W 32W +Media SSD (via Pi) 5W 37W +Laptop (charging) 65W 102W +Phone (charging) 20W 122W +───────────────────────────────────────────────── +Total 122W / 200W +Headroom: 78W +``` + +--- + +## Software Stack + +### Host OS & Services (Native) + +**Operating System:** +- Raspberry Pi OS Lite 64-bit (headless, no desktop environment) +- Alternative: Ubuntu Server 22.04 LTS ARM64 + +**Storage & Filesystems:** +- ZFS (OpenZFS) - Encrypted pools + - `vaultpg` pool (Vault SSD, always connected) + - `mediapg` pool (Media SSDs, rotated personal/family) + - Native ZFS encryption with passphrase unlock + - ARC memory capped (512MB-1GB maximum) + +**File Sharing:** +- NFS server (host-level, not containerized) + - Exports `/srv/mediapg` to LAN (read-only) + - Laptop and Onn boxes mount for media access + +**Sync & Automation:** +- systemd timers (scheduled jobs every 6 hours) + - ZFS replication from Netgrimoire via syncoid + - Git pulls for wiki/docs repositories + - ntfy failure notifications + +**Networking:** +- Standard Linux networking +- Docker and Docker Compose + +### Docker Containers + +**Required Stack:** + +1. **Wiki.js** - Documentation mirror + - Read-only wiki pulling from Forgejo + - Git backend with SSH deploy key (read-only) + - Works fully offline after sync + - Port: 3000 + +2. **PostgreSQL** - Wiki.js database backend + - Stored on Vault SSD + - Tuned for 16K recordsize (Postgres optimal) + +3. **Jellyfin** - Media server + - Direct play ONLY (transcoding disabled) + - Serves H.264/AAC pre-encoded media + - Accessible from Onn boxes and laptop + - Port: 8096 + +**Optional Containers:** + +4. **File Browser** - Read-only web UI + - Quick LAN access to vault/media without SSH + - Port: 8080 + +5. **Dozzle** - Container log viewer + - Simple Docker log viewer for debugging + - Port: 9999 + +--- + +## Network Architecture + +### Operating Modes + +**Home Base (Netgrimoire LAN):** +- Direct LAN connectivity +- VPN not required +- Fast local synchronization +- All services accessible + +**Travel (Online):** +- All traffic routed via WireGuard VPN to Netgrimoire +- DNS and ad blocking handled by Beryl AX router +- Primary DNS: Netgrimoire (via VPN) +- Fallback DNS: Public resolvers + +**Travel (Offline):** +- Full local access to all services +- Wiki, files, and media available +- No synchronization until connectivity returns +- DNS handled locally by router + +### Router Configuration (Beryl AX) + +**DNS & Ad Blocking:** +- AdGuard Home enabled on router +- Acts as primary DNS for all clients +- Blocks ads and trackers network-wide + +**DNS Behavior:** +- Primary DNS: Netgrimoire (via VPN when available) +- Fallback DNS: Public resolvers (1.1.1.1, 9.9.9.9) +- Local DNS entries: + - `pocket-grimoire.local` + - `wiki.pocket-grimoire.local` + - `media.pocket-grimoire.local` + +**VPN Behavior:** +- WireGuard client configured +- When VPN available: All traffic tunneled to Netgrimoire +- When VPN unavailable: Normal WAN routing + +--- + +## Directory Structure + +``` +/srv/pocket-grimoire/ # Main application root (on Vault SSD) +├── stacks/ # Docker Compose files +│ ├── wikijs/ +│ │ ├── docker-compose.yml +│ │ └── .env +│ ├── jellyfin/ +│ │ ├── docker-compose.yml +│ │ └── .env +│ └── filebrowser/ # Optional +│ └── docker-compose.yml +├── data/ # Persistent container data +│ ├── postgres/ # PostgreSQL data +│ ├── wikijs/ # Wiki.js data +│ ├── jellyfin/ # Jellyfin metadata/config +│ └── filebrowser/ # File browser config +├── repos/ # Git repository mirrors +│ └── wiki/ # Wiki content from Forgejo +└── keys/ # SSH keys + ├── forgejo_wiki_ro # Read-only wiki deploy key + └── zfs_pull_ro # ZFS replication key + +/srv/vaultpg/ # Vault SSD ZFS mount +└── (mirrors from Netgrimoire) + +/srv/mediapg/ # Media SSD ZFS mount (rotated) +└── library/ # H.264 encoded media + ├── movies/ + └── tv/ + +/usr/local/sbin/ # System scripts +├── pocketgrimoire-sync.sh # Main sync script +└── pocketgrimoire-zfs-pull.sh # ZFS replication script + +/etc/ # Config files +├── pocketgrimoire-sync.env # Secrets (ntfy tokens) +├── exports # NFS exports +└── systemd/system/ + ├── pocketgrimoire-sync.service + └── pocketgrimoire-sync.timer +``` + +--- + +## Installation Instructions + +### 1. Base OS Installation + +**Download Raspberry Pi OS:** +```bash +# On your laptop +# Download Raspberry Pi OS Lite (64-bit) from raspberrypi.com +# Use Raspberry Pi Imager to flash to MicroSD card + +# Configure: +# - Hostname: pocket-grimoire +# - Enable SSH +# - Set username/password +# - Configure WiFi (for initial setup only) +``` + +**First Boot:** +```bash +# SSH into Pi +ssh user@pocket-grimoire.local + +# Update system +sudo apt update && sudo apt upgrade -y + +# Set timezone +sudo timedatectl set-timezone America/Chicago + +# Configure locale +sudo raspi-config +# System Options → Locale → en_US.UTF-8 +``` + +### 2. Install ZFS + +```bash +# Install ZFS utilities +sudo apt install -y zfsutils-linux + +# Verify ZFS is working +sudo zpool list +``` + +### 3. Configure ZFS Pools + +**Important:** Replace `/dev/sdX` with your actual device identifiers. Use `lsblk` to identify drives. + +**Create Vault Pool (Always Connected):** +```bash +# Identify Vault SSD +lsblk + +# Create encrypted ZFS pool +sudo zpool create -o ashift=12 \ + -O encryption=on \ + -O keylocation=prompt \ + -O keyformat=passphrase \ + -O compression=lz4 \ + -O atime=off \ + -O recordsize=1M \ + -m /srv/vaultpg \ + vaultpg /dev/sdX + +# Create datasets +sudo zfs create -o recordsize=16K vaultpg/wiki-pg # PostgreSQL +sudo zfs create vaultpg/repos # Git repos +sudo zfs create vaultpg/pocket-grimoire # App data +``` + +**Create Media Pool (Rotated):** +```bash +# For Personal Media SSD +sudo zpool create -o ashift=12 \ + -O encryption=on \ + -O keylocation=prompt \ + -O keyformat=passphrase \ + -O compression=lz4 \ + -O atime=off \ + -O recordsize=1M \ + -m /srv/mediapg \ + mediapg /dev/sdY + +sudo zfs create mediapg/library +sudo zfs create mediapg/library/movies +sudo zfs create mediapg/library/tv + +# For Family Media SSD (unencrypted) +# When swapping drives: +sudo zpool export mediapg +# Connect family SSD +sudo zpool create -o ashift=12 \ + -O compression=lz4 \ + -O atime=off \ + -O recordsize=1M \ + -m /srv/mediapg \ + mediapg /dev/sdY +``` + +**Cap ZFS ARC Memory:** +```bash +# Create /etc/modprobe.d/zfs.conf +sudo nano /etc/modprobe.d/zfs.conf + +# Add this line (for 8GB Pi, cap at 1GB): +options zfs zfs_arc_max=1073741824 + +# Reboot to apply +sudo reboot +``` + +**Configure ZFS to Wait for Passphrase on Boot:** +```bash +# Edit /etc/systemd/system/zfs-load-key.service +sudo nano /etc/systemd/system/zfs-load-key.service +``` + +Add: +```ini +[Unit] +Description=Load ZFS encryption keys +Before=zfs-mount.service +After=zfs-import.target + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart=/sbin/zfs load-key -a + +[Install] +WantedBy=zfs-mount.service +``` + +Enable: +```bash +sudo systemctl daemon-reload +sudo systemctl enable zfs-load-key.service +``` + +### 4. Install Docker + +```bash +# Install Docker +sudo apt install -y docker.io docker-compose + +# Add user to docker group +sudo usermod -aG docker $USER + +# Enable Docker service +sudo systemctl enable docker +sudo systemctl start docker + +# Log out and back in for group changes +exit +# SSH back in +``` + +### 5. Install NFS Server + +```bash +# Install NFS server +sudo apt install -y nfs-kernel-server + +# Configure exports +sudo nano /etc/exports +``` + +Add: +``` +/srv/mediapg 10.0.0.0/24(ro,fsid=10,async,no_subtree_check) +``` + +Apply: +```bash +sudo exportfs -ra +sudo systemctl restart nfs-server +sudo systemctl enable nfs-server + +# Verify +sudo exportfs -v +``` + +### 6. Install System Packages + +```bash +# Core utilities +sudo apt install -y \ + curl \ + git \ + htop \ + ncdu \ + smartmontools \ + sanoid + +# For ntfy notifications +sudo apt install -y curl +``` + +--- + +## Docker Configuration + +### Wiki.js Stack + +**Create directory structure:** +```bash +mkdir -p /srv/pocket-grimoire/stacks/wikijs +mkdir -p /srv/pocket-grimoire/data/postgres +mkdir -p /srv/pocket-grimoire/data/wikijs +mkdir -p /srv/pocket-grimoire/repos/wiki +mkdir -p /srv/pocket-grimoire/keys +``` + +**Create environment file:** +```bash +nano /srv/pocket-grimoire/stacks/wikijs/.env +``` + +```env +TZ=America/Chicago + +PUID=1000 +PGID=1000 + +POSTGRES_DB=wikijs +POSTGRES_USER=wikijs +POSTGRES_PASSWORD=CHANGE_ME_LONG_RANDOM_PASSWORD + +WIKI_PORT=3000 +COMPOSE_PROJECT_NAME=pocketgrimoire_wikijs +``` + +**Create Docker Compose file:** +```bash +nano /srv/pocket-grimoire/stacks/wikijs/docker-compose.yml +``` + +```yaml +services: + db: + image: postgres:16-alpine + container_name: pocketgrimoire_db + environment: + TZ: ${TZ} + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + volumes: + - /srv/pocket-grimoire/data/postgres:/var/lib/postgresql/data + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] + interval: 10s + timeout: 5s + retries: 10 + + wikijs: + image: requarks/wiki:2 + container_name: pocketgrimoire_wikijs + depends_on: + db: + condition: service_healthy + environment: + TZ: ${TZ} + DB_TYPE: postgres + DB_HOST: db + DB_PORT: 5432 + DB_USER: ${POSTGRES_USER} + DB_PASS: ${POSTGRES_PASSWORD} + DB_NAME: ${POSTGRES_DB} + ports: + - "${WIKI_PORT}:3000" + volumes: + - /srv/pocket-grimoire/repos:/repos + restart: unless-stopped +``` + +**Start Wiki.js:** +```bash +cd /srv/pocket-grimoire/stacks/wikijs +docker compose up -d +``` + +**Access Wiki.js:** +- Open browser: `http://pocket-grimoire.local:3000` +- Complete initial setup +- Configure as read-only (see Wiki.js Configuration section below) + +### Jellyfin Stack + +**Create directory structure:** +```bash +mkdir -p /srv/pocket-grimoire/stacks/jellyfin +mkdir -p /srv/pocket-grimoire/data/jellyfin/config +mkdir -p /srv/pocket-grimoire/data/jellyfin/cache +``` + +**Create environment file:** +```bash +nano /srv/pocket-grimoire/stacks/jellyfin/.env +``` + +```env +TZ=America/Chicago +PUID=1000 +PGID=1000 +JELLYFIN_PORT=8096 +COMPOSE_PROJECT_NAME=pocketgrimoire_jellyfin +``` + +**Create Docker Compose file:** +```bash +nano /srv/pocket-grimoire/stacks/jellyfin/docker-compose.yml +``` + +```yaml +services: + jellyfin: + image: jellyfin/jellyfin:latest + container_name: pocketgrimoire_jellyfin + user: "${PUID}:${PGID}" + environment: + - TZ=${TZ} + volumes: + - /srv/pocket-grimoire/data/jellyfin/config:/config + - /srv/pocket-grimoire/data/jellyfin/cache:/cache + - /srv/mediapg:/media:ro + ports: + - "${JELLYFIN_PORT}:8096" + restart: unless-stopped +``` + +**Start Jellyfin:** +```bash +cd /srv/pocket-grimoire/stacks/jellyfin +docker compose up -d +``` + +**Access Jellyfin:** +- Open browser: `http://pocket-grimoire.local:8096` +- Complete initial setup +- Add media library: `/media/library` +- Configure for direct play only (see Jellyfin Configuration section below) + +### Optional: File Browser + +**Create directory structure:** +```bash +mkdir -p /srv/pocket-grimoire/stacks/filebrowser +mkdir -p /srv/pocket-grimoire/data/filebrowser +``` + +**Create Docker Compose file:** +```bash +nano /srv/pocket-grimoire/stacks/filebrowser/docker-compose.yml +``` + +```yaml +services: + filebrowser: + image: filebrowser/filebrowser:s6 + container_name: pocketgrimoire_filebrowser + ports: + - "8080:80" + volumes: + - /srv/pocket-grimoire/data/filebrowser:/database + - /srv/pocket-grimoire/data/filebrowser:/config + - /srv/vaultpg:/vault:ro + - /srv/mediapg:/media:ro + restart: unless-stopped +``` + +**Start File Browser:** +```bash +cd /srv/pocket-grimoire/stacks/filebrowser +docker compose up -d +``` + +**Access File Browser:** +- Open browser: `http://pocket-grimoire.local:8080` +- Default login: `admin` / `admin` +- Change password immediately +- Configure as read-only in settings + +### Optional: Dozzle (Container Logs) + +**Create Docker Compose file:** +```bash +mkdir -p /srv/pocket-grimoire/stacks/dozzle +nano /srv/pocket-grimoire/stacks/dozzle/docker-compose.yml +``` + +```yaml +services: + dozzle: + image: amir20/dozzle:latest + container_name: pocketgrimoire_dozzle + ports: + - "9999:8080" + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + restart: unless-stopped +``` + +**Start Dozzle:** +```bash +cd /srv/pocket-grimoire/stacks/dozzle +docker compose up -d +``` + +--- + +## Service Configuration + +### Wiki.js Configuration + +**After initial setup, configure read-only mode:** + +1. **Disable User Registration:** + - Administration → Users → Settings + - Disable "Allow self-registration" + +2. **Configure Read-Only Permissions:** + - Administration → Groups + - Edit "Guests" group (or create "Readers" group) + - Permissions: + - ✓ Read pages + - ✗ Create pages + - ✗ Edit pages + - ✗ Delete pages + - ✗ Upload files + +3. **Configure Git Storage:** + - Administration → Storage → Git + - **Setup:** + - Remote: `git@your-forgejo-host:username/wiki-content.git` + - Authentication: SSH (deploy key) + - Sync Direction: Pull/Import only + - Branch: `main` + +4. **Generate SSH Deploy Key:** + ```bash + mkdir -p /srv/pocket-grimoire/keys + ssh-keygen -t ed25519 -f /srv/pocket-grimoire/keys/forgejo_wiki_ro -N "" + chmod 600 /srv/pocket-grimoire/keys/forgejo_wiki_ro + cat /srv/pocket-grimoire/keys/forgejo_wiki_ro.pub + ``` + +5. **Add Deploy Key to Forgejo:** + - Copy public key + - Forgejo → Repository → Settings → Deploy Keys + - Add key (read-only access) + +6. **Import Content:** + - Administration → Storage → Git + - Click "Import Content" or "Force Sync" + - Verify pages appear + +### Jellyfin Configuration + +**Critical: Disable All Transcoding** + +1. **Dashboard → Playback:** + - ✓ Prefer Direct Play + - ✓ Prefer Direct Stream + - ✗ Allow video transcoding (DISABLE THIS) + - ✗ Allow audio transcoding when supported (DISABLE THIS) + - ✗ Hardware acceleration: None (not needed) + +2. **Dashboard → Libraries:** + - Add Library: `/media/library/movies` + - Content type: Movies + - Add Library: `/media/library/tv` + - Content type: TV Shows + +3. **Dashboard → Networking:** + - Published Server URL: `http://pocket-grimoire.local:8096` + - Enable automatic port mapping: No + +4. **Dashboard → Scheduled Tasks:** + - Disable aggressive scanning + - Scan library: Manual only (or daily at most) + +**Verify Direct Play:** +- Play a movie +- During playback: Click info icon +- Verify: "Direct Play" (NOT "Transcoding" or "Direct Stream with Transcode") +- If transcoding appears: Media is not properly encoded + +--- + +## Synchronization Configuration + +### Create ntfy Environment File + +```bash +sudo nano /etc/pocketgrimoire-sync.env +``` + +```bash +NTFY_URL="https://ntfy.YOUR_DOMAIN/pocket-grimoire" +NTFY_TOKEN="YOUR_NTFY_TOKEN_HERE" # Optional +HOSTNAME_TAG="$(hostname -s)" +``` + +```bash +sudo chmod 600 /etc/pocketgrimoire-sync.env +``` + +### Create Main Sync Script + +```bash +sudo nano /usr/local/sbin/pocketgrimoire-sync.sh +``` + +```bash +#!/usr/bin/env bash +set -euo pipefail + +ENV_FILE="/etc/pocketgrimoire-sync.env" +LOG="/var/log/pocketgrimoire-sync.log" +LOCK="/run/pocketgrimoire-sync.lock" +STATE_DIR="/var/lib/pocketgrimoire" +FAIL_FLAG="${STATE_DIR}/last_run_failed" + +mkdir -p "$STATE_DIR" +touch "$LOG" +chmod 640 "$LOG" + +# shellcheck disable=SC1090 +source "$ENV_FILE" + +notify_ntfy() { + local title="$1" + local msg="$2" + local priority="${3:-default}" + local tags="${4:-warning}" + + local auth=() + if [[ -n "${NTFY_TOKEN:-}" ]]; then + auth=(-H "Authorization: Bearer ${NTFY_TOKEN}") + fi + + curl -fsS -X POST "${NTFY_URL}" \ + "${auth[@]}" \ + -H "Title: ${title}" \ + -H "Priority: ${priority}" \ + -H "Tags: ${tags}" \ + -d "${msg}" >/dev/null 2>&1 || true +} + +# Prevent overlapping runs +exec 9>"$LOCK" +if ! flock -n 9; then + echo "$(date -Is) sync already running, exiting" >> "$LOG" + exit 0 +fi + +run_or_fail() { + local label="$1"; shift + echo "$(date -Is) --- ${label} START ---" >> "$LOG" + if "$@" >> "$LOG" 2>&1; then + echo "$(date -Is) --- ${label} OK ---" >> "$LOG" + return 0 + else + local rc=$? + echo "$(date -Is) --- ${label} FAIL (rc=${rc}) ---" >> "$LOG" + return $rc + fi +} + +main() { + echo "$(date -Is) ===== Pocket Grimoire sync start =====" >> "$LOG" + + # 1) ZFS replication pull + # Placeholder - configure after setting up ZFS replication + # run_or_fail "ZFS pull" /usr/local/sbin/pocketgrimoire-zfs-pull.sh + echo "$(date -Is) ZFS pull: placeholder (configure syncoid)" >> "$LOG" + + # 2) Git pull for wiki content + REPO_DIR="/srv/pocket-grimoire/repos/wiki" + BRANCH="main" + + if [[ -d "${REPO_DIR}/.git" ]]; then + run_or_fail "Git fetch (wiki)" git -C "$REPO_DIR" fetch --all --prune + run_or_fail "Git reset (wiki)" git -C "$REPO_DIR" reset --hard "origin/${BRANCH}" + else + echo "$(date -Is) WARNING: ${REPO_DIR} is not a git repo" >> "$LOG" + fi + + echo "$(date -Is) ===== Pocket Grimoire sync end =====" >> "$LOG" + + # If previously failing, send recovery notice + if [[ -f "$FAIL_FLAG" ]]; then + rm -f "$FAIL_FLAG" + notify_ntfy \ + "Pocket Grimoire sync recovered (${HOSTNAME_TAG})" \ + "Sync is healthy again. Last run succeeded at $(date -Is)." \ + "low" \ + "white_check_mark" + fi +} + +# Trap errors to notify +on_error() { + local rc=$? + touch "$FAIL_FLAG" + + local tail_txt + tail_txt="$(tail -n 60 "$LOG" 2>/dev/null || true)" + + notify_ntfy \ + "Pocket Grimoire sync FAILED (${HOSTNAME_TAG})" \ + "Return code: ${rc}\nTime: $(date -Is)\n\nLast log lines:\n${tail_txt}" \ + "high" \ + "rotating_light" + + exit $rc +} +trap on_error ERR + +main +``` + +```bash +sudo chmod +x /usr/local/sbin/pocketgrimoire-sync.sh +sudo mkdir -p /var/lib/pocketgrimoire +``` + +### Create ZFS Replication Script + +```bash +sudo nano /usr/local/sbin/pocketgrimoire-zfs-pull.sh +``` + +```bash +#!/usr/bin/env bash +set -euo pipefail + +# Configuration - UPDATE THESE +SRC_HOST="netgrimoire.example.lan" +SRC_DATASET="vault/source_dataset" +DST_DATASET="vaultpg/mirror_dataset" +SSH_KEY="/srv/pocket-grimoire/keys/zfs_pull_ro" + +# Run syncoid for incremental replication +syncoid --no-sync-snap --recursive \ + --sshkey "${SSH_KEY}" \ + "root@${SRC_HOST}:${SRC_DATASET}" \ + "${DST_DATASET}" +``` + +```bash +sudo chmod +x /usr/local/sbin/pocketgrimoire-zfs-pull.sh +``` + +**Note:** Configure SSH keys and ZFS send/receive permissions on Netgrimoire before enabling this script. + +### Create systemd Service + +```bash +sudo nano /etc/systemd/system/pocketgrimoire-sync.service +``` + +```ini +[Unit] +Description=Pocket Grimoire periodic sync (ZFS + Git) with ntfy alerts +After=network-online.target +Wants=network-online.target + +[Service] +Type=oneshot +ExecStart=/usr/local/sbin/pocketgrimoire-sync.sh +``` + +### Create systemd Timer + +```bash +sudo nano /etc/systemd/system/pocketgrimoire-sync.timer +``` + +```ini +[Unit] +Description=Run Pocket Grimoire sync every 6 hours + +[Timer] +OnBootSec=10min +OnUnitActiveSec=6h +Persistent=true + +[Install] +WantedBy=timers.target +``` + +### Enable and Start Timer + +```bash +sudo systemctl daemon-reload +sudo systemctl enable pocketgrimoire-sync.timer +sudo systemctl start pocketgrimoire-sync.timer + +# Verify timer is active +systemctl list-timers | grep pocketgrimoire + +# Check timer status +systemctl status pocketgrimoire-sync.timer + +# View sync logs +tail -f /var/log/pocketgrimoire-sync.log + +# Manually trigger sync (for testing) +sudo systemctl start pocketgrimoire-sync.service +``` + +--- + +## Media Encoding Requirements + +**All media MUST be encoded to these specifications for direct play:** + +### Video Codec +- **Codec:** H.264 (AVC) +- **Profile:** High +- **Level:** 4.1 +- **Bit depth:** 8-bit +- **Pixel format:** yuv420p +- **Container:** MKV or MP4 + +### Audio Codec +- **Primary:** AAC 2.0 (stereo, 192 kbps) +- **Optional:** AC3 5.1 (surround, if needed) +- **Avoid:** DTS, DTS-HD, TrueHD (these force audio transcoding) + +### Subtitles +- **Format:** SRT (SubRip Text) only +- **Avoid:** PGS/VobSub (image-based subs force video transcoding) +- **Location:** External .srt files or embedded in MKV + +### FFmpeg Encoding Command + +**Single file:** +```bash +ffmpeg -i input.mkv \ + -map 0:v:0 -map 0:a:0 -map 0:s? \ + -c:v libx264 -preset slow -crf 20 \ + -profile:v high -level 4.1 -pix_fmt yuv420p \ + -c:a aac -b:a 192k \ + -c:s srt \ + output.mkv +``` + +**CRF Quality Guide:** +- **18** - Near-lossless (large files, ~8-12 GB per movie) +- **20** - Excellent quality (recommended, ~4-6 GB per movie) +- **22** - Good quality (smaller files, ~3-4 GB per movie) + +**Batch encode directory:** +```bash +#!/bin/bash +for f in *.mkv; do + ffmpeg -i "$f" \ + -c:v libx264 -preset slow -crf 20 \ + -profile:v high -level 4.1 -pix_fmt yuv420p \ + -c:a aac -b:a 192k \ + -c:s srt \ + "${f%.mkv}.h264.mkv" +done +``` + +**Check existing media codec:** +```bash +ffprobe input.mkv 2>&1 | grep -E "Video:|Audio:" +``` + +**Verify direct play compatibility:** +```bash +# After encoding, verify: +ffprobe output.mkv 2>&1 | grep "h264" # Should show h264 +ffprobe output.mkv 2>&1 | grep "aac" # Should show aac +``` + +--- + +## Pre-Trip Checklist + +**Complete these tasks before traveling:** + +### 1. System Health Check +```bash +# Check ZFS pool health +sudo zpool status + +# Check disk space +df -h /srv/vaultpg /srv/mediapg + +# Check SSD health +sudo smartctl -a /dev/sdX # Replace with actual device + +# Verify Docker containers running +docker ps +``` + +### 2. Sync Everything +```bash +# Manually trigger sync +sudo systemctl start pocketgrimoire-sync.service + +# Wait for completion and verify +journalctl -u pocketgrimoire-sync.service -n 100 --no-pager +tail -n 200 /var/log/pocketgrimoire-sync.log +``` + +### 3. Test Media Playback +```bash +# Access Jellyfin +# Open: http://pocket-grimoire.local:8096 +# Play a movie +# Verify: Direct Play (check info during playback) +# No transcoding icon should appear +``` + +### 4. Test Offline Mode +```bash +# Disconnect from internet +# Verify services accessible: +# - http://pocket-grimoire.local:3000 (Wiki.js) +# - http://pocket-grimoire.local:8096 (Jellyfin) +# - http://pocket-grimoire.local:8080 (File Browser) + +# Test media playback offline +# Test wiki page browsing offline +``` + +### 5. Verify NFS Export +```bash +# On laptop +sudo mkdir -p /mnt/pocket-media +sudo mount -t nfs pocket-grimoire.local:/srv/mediapg /mnt/pocket-media +ls /mnt/pocket-media/library +sudo umount /mnt/pocket-media +``` + +### 6. Label Hardware +```bash +# Ensure all SSDs are labeled: +# - VAULT (always stays connected) +# - MEDIA-PERSONAL (for personal trips) +# - MEDIA-FAMILY (for family visits) +``` + +### 7. Pack Emergency Items +- [ ] Spare MicroSD card (Pi recovery) +- [ ] USB card reader +- [ ] Micro-HDMI to HDMI cable +- [ ] USB Ethernet adapter +- [ ] Extra cables (USB-C, HDMI) +- [ ] Flashlight +- [ ] Small screwdriver + +### 8. Document Passphrases +- [ ] ZFS encryption passphrases (written down, secured) +- [ ] WiFi credentials for travel router +- [ ] Jellyfin admin password +- [ ] Wiki.js admin password + +--- + +## Deployment Procedure + +**Hotel/Travel Location Setup:** + +### Physical Setup (5 minutes) +1. Unpack Pocket Grimoire enclosure +2. Connect Beryl AX to hotel WiFi (configure via phone app) +3. Connect Pi to Beryl AX via Ethernet +4. Plug Anker Prime into wall outlet +5. Connect all USB devices to Anker Prime +6. Power on (wait 2-3 minutes for boot) + +### ZFS Unlock (2 minutes) +```bash +# SSH into Pi +ssh user@pocket-grimoire.local + +# If ZFS pools didn't auto-unlock, unlock manually +sudo zfs load-key vaultpg +sudo zfs mount -a + +sudo zfs load-key mediapg +sudo zfs mount -a + +# Verify pools mounted +zfs list +``` + +### Verify Services (2 minutes) +```bash +# Check Docker containers +docker ps + +# Should see: +# - pocketgrimoire_db (PostgreSQL) +# - pocketgrimoire_wikijs (Wiki.js) +# - pocketgrimoire_jellyfin (Jellyfin) +# - pocketgrimoire_filebrowser (File Browser, if enabled) +``` + +### Connect Onn Boxes (5 minutes) +1. Power on Onn streaming box +2. Connect to hotel TV via HDMI +3. Configure Onn to connect to Beryl AX WiFi network +4. Install Jellyfin app on Onn (if not already installed) +5. Open Jellyfin app +6. Add server: `http://pocket-grimoire.local:8096` +7. Login and browse library + +### Laptop Setup (2 minutes) +```bash +# Mount NFS share (optional, for Jellyfin client on laptop) +sudo mkdir -p /mnt/pocket-media +sudo mount -t nfs pocket-grimoire.local:/srv/mediapg /mnt/pocket-media + +# Or configure in /etc/fstab for persistence: +pocket-grimoire.local:/srv/mediapg /mnt/pocket-media nfs defaults,_netdev 0 0 +``` + +**Total setup time: ~15 minutes** + +--- + +## Troubleshooting + +### Pi Won't Boot +1. Check power LED on Pi (should be solid red) +2. Check ACT LED (should blink green during boot) +3. If no LEDs: Check USB-C cable and Anker USB-A port +4. If ACT LED doesn't blink: MicroSD card issue + - Use spare MicroSD card + - Reflash OS with USB card reader + +### ZFS Pools Won't Mount +```bash +# Check pool status +sudo zpool status + +# Import pool manually +sudo zpool import -a + +# Load encryption keys +sudo zfs load-key vaultpg +sudo zfs load-key mediapg + +# Mount all +sudo zfs mount -a + +# If corruption detected +sudo zpool scrub vaultpg +sudo zpool scrub mediapg +``` + +### Docker Containers Not Starting +```bash +# Check if ZFS pools are mounted first +zfs list + +# Check Docker service +sudo systemctl status docker + +# View container logs +docker logs pocketgrimoire_wikijs +docker logs pocketgrimoire_jellyfin + +# Restart containers +cd /srv/pocket-grimoire/stacks/wikijs +docker compose restart + +cd /srv/pocket-grimoire/stacks/jellyfin +docker compose restart +``` + +### Jellyfin Shows Transcoding +**This should never happen - all media must be direct play only** + +1. During playback, click info icon +2. If "Transcoding" appears: + - Media is not H.264/AAC + - Re-encode media before next trip + - Do NOT allow transcoding on Pi (will overheat/crash) + +3. Verify media codec: + ```bash + ffprobe /srv/mediapg/library/movies/example.mkv + ``` + +4. If incorrect codec, re-encode: + ```bash + ffmpeg -i input.mkv -c:v libx264 -preset slow -crf 20 \ + -profile:v high -level 4.1 -pix_fmt yuv420p \ + -c:a aac -b:a 192k -c:s srt output.mkv + ``` + +### NFS Mount Fails on Laptop +```bash +# Check if NFS is running on Pi +ssh user@pocket-grimoire.local +sudo systemctl status nfs-server + +# Check exports +sudo exportfs -v + +# Try manual mount with verbose +sudo mount -v -t nfs pocket-grimoire.local:/srv/mediapg /mnt/pocket-media + +# Check firewall (if enabled) +sudo ufw status +``` + +### Wiki.js Not Loading +```bash +# Check container status +docker ps | grep wikijs + +# Check logs +docker logs pocketgrimoire_wikijs +docker logs pocketgrimoire_db + +# Restart Wiki.js stack +cd /srv/pocket-grimoire/stacks/wikijs +docker compose restart + +# Check database +docker exec -it pocketgrimoire_db psql -U wikijs -d wikijs -c "\dt" +``` + +### Sync Failures +```bash +# Check sync log +tail -n 200 /var/log/pocketgrimoire-sync.log + +# Check ntfy notifications (should have received failure alert) + +# Manually run sync +sudo /usr/local/sbin/pocketgrimoire-sync.sh + +# Check timer status +systemctl status pocketgrimoire-sync.timer +systemctl list-timers | grep pocketgrimoire + +# Reset timer +sudo systemctl restart pocketgrimoire-sync.timer +``` + +### Beryl AX Won't Connect to Hotel WiFi +1. Access Beryl AX admin panel: `http://192.168.8.1` +2. Navigate to: Internet → Repeater +3. Scan for hotel WiFi networks +4. Connect (may require captive portal login) +5. If captive portal required: + - Connect phone to Beryl AX WiFi + - Open browser, complete hotel WiFi login + - Beryl AX will inherit connection + +### Pi Overheating +**Should not happen with media-only stack** + +```bash +# Check temperature +vcgencmd measure_temp + +# Normal: <60°C idle, <70°C under load +# Warning: >70°C +# Critical: >80°C + +# If overheating: +# 1. Ensure passive heatsink case is properly installed +# 2. Verify Pi is not in enclosed space (needs airflow) +# 3. Check if transcoding is occurring (should never happen) +# 4. Check for runaway processes +htop +``` + +--- + +## Shutdown Procedure + +**Proper shutdown to protect encrypted ZFS pools:** + +### From SSH +```bash +# SSH into Pi +ssh user@pocket-grimoire.local + +# Stop Docker containers +cd /srv/pocket-grimoire/stacks/wikijs +docker compose down + +cd /srv/pocket-grimoire/stacks/jellyfin +docker compose down + +# Optional: Stop other containers +cd /srv/pocket-grimoire/stacks/filebrowser +docker compose down + +# Unmount and export ZFS pools +sudo zfs unmount -a +sudo zpool export vaultpg +sudo zpool export mediapg + +# Shutdown Pi +sudo shutdown -h now + +# Wait 30 seconds for complete shutdown +# Red LED will turn off when safe to unplug +``` + +### Emergency Shutdown +**If SSH is unavailable:** + +1. Unplug Ethernet cable from Pi (stops network activity) +2. Wait 10 seconds +3. Unplug power from Anker Prime +4. ZFS pools may need recovery on next boot (usually auto-repairs) + +**Note:** ZFS is resilient, but proper shutdown is always better. + +--- + +## Maintenance + +### Weekly (While at Home) +```bash +# Check ZFS pool health +sudo zpool status + +# Check for errors +sudo zpool status -v | grep -i error + +# Verify sync is working +tail -n 50 /var/log/pocketgrimoire-sync.log + +# Check Docker disk usage +docker system df +``` + +### Monthly +```bash +# Run ZFS scrub (verify data integrity) +sudo zpool scrub vaultpg +sudo zpool scrub mediapg + +# Check scrub results (after completion, usually 1-2 hours) +sudo zpool status + +# Update system packages +sudo apt update && sudo apt upgrade -y + +# Update Docker images +cd /srv/pocket-grimoire/stacks/wikijs +docker compose pull +docker compose up -d + +cd /srv/pocket-grimoire/stacks/jellyfin +docker compose pull +docker compose up -d + +# Prune unused Docker images +docker system prune -a +``` + +### Before Each Trip +- Run pre-trip checklist (see section above) +- Verify all media plays directly (no transcoding) +- Test offline mode +- Check battery/charge status of all devices +- Update any documentation that changed + +### After Each Trip +```bash +# Check for any errors in logs +journalctl -p err -b +tail -n 500 /var/log/pocketgrimoire-sync.log + +# Verify ZFS pool health +sudo zpool status + +# Check SSD health +sudo smartctl -a /dev/sdX + +# Review and clear old sync logs if needed +sudo truncate -s 0 /var/log/pocketgrimoire-sync.log +``` + +--- + +## Service Access Summary + +**When connected to Pocket Grimoire network:** + +``` +Wiki.js: http://pocket-grimoire.local:3000 +Jellyfin: http://pocket-grimoire.local:8096 +File Browser: http://pocket-grimoire.local:8080 +Dozzle: http://pocket-grimoire.local:9999 +SSH: ssh user@pocket-grimoire.local +NFS Media: nfs://pocket-grimoire.local/srv/mediapg +Router Admin: http://192.168.8.1 +``` + +--- + +## Resource Profile + +### Idle (At Home, Syncing) +``` +Wiki.js + PostgreSQL: ~250MB RAM +Jellyfin (idle): ~150MB RAM +ZFS ARC (capped): ~512MB RAM +System overhead: ~200MB RAM +───────────────────────────────── +Total: ~1.1GB / 8GB RAM +CPU: <5% +Temperature: Cool (<60°C) +``` + +### Media Playback (Direct Play) +``` +Jellyfin (serving): ~200MB RAM +NFS: ~50MB RAM +No transcoding: 0 CPU spike +───────────────────────────────── +Total: ~1.4GB / 8GB RAM +CPU: <10% +Temperature: Cool (<65°C) +``` + +**The Pi should remain cool and quiet during all operations.** + +--- + +## Security Notes + +### Encryption +- Both SSDs use native ZFS encryption +- Passphrases required on boot (manual unlock) +- Family media SSD is unencrypted (for portability/sharing) +- SSH keys are stored on encrypted Vault SSD + +### Network Security +- All services bound to LAN only (not exposed to WAN) +- Beryl AX handles firewall and VPN routing +- No services accept connections from internet directly +- WireGuard VPN to Netgrimoire when online + +### Physical Security +- Pocket Grimoire is a physical device - keep secure +- Encrypted SSDs protect data at rest +- Require passphrase on boot (prevents unauthorized access) +- Keep ZFS passphrases separate from device + +### Backup Strategy +- Pocket Grimoire is a mirror, not primary storage +- All data originates from Netgrimoire (source of truth) +- ZFS replication provides redundancy +- Can rebuild Pocket Grimoire from Netgrimoire if needed + +--- + +## Appendix A: System Specifications + +### Raspberry Pi 4 (8GB) +- CPU: Broadcom BCM2711, Quad-core Cortex-A72 @ 1.5GHz +- RAM: 8GB LPDDR4-3200 +- Storage: MicroSD (OS) + 2× USB 3.0 SSDs (data) +- Network: Gigabit Ethernet + WiFi 5 (802.11ac) +- Power: 5V/3A via USB-C (15W) + +### GL.iNet Beryl AX (GL-MT3000) +- CPU: MediaTek MT7981B, Dual-core ARM Cortex-A53 @ 1.3GHz +- RAM: 512MB DDR4 +- WiFi: WiFi 6 (802.11ax) dual-band +- VPN: WireGuard, OpenVPN +- Ports: 1× WAN, 1× LAN, 1× USB 3.0 +- Power: USB-C, 12W max + +### Anker Prime 200W (Model A2683) +- Total Output: 200W +- USB-C Ports: 4× (100W max each) +- USB-A Ports: 2× (5V/3A, 15W max each) +- AC Outlets: 2× +- Surge Protection: Yes + +### Storage Configuration +- SSD #1 (Vault): 1-2TB, encrypted ZFS +- SSD #2 (Personal Media): 2TB+, encrypted ZFS +- SSD #3 (Family Media): 2TB+, unencrypted ZFS +- Total capacity: 5-6TB (2 active at a time) + +--- + +## Appendix B: Quick Reference Commands + +### System Status +```bash +# Check ZFS pools +sudo zpool status + +# Check mounted filesystems +df -h + +# Check memory usage +free -h + +# Check temperature +vcgencmd measure_temp + +# Check Docker containers +docker ps + +# Check system load +htop +``` + +### Service Management +```bash +# Restart Wiki.js +cd /srv/pocket-grimoire/stacks/wikijs && docker compose restart + +# Restart Jellyfin +cd /srv/pocket-grimoire/stacks/jellyfin && docker compose restart + +# View Wiki.js logs +docker logs -f pocketgrimoire_wikijs + +# View Jellyfin logs +docker logs -f pocketgrimoire_jellyfin + +# Restart NFS +sudo systemctl restart nfs-server +``` + +### Sync Management +```bash +# Check sync timer status +systemctl status pocketgrimoire-sync.timer + +# View recent sync logs +tail -n 200 /var/log/pocketgrimoire-sync.log + +# Manually trigger sync +sudo systemctl start pocketgrimoire-sync.service + +# Watch sync in real-time +tail -f /var/log/pocketgrimoire-sync.log +``` + +### ZFS Operations +```bash +# List all pools and datasets +zfs list + +# Check pool health +sudo zpool status + +# Load encryption keys +sudo zfs load-key vaultpg +sudo zfs load-key mediapg + +# Mount all datasets +sudo zfs mount -a + +# Unmount all datasets +sudo zfs unmount -a + +# Export pools (before shutdown) +sudo zpool export vaultpg +sudo zpool export mediapg + +# Import pools +sudo zpool import vaultpg +sudo zpool import mediapg + +# Start scrub (data verification) +sudo zpool scrub vaultpg + +# Check scrub progress +sudo zpool status -v +``` + +### Network Diagnostics +```bash +# Check network interfaces +ip addr + +# Test connectivity to router +ping 192.168.8.1 + +# Test DNS resolution +nslookup google.com + +# Check NFS exports +sudo exportfs -v + +# Test NFS mount (from laptop) +sudo mount -t nfs pocket-grimoire.local:/srv/mediapg /mnt/test +``` + +--- + +## Appendix C: Useful Links + +### Official Documentation +- Raspberry Pi OS: https://www.raspberrypi.com/documentation/ +- OpenZFS: https://openzfs.github.io/openzfs-docs/ +- Docker: https://docs.docker.com/ +- Wiki.js: https://docs.requarks.io/ +- Jellyfin: https://jellyfin.org/docs/ +- GL.iNet: https://docs.gl-inet.com/ + +### Netgrimoire Resources +- Main documentation: (link to your Netgrimoire Wiki) +- Forgejo instance: (link to your Forgejo) +- ntfy instance: (link to your ntfy server) + +### Tools & Utilities +- FFmpeg documentation: https://ffmpeg.org/documentation.html +- Syncoid (part of Sanoid): https://github.com/jimsalterjrs/sanoid + +--- + +## Version History + +**v1.0 - Initial Release** +- Basic media server + documentation setup +- ZFS encrypted storage +- Automatic sync with Netgrimoire +- Gaming components removed for simplicity + +--- + +## Support & Feedback + +For issues or improvements to this documentation: +- Update this Wiki page directly +- Or submit changes to Forgejo repository +- Test all changes on non-production system first + +--- + +*This guide was created for Pocket Grimoire deployment and maintenance. Keep this documentation updated as the system evolves.*