Netgrimoire/Pocket-Grimoire/Sync/Deployment-Guide.md
2026-04-12 09:53:51 -05:00

3703 lines
91 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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.*