--- title: Pocket Grimoire description: published: true date: 2026-02-26T12:42:50.676Z 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) - Git repository mirrors (from Forgejo) - Wiki.js content backups - Kopia repository (backup data) - Photos and documents - System backups and configs - SSH keys - **Does NOT contain media, Stash data, or VeraCrypt containers** - **SSD #2 – GREEN** (2TB+, encrypted, for personal trips) - Personal media library (H.264/AAC movies and TV) - Stash-Pocket data (database, previews, blobs) - VeraCrypt containers (for ultra-sensitive files) - Personal content organized under `/Green/` structure - Connected during personal/solo trips - Syncs from `/export/vault/Green/` on Netgrimoire - **SSD #3 – MEDIA-FAMILY** (2TB+, unencrypted, for family trips) - Family-friendly movies and TV shows (H.264/AAC) - Simple `/library/movies/` and `/library/tv/` structure - Connected during family visits/trips - Unencrypted for easy sharing with relatives - Can be used on 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 │ ├── stash/ │ │ ├── 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 (always connected) ├── kopia/ # Kopia backup repository ├── backups/ # System backups │ ├── wiki/ # Wiki.js backups │ ├── photos/ # Photo backups │ └── documents/ # Document backups └── repos/ # Git repository mirrors /srv/greenpg/ # GREEN SSD (personal, rotated) └── Pocket/ # Dataset received from vault/Green/Pocket ├── media/library/ # Personal media files │ ├── movies/ │ └── tv/ ├── stash/ # Stash-Pocket data │ ├── config/ # Stash database │ ├── generated/ # Previews │ └── blobs/ # Scene markers └── veracrypt/ # VeraCrypt containers └── sensitive.vc # Encrypted container files /srv/mediapg/ # MEDIA-FAMILY SSD (family, rotated) └── library/ # Family media files ├── movies/ └── tv/ /mnt/veracrypt/ # VeraCrypt mount points (optional) ├── vault1/ # Mounted container 1 └── vault2/ # Mounted container 2 (if needed) /usr/local/sbin/ # System scripts ├── pocketgrimoire-sync.sh # Main sync script ├── pocketgrimoire-zfs-pull.sh # ZFS replication script ├── unlock-pocket-grimoire.sh # Headless unlock script └── mount-veracrypt-vault.sh # VeraCrypt mount script (optional) /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 ``` **⚠️ Important: Ubuntu Pi Boot Configuration Note** Ubuntu on Raspberry Pi uses a different boot config location than Raspberry Pi OS. The active kernel command line is in: ``` /boot/firmware/current/cmdline.txt ``` **Do NOT edit** `/boot/firmware/cmdline.txt` for kernel parameters — that file is only read during `tryboot` scenarios and is ignored on normal boot. Any kernel parameters (including USB quirks for drives) must go in `/boot/firmware/current/cmdline.txt` as a single unbroken line. This is critical for applying USB storage quirks (see Troubleshooting section if you experience drive issues). --- ### 2. Install VeraCrypt (Optional - For Encrypted Container Files) **VeraCrypt** allows you to mount encrypted container files as virtual drives. This is useful for: - Encrypted file containers for ultra-sensitive data - Portable encrypted volumes that can be moved between systems - Additional layer of encryption beyond ZFS (nested encryption) - Cross-platform compatibility (Windows, Mac, Linux) **Installation:** ```bash # Add VeraCrypt PPA repository sudo add-apt-repository ppa:unit193/encryption -y # Update package lists sudo apt update # Install VeraCrypt sudo apt install veracrypt -y # Verify installation veracrypt --text --version ``` **Create Mount Point:** ```bash # Create directory for VeraCrypt volumes sudo mkdir -p /mnt/veracrypt sudo mkdir -p /mnt/veracrypt/vault1 sudo mkdir -p /mnt/veracrypt/vault2 ``` **Mount VeraCrypt Container:** ```bash # Mount a VeraCrypt container file sudo veracrypt --text \ --mount /path/to/container.vc \ /mnt/veracrypt/vault1 # You will be prompted for: # - Container password # - PIM (leave blank if not used) # - Keyfiles (if any) # Verify mounted mount | grep veracrypt df -h /mnt/veracrypt/vault1 ``` **Auto-Mount on Boot (Optional):** Create systemd service to mount VeraCrypt on boot with manual password entry: ```bash sudo nano /etc/systemd/system/veracrypt-vault.service ``` ```ini [Unit] Description=Mount VeraCrypt vault container After=local-fs.target [Service] Type=oneshot RemainAfterExit=yes ExecStart=/usr/bin/veracrypt --text --non-interactive \ --password-stdin \ --mount /srv/vaultpg/containers/vault.vc \ /mnt/veracrypt/vault1 ExecStop=/usr/bin/veracrypt --text --dismount /mnt/veracrypt/vault1 [Install] WantedBy=multi-user.target ``` **Note:** For security, password should be entered manually at boot, not stored in files. **Better Approach - Manual Mount Script:** ```bash sudo nano /usr/local/sbin/mount-veracrypt-vault.sh ``` ```bash #!/bin/bash # Mount VeraCrypt container from GREEN drive CONTAINER="/srv/greenpg/Pocket/veracrypt/sensitive.vc" MOUNT_POINT="/mnt/veracrypt/vault1" if mount | grep -q "$MOUNT_POINT"; then echo "VeraCrypt volume already mounted at $MOUNT_POINT" exit 0 fi # Check if GREEN drive is mounted if [ ! -f "$CONTAINER" ]; then echo "Error: VeraCrypt container not found at $CONTAINER" echo "Is GREEN drive mounted?" exit 1 fi echo "Mounting VeraCrypt container from GREEN drive..." sudo veracrypt --text --mount "$CONTAINER" "$MOUNT_POINT" if [ $? -eq 0 ]; then echo "Successfully mounted: $MOUNT_POINT" df -h "$MOUNT_POINT" else echo "Failed to mount VeraCrypt container" exit 1 fi ``` ```bash sudo chmod +x /usr/local/sbin/mount-veracrypt-vault.sh ``` **Usage:** ```bash # Mount manually after boot sudo /usr/local/sbin/mount-veracrypt-vault.sh # Unmount sudo veracrypt --text --dismount /mnt/veracrypt/vault1 # List mounted volumes veracrypt --text --list ``` **VeraCrypt Container Creation (Do this on Netgrimoire first):** ```bash # Create directory in your existing vault/Green/Pocket dataset sudo mkdir -p /export/Green/Pocket/veracrypt # Create a new VeraCrypt container (example: 10GB) veracrypt --text --create /export/Green/Pocket/veracrypt/sensitive.vc # Follow prompts: # - Volume type: Normal # - Encryption algorithm: AES # - Hash algorithm: SHA-512 # - Filesystem: Linux Ext4 # - Size: 10GB (or desired size) # - Password: (enter strong password) # - Format volume: Yes ``` **Sync VeraCrypt Container via ZFS:** ```bash # VeraCrypt containers are stored in vault/Green/Pocket/veracrypt/ # They automatically sync to GREEN drive with the rest of the Pocket dataset # On Netgrimoire: # /export/Green/Pocket/veracrypt/sensitive.vc # After sync to GREEN drive: # /mnt/pocket-green/Pocket/veracrypt/sensitive.vc # On Pocket Grimoire after import: # /srv/greenpg/Pocket/veracrypt/sensitive.vc # The container syncs automatically when you sync the Green/Pocket dataset ``` **When to Use VeraCrypt vs ZFS Encryption:** **Use VeraCrypt when:** - Need portable encrypted containers (can move to other systems) - Want different passwords for different data sets - Need compatibility with Windows/Mac (VeraCrypt is cross-platform) - Want nested encryption (VeraCrypt inside ZFS) **Use ZFS encryption when:** - Encrypting entire drives/pools - Want transparent encryption (no manual mounting) - Need better performance (native filesystem encryption) - Don't need to move encrypted data to non-Linux systems **For Pocket Grimoire, recommended approach:** - ZFS encryption for VAULT and GREEN SSDs (always) - VeraCrypt for ultra-sensitive files on GREEN drive (optional) - Example: Tax documents, financial records, personal files - VeraCrypt containers stored in `/export/Green/Pocket/veracrypt/` - Syncs to GREEN drive automatically with other Pocket data ### 3. Install ZFS ```bash # Install ZFS utilities sudo apt install -y zfsutils-linux # Verify ZFS is working sudo zpool list ``` ### 4. Initial Drive Setup on Netgrimoire (Before Moving to Pocket) **IMPORTANT: Build drives on Netgrimoire first, then move to Pocket Grimoire.** This approach allows you to: - Create encrypted pools with proper passphrases - Perform initial ZFS sync while drives are fast-connected (SATA/USB 3.0) - Verify data integrity before moving drives - Test encryption/unlock on powerful hardware first #### Drive Configuration Overview **Drive #1: VAULT** (1-2TB, encrypted, always connected) - Purpose: Backups and system data ONLY - Contains: Git repos, Wiki backups, Kopia repository, photos, documents - Does NOT contain media, Stash data, or VeraCrypt containers **Drive #2: GREEN** (2TB+, encrypted, rotated for personal trips) - Purpose: Personal media, Stash-Pocket data, and VeraCrypt containers - Contains: Personal media library, Stash database/previews/blobs, VeraCrypt files - Syncs from `/export/vault/Green/` on Netgrimoire **Drive #3: MEDIA-FAMILY** (2TB+, unencrypted, rotated for family trips) - Purpose: Family-friendly shareable content - Contains: Simple library structure with movies and TV - Unencrypted for easy sharing with relatives #### On Netgrimoire: Create and Populate Drives **Connect drives to Netgrimoire:** - VAULT SSD (1-2TB) via USB 3.0 or SATA - GREEN SSD (2TB+) via USB 3.0 or SATA - MEDIA-FAMILY SSD (2TB+) via USB 3.0 or SATA (optional, can be created later) **Identify drives:** ```bash # On Netgrimoire lsblk # Note device names: /dev/sdX, /dev/sdY, /dev/sdZ ``` **Create VAULT pool (encrypted - backups only):** ```bash # On Netgrimoire 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 /mnt/pocket-vault \ pocket-vault /dev/sdX # Enter STRONG passphrase when prompted # Write down this passphrase - you'll need it on Pocket Grimoire # Create datasets for backups and system data sudo zfs create -o recordsize=16K pocket-vault/wiki-pg # PostgreSQL backups sudo zfs create pocket-vault/repos # Git repository mirrors sudo zfs create pocket-vault/kopia # Kopia backup repository sudo zfs create pocket-vault/backups # General backups sudo zfs create pocket-vault/backups/wiki # Wiki.js backups sudo zfs create pocket-vault/backups/photos # Photo backups sudo zfs create pocket-vault/backups/documents # Document backups # Set ownership sudo chown -R 1000:1000 /mnt/pocket-vault ``` **GREEN pool - Use Existing vault/Green/Pocket Dataset:** **IMPORTANT:** You already have an encrypted dataset `vault/Green/Pocket` on Netgrimoire with your personal media and Stash data. **Do NOT create a new pool from scratch.** Instead, you'll use ZFS send/receive to replicate this existing dataset to the GREEN drive. ```bash # On Netgrimoire # Verify your existing dataset zfs list vault/Green/Pocket # Should show: vault/Green/Pocket 5.01T 2.49T 5.01T /export/Green/Pocket # Check what's in it ls /export/Green/Pocket/ # Should show: media/ and stash/ directories # This dataset will be sent to the GREEN drive in the next step # No need to create pocket-green datasets manually ``` **Create empty GREEN pool (will receive data via ZFS send):** ```bash # On Netgrimoire with GREEN SSD connected 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 /mnt/pocket-green \ pocket-green /dev/sdY # Enter STRONG passphrase (can be different from VAULT) # Write down this passphrase # Don't create datasets manually - they'll be created by zfs receive # The pool is now ready to receive vault/Green/Pocket dataset ``` **Create MEDIA-FAMILY pool (unencrypted - family content):** ```bash # On Netgrimoire sudo zpool create -o ashift=12 \ -O compression=lz4 \ -O atime=off \ -O recordsize=1M \ -m /mnt/pocket-media \ pocket-media /dev/sdZ # No encryption - family can use this drive on any system # Create simple library structure sudo zfs create pocket-media/library sudo zfs create pocket-media/library/movies sudo zfs create pocket-media/library/tv # Set ownership sudo chown -R 1000:1000 /mnt/pocket-media ``` **Perform initial sync to VAULT:** ```bash # On Netgrimoire # Sync backups and system data to VAULT drive # Sync Wiki backups sudo rsync -avP \ /export/vault/wiki-backups/ \ /mnt/pocket-vault/backups/wiki/ # Sync Git repositories sudo rsync -avP \ /export/vault/repos/ \ /mnt/pocket-vault/repos/ # Sync Kopia repository (if exists) sudo rsync -avP \ /export/vault/kopia/ \ /mnt/pocket-vault/kopia/ # Sync photos and documents sudo rsync -avP \ /export/vault/photos/ \ /mnt/pocket-vault/backups/photos/ sudo rsync -avP \ /export/vault/documents/ \ /mnt/pocket-vault/backups/documents/ # Verify data ls -lh /mnt/pocket-vault/ du -sh /mnt/pocket-vault/ ``` **Perform initial sync to GREEN:** You have two options for syncing your existing `vault/Green/Pocket` dataset to the GREEN drive: **Option A: Using Syncoid (Recommended - Easier)** ```bash # On Netgrimoire with GREEN drive connected # Syncoid handles snapshots and incremental transfers automatically sudo syncoid vault/Green/Pocket pocket-green/Pocket # Syncoid will: # - Create snapshot automatically # - Send data to pocket-green/Pocket # - Show progress bar # - Handle all ZFS send/receive details # Verify received zfs list pocket-green/Pocket ls -lh /mnt/pocket-green/Pocket/ du -sh /mnt/pocket-green/Pocket/ ``` **Important Note on Pool Naming:** - On Netgrimoire during initial build: Pool is called `pocket-green` - After moving to Pocket Grimoire: Pool is renamed to `greenpg` during import - If you've already moved the drive to Pocket and back, use: `sudo syncoid vault/Green/Pocket greenpg/Pocket` **Option B: Manual ZFS Send (Advanced)** ```bash # On Netgrimoire # You have an existing encrypted dataset: vault/Green/Pocket # This contains your personal media and will include Stash data # First, verify the dataset exists and its size zfs list vault/Green/Pocket # Should show: vault/Green/Pocket 5.01T 2.49T 5.01T /export/Green/Pocket # Create snapshot for initial send sudo zfs snapshot vault/Green/Pocket@initial # Send to pocket-green pool, creating pocket-green/Pocket dataset # IMPORTANT: Must specify destination dataset name, not just pool name sudo zfs send vault/Green/Pocket@initial | \ sudo zfs receive pocket-green/Pocket # Or if pool was already renamed to greenpg: # sudo zfs send vault/Green/Pocket@initial | \ # sudo zfs receive greenpg/Pocket # This creates: pocket-green/Pocket (or greenpg/Pocket) # NOT just "pocket-green" (which is the pool name) # Verify received zfs list pocket-green/Pocket # or greenpg/Pocket ls -lh /mnt/pocket-green/Pocket/ # or /srv/greenpg/Pocket # Verify data integrity du -sh /mnt/pocket-green/Pocket/ # or /srv/greenpg/Pocket ``` **Both options create the same result:** ``` # The data structure will be: # /mnt/pocket-green/Pocket/ (or /srv/greenpg/Pocket if already renamed) # ├── media/library/ # │ ├── movies/ # │ └── tv/ # └── stash/ # ├── config/ # ├── generated/ # └── blobs/ ``` **Important notes:** - The `vault/Green/Pocket` dataset is encrypted on Netgrimoire - `zfs send` transfers the data (decrypted during send) - `pocket-green` (or `greenpg`) pool has its own encryption (encrypts during receive) - Result: Data is encrypted at rest on both systems with different keys - The dataset name becomes `pocket-green/Pocket` initially, or `greenpg/Pocket` if pool was already renamed - **Recommended:** Use syncoid (Option A) - it's simpler and handles everything automatically **Populate MEDIA-FAMILY (optional - curate family content):** ```bash # On Netgrimoire # Copy family-friendly media to MEDIA-FAMILY drive # Example: Copy family movies sudo cp /export/vault/media/family-movies/*.mp4 \ /mnt/pocket-media/library/movies/ # Or use rsync for large transfers sudo rsync -avP \ /export/vault/media/family-shows/ \ /mnt/pocket-media/library/tv/ # Verify du -sh /mnt/pocket-media/library/ ``` **Export pools before disconnecting:** ```bash # On Netgrimoire # CRITICAL: Export pools before physically disconnecting drives sudo zpool export pocket-vault # For GREEN drive - check which name it has zpool list | grep -E "pocket-green|greenpg" # If it shows "pocket-green": sudo zpool export pocket-green # If it shows "greenpg" (already renamed from previous import): sudo zpool export greenpg # For MEDIA-FAMILY (if created): sudo zpool export pocket-media # or mediapg if renamed # Verify exported zpool list # Should NOT show pocket-* or *pg pools ``` **Physically disconnect drives from Netgrimoire.** ### 5. Configure ZFS Pools on Pocket Grimoire **Now connect drives to Pocket Grimoire:** - VAULT → Anker USB-A port #2 (always connected) - GREEN (for personal trips) → Raspberry Pi USB 3.0 port OR - MEDIA-FAMILY (for family trips) → Raspberry Pi USB 3.0 port **Import and rename pools:** ```bash # On Pocket Grimoire (SSH into Pi) ssh user@pocket-grimoire.local # Import VAULT pool with new name sudo zpool import pocket-vault vaultpg # Import GREEN pool with new name (for personal trips) sudo zpool import pocket-green greenpg # OR import MEDIA-FAMILY pool (for family trips) # sudo zpool import pocket-media mediapg # Verify pools imported zpool list # Should show: vaultpg, greenpg (or mediapg for family) ``` **Set mount points for Pocket Grimoire:** ```bash # Set proper mount points sudo zfs set mountpoint=/srv/vaultpg vaultpg sudo zfs set mountpoint=/srv/greenpg greenpg # For the Pocket dataset (received from vault/Green/Pocket) sudo zfs set mountpoint=/srv/greenpg/Pocket greenpg/Pocket # Or for family drive (when you swap): # sudo zfs set mountpoint=/srv/mediapg mediapg # Create mount points sudo mkdir -p /srv/vaultpg sudo mkdir -p /srv/greenpg sudo mkdir -p /srv/mediapg # Create both, use as needed # Unmount and remount with new paths sudo zfs unmount -a sudo zfs mount -a # Verify mounted df -h | grep srv # Should show: # vaultpg mounted on /srv/vaultpg # greenpg mounted on /srv/greenpg # greenpg/Pocket mounted on /srv/greenpg/Pocket # Verify data ls /srv/vaultpg/ ls /srv/greenpg/Pocket/media/library/ ls /srv/greenpg/Pocket/stash/ # Or for family: # ls /srv/mediapg/library/ ``` **Configure for headless unlock:** ```bash # Set pools to NOT auto-mount on boot # This prevents boot hanging waiting for passphrase sudo zfs set canmount=noauto vaultpg sudo zfs set canmount=noauto greenpg sudo zfs set canmount=noauto greenpg/Pocket sudo zfs set canmount=noauto mediapg # For when you swap to family drive # Pools will need manual unlock via SSH after boot ``` **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 # Save and apply sudo update-initramfs -u sudo reboot ``` ### 6. Create Headless Unlock Script **After reboot, SSH back in and create unlock script:** ```bash sudo nano /usr/local/sbin/unlock-pocket-grimoire.sh ``` ```bash #!/bin/bash # Unlock Pocket Grimoire encrypted ZFS pools (headless operation) set -e echo "==========================================" echo " Pocket Grimoire ZFS Unlock (Headless)" echo "==========================================" echo # Check if VAULT pool is already unlocked if zfs list vaultpg &>/dev/null && mount | grep -q /srv/vaultpg; then echo "✓ vaultpg (VAULT) already unlocked and mounted" else # Import pool if needed if ! zpool list vaultpg &>/dev/null; then echo "Importing vaultpg pool..." sudo zpool import vaultpg fi # Unlock VAULT pool echo "Unlocking vaultpg (VAULT - backups and system data)..." sudo zfs load-key vaultpg # Mount all vaultpg datasets sudo zfs mount vaultpg sudo zfs mount -a if mount | grep -q /srv/vaultpg; then echo "✓ vaultpg unlocked and mounted at /srv/vaultpg" else echo "✗ Failed to mount vaultpg" exit 1 fi fi echo # Check for GREEN pool (personal media + Stash) if zpool list greenpg &>/dev/null; then if zfs list greenpg &>/dev/null && mount | grep -q /srv/greenpg; then echo "✓ greenpg (GREEN - personal media + Stash) already unlocked" else echo "Unlocking greenpg (GREEN - personal media + Stash)..." sudo zfs load-key greenpg sudo zfs mount greenpg sudo zfs mount -a if mount | grep -q /srv/greenpg; then echo "✓ greenpg unlocked and mounted at /srv/greenpg" else echo "✗ Failed to mount greenpg" exit 1 fi fi else echo "ℹ greenpg pool not found (GREEN drive not connected)" fi echo # Check for MEDIA-FAMILY pool (family content) if zpool list mediapg &>/dev/null; then if zfs list mediapg &>/dev/null && mount | grep -q /srv/mediapg; then echo "✓ mediapg (MEDIA-FAMILY) already unlocked" else echo "Unlocking mediapg (MEDIA-FAMILY - family content)..." # Check if encrypted (shouldn't be, but check anyway) if zfs get encryption mediapg | grep -q "encryption.*on"; then sudo zfs load-key mediapg fi sudo zfs mount mediapg sudo zfs mount -a if mount | grep -q /srv/mediapg; then echo "✓ mediapg unlocked and mounted at /srv/mediapg" else echo "✗ Failed to mount mediapg" exit 1 fi fi else echo "ℹ mediapg pool not found (MEDIA-FAMILY drive not connected)" fi echo # Optional: Mount VeraCrypt containers if [ -f /usr/local/sbin/mount-veracrypt-vault.sh ]; then echo "VeraCrypt container found. Mount now? (y/n)" read -r response if [[ "$response" == "y" ]]; then /usr/local/sbin/mount-veracrypt-vault.sh fi fi echo echo "==========================================" echo " Starting Docker Services" echo "==========================================" echo # Start Docker service if ! systemctl is-active --quiet docker; then echo "Starting Docker..." sudo systemctl start docker sleep 3 fi # Start containers echo "Starting Wiki.js stack..." cd /srv/pocket-grimoire/stacks/wikijs && docker compose up -d echo "Starting Jellyfin stack..." cd /srv/pocket-grimoire/stacks/jellyfin && docker compose up -d echo "Starting Stash stack..." if [ -d /srv/pocket-grimoire/stacks/stash ]; then cd /srv/pocket-grimoire/stacks/stash && docker compose up -d fi # Optional containers if [ -d /srv/pocket-grimoire/stacks/filebrowser ]; then echo "Starting File Browser..." cd /srv/pocket-grimoire/stacks/filebrowser && docker compose up -d fi echo echo "==========================================" echo " Pocket Grimoire Ready!" echo "==========================================" echo echo "Drives mounted:" if mount | grep -q /srv/vaultpg; then echo " ✓ VAULT (vaultpg) at /srv/vaultpg" fi if mount | grep -q /srv/greenpg; then echo " ✓ GREEN (greenpg) at /srv/greenpg - Personal media + Stash" fi if mount | grep -q /srv/mediapg; then echo " ✓ MEDIA-FAMILY (mediapg) at /srv/mediapg - Family content" fi echo echo "Services available at:" echo " Wiki.js: http://pocket-grimoire.local:3000" echo " Jellyfin: http://pocket-grimoire.local:8096" echo " Stash: http://pocket-grimoire.local:9999" echo " File Browser: http://pocket-grimoire.local:8080" echo echo "Total unlock time: $(($SECONDS / 60)) minutes $(($SECONDS % 60)) seconds" echo ``` ```bash sudo chmod +x /usr/local/sbin/unlock-pocket-grimoire.sh ``` ### 7. Disable Docker Auto-Start (Headless Configuration) **Prevent Docker from starting before ZFS pools are unlocked:** ```bash # Disable Docker auto-start on boot sudo systemctl disable docker # Docker will be started manually by unlock script ``` **Or, configure Docker to wait for ZFS (if you prefer):** ```bash sudo mkdir -p /etc/systemd/system/docker.service.d sudo nano /etc/systemd/system/docker.service.d/wait-for-zfs.conf ``` ```ini [Unit] # Don't start Docker until after manual ZFS unlock After=zfs-mount.service Wants=zfs-mount.service [Service] # Restart Docker if it fails (ZFS not ready) Restart=on-failure RestartSec=10 ``` ```bash sudo systemctl daemon-reload ``` **Recommended: Just disable auto-start and use unlock script.** ### 8. Test Headless Unlock Procedure **Test at home before traveling:** ```bash # 1. Reboot Pi sudo reboot # 2. Wait 2-3 minutes for boot (don't connect monitor/keyboard) # 3. SSH from laptop ssh user@pocket-grimoire.local # 4. Run unlock script /usr/local/sbin/unlock-pocket-grimoire.sh # Enter passphrases when prompted: # - VAULT passphrase # - MEDIA-PERSONAL passphrase (if encrypted) # - VeraCrypt password (if using) # 5. Wait for Docker containers to start # 6. Verify services running docker ps # 7. Access from browser # http://pocket-grimoire.local:3000 # http://pocket-grimoire.local:8096 # http://pocket-grimoire.local:9999 # 8. Verify data accessible ls /srv/vaultpg/Green/Pocket/ ls /srv/mediapg/library/ ``` **If everything works, you're ready for travel!** ### 9. Quick Manual Unlock (If Script Fails) ```bash # SSH into Pocket Grimoire ssh user@pocket-grimoire.local # Import pools if needed sudo zpool import vaultpg sudo zpool import greenpg # For GREEN (personal) # Or: # sudo zpool import mediapg # For MEDIA-FAMILY # Load encryption keys sudo zfs load-key vaultpg # VAULT (always encrypted) sudo zfs load-key greenpg # GREEN (encrypted) # mediapg is unencrypted (MEDIA-FAMILY) - no key needed # Mount all datasets sudo zfs mount -a # Verify mounted df -h | grep srv # Should show vaultpg and either greenpg or mediapg # Start Docker sudo systemctl start docker # Start containers manually cd /srv/pocket-grimoire/stacks/wikijs && docker compose up -d cd /srv/pocket-grimoire/stacks/jellyfin && docker compose up -d cd /srv/pocket-grimoire/stacks/stash && docker compose up -d ``` **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 Syncoid (ZFS Replication Tool) **Syncoid** is a ZFS replication tool that makes syncing datasets much easier than manual ZFS send/receive. ```bash # Install Sanoid (includes syncoid) sudo apt update sudo apt install -y sanoid # Verify installation which syncoid syncoid --version # Should show: syncoid version X.X.X ``` **What syncoid does:** - ✅ Automatically creates snapshots - ✅ Handles incremental ZFS send/receive - ✅ Manages snapshot cleanup - ✅ Shows progress bars - ✅ Works over SSH - ✅ Resumes interrupted transfers **Example usage:** ```bash # Local sync (same machine) sudo syncoid source/dataset destination/dataset # Remote sync over SSH sudo syncoid --sshkey /path/to/key \ root@remote-host:source/dataset \ local/dataset ``` **You'll use this for:** - Initial GREEN drive sync on Netgrimoire - Ongoing syncs from Netgrimoire to Pocket Grimoire over network - Much simpler than manual `zfs send` commands ### 7. 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) - VAULT (vaultpg): [write passphrase on paper] - GREEN (greenpg): [write passphrase on paper] - MEDIA-FAMILY (mediapg): N/A (unencrypted) - [ ] VeraCrypt container passwords (if using, written down, secured) - [ ] WiFi credentials for travel router (portapotty network) - [ ] Jellyfin admin password - [ ] Wiki.js admin password - [ ] Stash admin password - [ ] Keep all passphrases in secure location separate from device ### 9. Test Headless Unlock Procedure (CRITICAL) ```bash # At home, test the exact hotel deployment workflow # 1. Reboot Pi without monitor/keyboard attached sudo reboot # 2. Wait 2-3 minutes for boot # 3. SSH from laptop ssh user@pocket-grimoire.local # 4. Run unlock script /usr/local/sbin/unlock-pocket-grimoire.sh # 5. Enter passphrases when prompted # - VAULT passphrase (always) # - GREEN passphrase (if GREEN drive connected for personal trip) # - MEDIA-FAMILY has no passphrase (unencrypted) # - VeraCrypt password (if applicable) # 6. Wait for Docker containers to start (~30 seconds) # 7. Verify all services running docker ps # 8. Test access from browser # http://pocket-grimoire.local:3000 (Wiki.js) # http://pocket-grimoire.local:8096 (Jellyfin) # http://pocket-grimoire.local:9999 (Stash) # 9. Test media playback in Jellyfin # 10. Test Stash preview playback # 11. Test NFS mount from laptop (optional) ``` **If anything fails during this test, debug at home before traveling!** ### 10. Resync GREEN Drive When Connected to Netgrimoire **If you've physically moved the GREEN drive back to Netgrimoire for fast resyncing:** ```bash # On Netgrimoire with GREEN SSD connected # 1. Check which name the pool has zpool list | grep -E "pocket-green|greenpg" # 2. Import if not already imported # If pool is named "pocket-green": sudo zpool import pocket-green # If pool is named "greenpg" (already renamed from Pocket): sudo zpool import greenpg # 3. Load encryption key sudo zfs load-key pocket-green # or greenpg # Enter GREEN drive passphrase # 4. Mount datasets sudo zfs mount -a # 5. Verify mounted zfs list | grep -E "pocket-green|greenpg" # Should show the Pocket dataset mounted # 6. Resync using syncoid # If pool is named "pocket-green": sudo syncoid vault/Green/Pocket pocket-green/Pocket # If pool is named "greenpg": sudo syncoid vault/Green/Pocket greenpg/Pocket # Syncoid shows progress: # Sending incremental vault/Green/Pocket@... # 2.3GB 0:01:23 [28.4MB/s] [===============>] 100% # 7. Verify sync completed zfs list pocket-green/Pocket # or greenpg/Pocket du -sh /mnt/pocket-green/Pocket/ # or /srv/greenpg/Pocket # 8. Export pool before disconnecting sudo zfs unmount -a sudo zpool export pocket-green # or greenpg # 9. Safe to physically disconnect GREEN SSD ``` **Quick Commands Based on Pool Name:** ```bash # Check pool name first POOL_NAME=$(zpool list | grep -oE "pocket-green|greenpg") echo "Pool name: $POOL_NAME" # Then use appropriate commands sudo zpool import $POOL_NAME sudo zfs load-key $POOL_NAME sudo zfs mount -a sudo syncoid vault/Green/Pocket ${POOL_NAME}/Pocket sudo zpool export $POOL_NAME ``` ### 11. Configure Ongoing ZFS Sync (After Initial Setup) **Once drives are on Pocket Grimoire, set up ongoing sync from Netgrimoire:** ```bash # On Pocket Grimoire # Create ZFS replication script sudo nano /usr/local/sbin/pocketgrimoire-zfs-pull.sh ``` ```bash #!/usr/bin/env bash set -euo pipefail SRC_HOST="netgrimoire.local" SSH_KEY="/srv/pocket-grimoire/keys/zfs_pull_ro" # Sync GREEN/Pocket dataset (personal media + Stash) # This pulls incremental changes from vault/Green/Pocket to greenpg/Pocket syncoid --no-sync-snap \ --sshkey "${SSH_KEY}" \ "root@${SRC_HOST}:vault/Green/Pocket" \ "greenpg/Pocket" # Note: VAULT and MEDIA-FAMILY don't sync ongoing # VAULT: Contains backups only, managed separately # MEDIA-FAMILY: Manually updated when needed ``` ```bash sudo chmod +x /usr/local/sbin/pocketgrimoire-zfs-pull.sh ``` **This sync runs every 6 hours automatically and:** - ✅ Syncs `/export/Green/Pocket/` from Netgrimoire - ✅ Includes personal media updates - ✅ Includes Stash database changes - ✅ Includes new previews/blobs - ✅ Only transfers incremental changes (fast) **Test sync manually:** ```bash # Unlock pools first /usr/local/sbin/unlock-pocket-grimoire.sh # Run sync sudo /usr/local/sbin/pocketgrimoire-zfs-pull.sh # Check for new data zfs list greenpg/Pocket du -sh /srv/greenpg/Pocket/ ``` ### 12. Verify Data Synced from Netgrimoire ```bash # Check VAULT data present (backups only) ls /srv/vaultpg/kopia/ ls /srv/vaultpg/backups/ ls /srv/vaultpg/repos/ du -sh /srv/vaultpg/ # Check GREEN data (personal media + Stash) ls /srv/greenpg/Pocket/media/library/ ls /srv/greenpg/Pocket/stash/ du -sh /srv/greenpg/Pocket/ # Or check MEDIA-FAMILY data (if that drive is connected) ls /srv/mediapg/library/ du -sh /srv/mediapg/ # Verify Stash database and previews (only on GREEN) ls -lh /srv/greenpg/Pocket/stash/config/ # Should show: stash-go.sqlite ls /srv/greenpg/Pocket/stash/generated/ | wc -l # Should show: hundreds of preview files ``` --- ## Drive Movement Workflow (VAULT, GREEN, MEDIA-FAMILY) This section covers moving SSDs between Netgrimoire and Pocket Grimoire for syncing and swapping. --- ### VAULT Drive Movement **VAULT is normally ALWAYS CONNECTED to Pocket Grimoire**, but you may need to move it back to Netgrimoire for: - Initial population with backup data - Major updates to backup repositories - Troubleshooting or recovery #### Connect VAULT to Netgrimoire ```bash # On Netgrimoire # Physical: Connect VAULT SSD via USB 3.0 or SATA # Import pool sudo zpool import pocket-vault # Load encryption key sudo zfs load-key pocket-vault # Enter VAULT passphrase # Mount datasets sudo zfs mount -a # Verify mounted zfs list | grep pocket-vault df -h | grep pocket-vault ``` #### Update VAULT on Netgrimoire ```bash # On Netgrimoire # Update Kopia repository sudo kopia repository connect filesystem --path=/mnt/pocket-vault/kopia sudo kopia snapshot create /path/to/backup/source # Sync Git repositories sudo rsync -avP /export/vault/repos/ /mnt/pocket-vault/repos/ # Sync Wiki backups sudo rsync -avP /srv/wikijs/backups/ /mnt/pocket-vault/backups/wiki/ # Sync photo/document backups sudo rsync -avP /export/vault/photos/ /mnt/pocket-vault/backups/photos/ sudo rsync -avP /export/vault/documents/ /mnt/pocket-vault/backups/documents/ # Verify updates du -sh /mnt/pocket-vault/ ``` #### Disconnect VAULT from Netgrimoire ```bash # On Netgrimoire # Unmount datasets sudo zfs unmount pocket-vault # Or unmount all at once: # sudo zfs unmount -a (be careful with this!) # Export pool sudo zpool export pocket-vault # Verify exported zpool list | grep pocket-vault # Should show nothing # Physical: Disconnect VAULT SSD ``` #### Connect VAULT to Pocket Grimoire ```bash # Physical: Connect to Anker USB-A port #2 # On Pocket Grimoire (SSH) ssh user@pocket-grimoire.local # Use unlock script (recommended) /usr/local/sbin/unlock-pocket-grimoire.sh # Enter VAULT passphrase when prompted # Or manual: sudo zpool import pocket-vault vaultpg sudo zfs load-key vaultpg sudo zfs set mountpoint=/srv/vaultpg vaultpg sudo zfs mount -a # Verify mounted df -h | grep vaultpg zfs list | grep vaultpg ``` --- ### GREEN Drive Movement (Personal Media + Stash) **GREEN is rotated** - connected during personal trips, synced on Netgrimoire when updates needed. #### Connect GREEN to Netgrimoire ```bash # On Netgrimoire # Physical: Connect GREEN SSD via USB 3.0 or SATA # Check if drive detected lsblk # Import pool sudo zpool import greenpg # Load encryption key sudo zfs load-key greenpg # Enter GREEN passphrase # Mount datasets sudo zfs mount -a # Verify mounted zfs list | grep greenpg df -h | grep greenpg # Should show: # greenpg 5.00T 2.14T 280K /srv/greenpg # greenpg/Pocket 5.00T 2.14T 5.00T /srv/greenpg/Pocket ``` #### Update GREEN on Netgrimoire **Using Syncoid (Recommended):** ```bash # On Netgrimoire # Sync from vault/Green/Pocket to GREEN drive sudo syncoid vault/Green/Pocket greenpg/Pocket # Progress shown: # Sending incremental vault/Green/Pocket@... # 2.3GB 0:01:23 [28.4MB/s] [===============>] 100% # Verify sync completed zfs list greenpg/Pocket du -sh /srv/greenpg/Pocket/ # Check latest snapshot zfs list -t snapshot greenpg/Pocket | tail -3 ``` **Manual file updates (if needed):** ```bash # On Netgrimoire # Add new media sudo cp /path/to/new/movie.mp4 /srv/greenpg/Pocket/media/library/movies/ # Add new TV episodes sudo cp -r /path/to/show/Season02 /srv/greenpg/Pocket/media/library/tv/ShowName/ # Add VeraCrypt containers sudo cp /path/to/sensitive.vc /srv/greenpg/Pocket/veracrypt/ # Update Stash data (usually automatic via syncoid) # Stash database, previews, and blobs sync automatically ``` #### Disconnect GREEN from Netgrimoire ```bash # On Netgrimoire # Stop any processes using the drive sudo lsof | grep greenpg # Kill processes if needed # Unmount datasets sudo zfs unmount greenpg/Pocket sudo zfs unmount greenpg # Export pool (CRITICAL!) sudo zpool export greenpg # Verify exported zpool list | grep greenpg # Should show nothing # Verify ready for import elsewhere sudo zpool import | grep greenpg # Should show pool available # Physical: Disconnect GREEN SSD ``` #### Connect GREEN to Pocket Grimoire ```bash # Physical: Connect to Raspberry Pi USB 3.0 port (blue port) # On Pocket Grimoire (SSH) ssh user@pocket-grimoire.local # Use unlock script (recommended) /usr/local/sbin/unlock-pocket-grimoire.sh # Enter passphrases when prompted # Or manual: sudo zpool import greenpg sudo zfs load-key greenpg sudo zfs set mountpoint=/srv/greenpg greenpg sudo zfs set mountpoint=/srv/greenpg/Pocket greenpg/Pocket sudo zfs mount -a # Start Docker containers cd /srv/pocket-grimoire/stacks/jellyfin && docker compose up -d cd /srv/pocket-grimoire/stacks/stash && docker compose up -d # Verify services docker ps ``` **Test services:** ```bash # From browser (on portapotty WiFi) http://pocket-grimoire.local:8096 # Jellyfin http://pocket-grimoire.local:9999 # Stash ``` #### Disconnect GREEN from Pocket Grimoire ```bash # On Pocket Grimoire (SSH) ssh user@pocket-grimoire.local # Stop Docker containers using GREEN cd /srv/pocket-grimoire/stacks/jellyfin docker compose down cd /srv/pocket-grimoire/stacks/stash docker compose down # Unmount VeraCrypt (if using) sudo veracrypt --text --dismount-all # Unmount datasets sudo zfs unmount greenpg/Pocket sudo zfs unmount greenpg # Export pool sudo zpool export greenpg # Verify exported zpool list | grep greenpg # Should show nothing # Physical: Disconnect GREEN SSD ``` --- ### MEDIA-FAMILY Drive Movement (Family Content) **MEDIA-FAMILY is rotated** - connected during family trips, manually updated as needed. #### Connect MEDIA-FAMILY to Netgrimoire ```bash # On Netgrimoire # Physical: Connect MEDIA-FAMILY SSD via USB 3.0 or SATA # Import pool (no encryption key needed - unencrypted) sudo zpool import pocket-media # Mount datasets sudo zfs mount -a # Verify mounted zfs list | grep pocket-media df -h | grep pocket-media ``` #### Update MEDIA-FAMILY on Netgrimoire ```bash # On Netgrimoire # Add new family movies sudo cp /export/vault/media/family-movies/*.mp4 \ /mnt/pocket-media/library/movies/ # Add new family TV shows sudo rsync -avP \ /export/vault/media/family-shows/NewShow/ \ /mnt/pocket-media/library/tv/NewShow/ # Remove old content to free space sudo rm -rf /mnt/pocket-media/library/movies/OldMovie/ sudo rm -rf /mnt/pocket-media/library/tv/OldShow/ # Verify space usage du -sh /mnt/pocket-media/library/ df -h /mnt/pocket-media ``` #### Disconnect MEDIA-FAMILY from Netgrimoire ```bash # On Netgrimoire # Unmount datasets sudo zfs unmount pocket-media # Export pool sudo zpool export pocket-media # Verify exported zpool list | grep pocket-media # Should show nothing # Physical: Disconnect MEDIA-FAMILY SSD ``` #### Connect MEDIA-FAMILY to Pocket Grimoire ```bash # Physical: Connect to Raspberry Pi USB 3.0 port (blue port) # Note: Only connect ONE media drive at a time (GREEN or MEDIA-FAMILY) # On Pocket Grimoire (SSH) ssh user@pocket-grimoire.local # Import pool and rename sudo zpool import pocket-media mediapg # Set mount point sudo zfs set mountpoint=/srv/mediapg mediapg # Mount datasets (no encryption key needed) sudo zfs mount -a # Verify mounted df -h | grep mediapg zfs list | grep mediapg # Start Jellyfin cd /srv/pocket-grimoire/stacks/jellyfin docker compose up -d # Verify docker ps | grep jellyfin ``` **Test Jellyfin:** ```bash # From browser http://pocket-grimoire.local:8096 # Should show family-friendly media ``` #### Disconnect MEDIA-FAMILY from Pocket Grimoire ```bash # On Pocket Grimoire (SSH) ssh user@pocket-grimoire.local # Stop Jellyfin cd /srv/pocket-grimoire/stacks/jellyfin docker compose down # Unmount datasets sudo zfs unmount mediapg # Export pool sudo zpool export mediapg # Verify exported zpool list | grep mediapg # Should show nothing # Physical: Disconnect MEDIA-FAMILY SSD ``` --- ### Swapping Media Drives (GREEN ↔ MEDIA-FAMILY) **To swap from GREEN to MEDIA-FAMILY on Pocket Grimoire:** ```bash # On Pocket Grimoire # 1. Disconnect GREEN (see above) cd /srv/pocket-grimoire/stacks/jellyfin && docker compose down cd /srv/pocket-grimoire/stacks/stash && docker compose down sudo zfs unmount greenpg/Pocket sudo zfs unmount greenpg sudo zpool export greenpg # Physically disconnect GREEN SSD # 2. Connect MEDIA-FAMILY (see above) # Physically connect MEDIA-FAMILY SSD sudo zpool import pocket-media mediapg sudo zfs set mountpoint=/srv/mediapg mediapg sudo zfs mount -a cd /srv/pocket-grimoire/stacks/jellyfin && docker compose up -d # Note: Stash won't work with MEDIA-FAMILY (no Stash data on that drive) # Only Jellyfin uses MEDIA-FAMILY ``` **To swap from MEDIA-FAMILY to GREEN:** ```bash # On Pocket Grimoire # 1. Disconnect MEDIA-FAMILY cd /srv/pocket-grimoire/stacks/jellyfin && docker compose down sudo zfs unmount mediapg sudo zpool export mediapg # Physically disconnect MEDIA-FAMILY SSD # 2. Connect GREEN # Physically connect GREEN SSD sudo zpool import greenpg sudo zfs load-key greenpg sudo zfs mount -a cd /srv/pocket-grimoire/stacks/jellyfin && docker compose up -d cd /srv/pocket-grimoire/stacks/stash && docker compose up -d # Both Jellyfin and Stash work with GREEN ``` --- ### Quick Reference: Drive Movement Commands **VAULT:** ```bash # To Netgrimoire: sudo zpool import pocket-vault sudo zfs load-key pocket-vault sudo zfs mount -a # From Netgrimoire: sudo zfs unmount pocket-vault sudo zpool export pocket-vault # To Pocket: sudo zpool import pocket-vault vaultpg sudo zfs load-key vaultpg sudo zfs mount -a ``` **GREEN:** ```bash # To Netgrimoire: sudo zpool import greenpg sudo zfs load-key greenpg sudo zfs mount -a sudo syncoid vault/Green/Pocket greenpg/Pocket # Update # From Netgrimoire: sudo zfs unmount greenpg/Pocket && sudo zfs unmount greenpg sudo zpool export greenpg # To Pocket: /usr/local/sbin/unlock-pocket-grimoire.sh # Easy way # Or manual: import → load-key → mount → start containers # From Pocket: docker compose down # Stop jellyfin & stash sudo zfs unmount greenpg/Pocket && sudo zfs unmount greenpg sudo zpool export greenpg ``` **MEDIA-FAMILY:** ```bash # To Netgrimoire: sudo zpool import pocket-media sudo zfs mount -a # Add/remove content # From Netgrimoire: sudo zfs unmount pocket-media sudo zpool export pocket-media # To Pocket: sudo zpool import pocket-media mediapg sudo zfs set mountpoint=/srv/mediapg mediapg sudo zfs mount -a docker compose up -d # Start jellyfin # From Pocket: docker compose down # Stop jellyfin sudo zfs unmount mediapg sudo zpool export mediapg ``` --- ### Typical Use Cases **Before Personal Trip:** 1. Connect GREEN to Netgrimoire 2. Update: `sudo syncoid vault/Green/Pocket greenpg/Pocket` 3. Disconnect from Netgrimoire 4. Connect GREEN to Pocket Grimoire 5. Test Jellyfin and Stash **Before Family Visit:** 1. Disconnect GREEN from Pocket (if connected) 2. Connect MEDIA-FAMILY to Netgrimoire 3. Add/update family content 4. Disconnect from Netgrimoire 5. Connect MEDIA-FAMILY to Pocket Grimoire 6. Test Jellyfin **Weekly at Home (No Drive Movement):** - Leave drives in Pocket Grimoire - Use network sync: `/usr/local/sbin/pocketgrimoire-zfs-pull.sh` - Automatic every 6 hours --- ## 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 or admin panel) 3. Connect Pi to Beryl AX via Ethernet (CAT5 cable) 4. Plug Anker Prime into wall outlet 5. Connect all USB devices to Anker Prime: - VAULT SSD → Anker USB-A port #2 - Media SSD (PERSONAL or FAMILY) → Pi USB 3.0 port - Beryl AX → Anker USB-C retractable port - Pi → Anker USB-A port #1 6. Power on (wait 2-3 minutes for boot) ### SSH Connection (1 minute) ```bash # From laptop (connected to portapotty WiFi) ssh user@pocket-grimoire.local # If .local doesn't work, find Pi's IP: # - Check Beryl AX admin: http://192.168.8.1 # - Look for "pocket-grimoire" in client list # - SSH via IP: ssh user@192.168.8.50 ``` ### ZFS Unlock (2-3 minutes) ```bash # Run unlock script /usr/local/sbin/unlock-pocket-grimoire.sh # Script will prompt for passphrases: # Enter passphrase for 'vaultpg': [type VAULT passphrase] # Enter passphrase for 'mediapg': [type MEDIA-PERSONAL passphrase] # (MEDIA-FAMILY is unencrypted, no passphrase needed) # Script automatically: # - Unlocks ZFS pools # - Mounts all datasets # - Starts Docker service # - Starts all containers (Wiki.js, Jellyfin, Stash) # - Displays service URLs # Total unlock time: ~2-3 minutes ``` ### Verify Services (1 minute) ### Verify Services (1 minute) ```bash # Check Docker containers running docker ps # Should show: # pocketgrimoire_wikijs # pocketgrimoire_db # pocketgrimoire_jellyfin # pocketgrimoire_stash # Check ZFS pools mounted df -h | grep srv # Should show: # vaultpg mounted on /srv/vaultpg # mediapg mounted on /srv/mediapg ``` ### Access Services (1 minute) **From laptop browser (connected to portapotty WiFi):** - Wiki.js: `http://pocket-grimoire.local:3000` - Jellyfin: `http://pocket-grimoire.local:8096` - Stash: `http://pocket-grimoire.local:9999` - File Browser: `http://pocket-grimoire.local:8080` (if enabled) **From Onn Streaming Boxes:** - Configure Jellyfin app: Server `http://pocket-grimoire.local:8096` - Configure StashApp: Server `http://pocket-grimoire.local:9999` **Total setup time: ~10-12 minutes** ### If Unlock Script Fails **Manual unlock procedure:** ```bash # SSH into Pi ssh user@pocket-grimoire.local # Import pools sudo zpool import vaultpg sudo zpool import mediapg # Load encryption keys sudo zfs load-key vaultpg sudo zfs load-key mediapg # Only if MEDIA-PERSONAL (encrypted) # Mount datasets sudo zfs mount -a # Verify df -h | grep srv # Start Docker sudo systemctl start docker # Start containers cd /srv/pocket-grimoire/stacks/wikijs && docker compose up -d cd /srv/pocket-grimoire/stacks/jellyfin && docker compose up -d cd /srv/pocket-grimoire/stacks/stash && docker compose up -d ``` ```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 greenpg # GREEN drive # Mount all sudo zfs mount -a # If corruption detected sudo zpool scrub vaultpg sudo zpool scrub greenpg ``` ### Pool Name Confusion (pocket-green vs greenpg) **Problem:** You're not sure if your GREEN pool is named `pocket-green` or `greenpg` **Solution:** ```bash # Check which name the pool has zpool list | grep -E "pocket-green|greenpg" # If on Netgrimoire (initial build): Usually "pocket-green" # If on Pocket Grimoire: Always "greenpg" (renamed during import) # If moved back to Netgrimoire: Keeps "greenpg" name from Pocket # Import using the correct name sudo zpool import pocket-green # if shows pocket-green # OR sudo zpool import greenpg # if shows greenpg # For syncoid, use whichever name it has: sudo syncoid vault/Green/Pocket pocket-green/Pocket # if pocket-green # OR sudo syncoid vault/Green/Pocket greenpg/Pocket # if greenpg ``` **Why the name changes:** - Created on Netgrimoire: `pocket-green` (temporary name for building) - Imported to Pocket: Renamed to `greenpg` (permanent name for travel) - Moved back to Netgrimoire: Keeps `greenpg` name (doesn't revert) **Best practice:** After first import to Pocket, the pool is permanently `greenpg` ### Kanguru UltraLock UAS Errors / Pool Suspended **Symptoms:** - ZFS pool repeatedly suspending with `error=5` (EIO) - dmesg showing `uas_eh_abort_handler` every ~30 seconds - Pool status shows `SUSPENDED` - Drive resets cycling: `uas_eh_device_reset_handler start/success` repeating ``` sd 0:0:0:0: [sda] tag#8 uas_eh_abort_handler 0 uas-tag 3 inflight: CMD IN scsi host0: uas_eh_device_reset_handler start scsi host0: uas_eh_device_reset_handler success WARNING: Pool 'greenpg' has encountered an uncorrectable I/O failure and has been suspended. ``` **Root Cause:** The Kanguru UltraLock (`idVendor=1e1d, idProduct=2001`) uses the UAS driver by default. The Raspberry Pi 4's xhci USB controller has a known incompatibility with UAS on certain drives. The fix is to force the drive to use the `usb-storage` driver instead via a kernel quirk parameter. **Fix (Ubuntu Pi — permanent):** ```bash # Edit the correct cmdline file (NOT /boot/firmware/cmdline.txt) sudo nano /boot/firmware/current/cmdline.txt ``` Add `usb-storage.quirks=1e1d:2001:u` to the end of the existing single line: ``` console=serial0,115200 multipath=off dwc_otg.lpm_enable=0 console=tty1 root=LABEL=writable rootfstype=ext4 panic=10 rootwait fixrtc usb-storage.quirks=1e1d:2001:u ``` ```bash # Verify: should show ONE $ at end, no blank lines cat -A /boot/firmware/current/cmdline.txt # Reboot sudo reboot ``` **Verify fix after reboot:** ```bash sudo dmesg | grep -i "kanguru\|uas\|usb-storage" | head -10 ``` Confirmed working output: ``` usb 2-2: UAS is ignored for this device, using usb-storage instead usb-storage 2-2:1.0: USB Mass Storage device detected usb-storage 2-2:1.0: Quirks match for vid 1e1d pid 2001: 800000 scsi host0: usb-storage 2-2:1.0 ``` **Recover suspended pool after applying fix:** ```bash sudo zpool clear greenpg sudo zfs load-key greenpg/Pocket sudo zfs mount -a ``` If pool has data errors from before the fix: ```bash sudo zpool status -v greenpg sudo zpool scrub greenpg # If metadata errors remain and can't be repaired, destroy and resync from Netgrimoire ``` **Why `/boot/firmware/cmdline.txt` doesn't work:** On Ubuntu Pi, `/boot/firmware/config.txt` only reads `cmdline=cmdline.txt` under the `[tryboot]` section. The active boot uses `/boot/firmware/current/cmdline.txt` instead. This differs from Raspberry Pi OS where `/boot/firmware/cmdline.txt` is the correct file. **Hardware reference:** - Kanguru UltraLock USB ID: `1e1d:2001` - Pi 4 USB controller: xhci_hcd (Broadcom BCM2711) - Issue: xhci + UAS incompatibility on large USB drives *Fix discovered and documented during greenpg pool troubleshooting, February 2026* ### 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" ``` ### VeraCrypt Container Won't Mount **Check container exists:** ```bash ls -lh /srv/vaultpg/veracrypt-containers/ # Should show vault.vc file ``` **Verify VeraCrypt is installed:** ```bash veracrypt --text --version # Should show version number ``` **Try mounting with verbose output:** ```bash sudo veracrypt --text --verbose \ --mount /srv/vaultpg/veracrypt-containers/vault.vc \ /mnt/veracrypt/vault1 ``` **Common issues:** - **Wrong password:** Re-enter carefully (passwords are case-sensitive) - **Container corrupted:** Try mounting read-only: ```bash sudo veracrypt --text --mount --protect-hidden=no \ /srv/vaultpg/veracrypt-containers/vault.vc \ /mnt/veracrypt/vault1 ``` - **Already mounted elsewhere:** Unmount first: ```bash sudo veracrypt --text --dismount /mnt/veracrypt/vault1 ``` - **FUSE not available:** ```bash sudo apt install -y fuse libfuse2 sudo modprobe fuse ``` **Check what's mounted:** ```bash veracrypt --text --list mount | grep veracrypt ``` **Force unmount (if stuck):** ```bash sudo veracrypt --text --force --dismount /mnt/veracrypt/vault1 # Or: sudo umount -f /mnt/veracrypt/vault1 ``` **Verify container integrity:** ```bash # Test mount without password (will fail but shows if container is valid) sudo veracrypt --test /srv/vaultpg/veracrypt-containers/vault.vc ``` 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 (headless operation):** ### From SSH (Recommended) ```bash # SSH into Pi from laptop 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 cd /srv/pocket-grimoire/stacks/stash docker compose down # Optional: Stop other containers cd /srv/pocket-grimoire/stacks/filebrowser docker compose down # Unmount VeraCrypt containers (if using) sudo veracrypt --text --dismount /mnt/veracrypt/vault1 # Or dismount all: sudo veracrypt --text --dismount-all # Verify unmounted veracrypt --text --list # Should show "No volumes mounted" # Unmount and export ZFS pools sudo zfs unmount -a # Export VAULT (always present) sudo zpool export vaultpg # Export GREEN (if connected for personal trip) if zpool list greenpg &>/dev/null; then sudo zpool export greenpg fi # Export MEDIA-FAMILY (if connected for family trip) if zpool list mediapg &>/dev/null; then sudo zpool export mediapg fi # Verify pools exported zpool list # Should NOT show vaultpg, greenpg, or mediapg # Shutdown Pi sudo shutdown -h now # Wait 30 seconds for complete shutdown # Pi's green ACT LED will stop blinking # Red power LED will turn off # Safe to unplug power ``` **Total shutdown time: ~2-3 minutes** ### Emergency Shutdown **If SSH is unavailable or Pi is unresponsive:** 1. Stop all network activity: - Unplug Ethernet cable from Pi - Wait 10 seconds 2. Power off: - Unplug power from Anker Prime (pulls power from everything) - Wait 10 seconds 3. Consequences: - ZFS pools may need recovery on next boot (usually auto-repairs) - VeraCrypt containers are generally safe with sudden unmount - Docker containers will need restart - No data loss expected (ZFS is resilient) **Note:** ZFS and VeraCrypt are resilient to sudden power loss, but proper shutdown is always better for data integrity. ### Shutdown Checklist Before leaving hotel: - [ ] SSH into Pocket Grimoire - [ ] Stop all Docker containers - [ ] Unmount VeraCrypt (if using) - [ ] Export ZFS pools (vaultpg, mediapg) - [ ] Shutdown Pi (`sudo shutdown -h now`) - [ ] Wait for Pi LEDs to turn off (30 seconds) - [ ] Unplug power from Anker Prime - [ ] Disconnect and pack all equipment **Never skip the ZFS export step!** Exporting pools ensures: - All data is flushed to disk - Filesystem is marked clean - Prevents corruption - Allows pools to be imported cleanly on next boot --- ## 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 - **ZFS 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 - **VeraCrypt Containers (Optional):** Additional encryption layer - Encrypted file containers within ZFS-encrypted drives (nested encryption) - Separate passwords for different data sets - Portable containers can be moved to other systems - Cross-platform compatibility (Windows, Mac, Linux) ### 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 - ZFS and/or VeraCrypt passphrases required on boot (prevents unauthorized access) - Keep all encryption passphrases separate from device - Consider: Write passphrases on paper, store in secure location ### Backup Strategy - Pocket Grimoire is a mirror, not primary storage - All data originates from Netgrimoire (source of truth) - ZFS replication provides redundancy - VeraCrypt containers sync like any other file - Can rebuild Pocket Grimoire from Netgrimoire if needed ### Encryption Best Practices - **Use strong passphrases:** 20+ characters, mix of types - **Don't reuse passwords:** ZFS ≠ VeraCrypt ≠ services - **Document recovery:** Write down passphrases (paper, not digital) - **Test recovery:** Verify you can unlock before traveling - **Secure storage:** Keep passphrase backup separate from device --- ## 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 ``` ### VeraCrypt Operations ```bash # Mount VeraCrypt container sudo veracrypt --text --mount \ /srv/vaultpg/veracrypt-containers/vault.vc \ /mnt/veracrypt/vault1 # Or use helper script sudo /usr/local/sbin/mount-veracrypt-vault.sh # List mounted volumes veracrypt --text --list # Check what's in mounted container ls -lh /mnt/veracrypt/vault1 # Unmount specific volume sudo veracrypt --text --dismount /mnt/veracrypt/vault1 # Unmount all VeraCrypt volumes sudo veracrypt --text --dismount-all # Force unmount (if stuck) sudo veracrypt --text --force --dismount /mnt/veracrypt/vault1 # Check VeraCrypt version veracrypt --text --version ``` 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.*