Netgrimoire/Netgrimoire/Pocket/Deployment_Guide.md
2026-02-20 04:44:50 +00:00

40 KiB
Raw Blame History

title description published date tags editor dateCreated
Pocket Grimoire true 2026-02-20T04:44:39.249Z markdown 2026-02-20T04:41:35.122Z

Pocket Grimoire - Complete Deployment Guide

Portable, Encrypted, Offline-Capable Media Server and Documentation Reference


Overview

Pocket Grimoire is a portable companion to the Netgrimoire homelab, providing offline access to:

  • Documentation and reference material (Wiki.js)
  • Personal and family media libraries (Jellyfin)
  • Photos, documents, and backups (encrypted vault)
  • Automatic synchronization with Netgrimoire when connected

Design Philosophy:

  • Calm and predictable
  • Encrypted at rest
  • Offline-first operation
  • Automatic synchronization
  • One wall plug
  • No cloud dependencies
  • Minimal services (media + docs only, no gaming)

Hardware Inventory

Core Compute

  • Raspberry Pi 4 (8GB)
  • Passive heatsink case or low-noise fan case
  • Official Raspberry Pi 4 Power Supply OR quality 3A USB-A to USB-C cable
  • Spare MicroSD card (32GB+, for OS recovery)
  • USB card reader (for flashing Pi images)

Storage (3 SSDs, 2 Active at a Time)

  • SSD #1 VAULT (1-2TB, encrypted, always connected)
    • Wiki.js data, PostgreSQL, git repos, photos, documents, backups
  • SSD #2 MEDIA-PERSONAL (2TB+, encrypted)
    • Your curated H.264/AAC movies and TV shows
    • Connected for personal trips
  • SSD #3 MEDIA-FAMILY (2TB+, unencrypted)
    • Family-friendly movies and TV shows (H.264/AAC)
    • Connected for family visits
    • Shareable/portable to other devices without Pocket Grimoire
  • USB drive ISO/Rebuild (64GB+, labeled, write-protected)
  • USB drive Data Transfer (128GB+, labeled)

Networking

  • GL.iNet Beryl AX (GL-MT3000) travel router
  • Short CAT5/6 Ethernet cable (6-12 inch, Pi ↔ Router)
  • USB Ethernet adapter (backup/emergency)

Power

  • Anker Prime 200W 6-Port GaN Charging Station (Model A2683)
  • Short USB-A to USB-C cable (3A-rated, 6-12 inch, for Pi)
  • Short USB-A to USB-A cable (6-12 inch, for Vault SSD)
  • 2× USB-C to USB-C cables (6ft, 100W with E-Marker chip, for laptop/phone)

Media Players

  • 2× Onn 4K streaming boxes with power supplies
  • 2× HDMI cables
  • Mini wireless keyboard (for Onn boxes and emergency Pi access)

Cables & Accessories

  • Micro-HDMI to HDMI cable (Pi emergency console access)
  • HDMI extender (if hotel TV ports are difficult to reach)

Organization & Emergency

  • Carry case for complete kit
  • Cable organizer pouch (separate from main case)
  • Velcro cable ties (pack of 20)
  • Labels for SSDs (VAULT, MEDIA-PERSONAL, MEDIA-FAMILY)
  • Small flashlight or headlamp
  • Small screwdriver or multitool (accessing hotel TV ports)

Power Configuration

Anker Prime A2683 Port Assignments

USB-C1 (retractable) → GL.iNet Beryl AX (12W)
USB-C2 (100W PD)     → Laptop charging (65-90W)
USB-C3               → Phone charging (20-30W)
USB-C4               → Tablet/spare (optional)
USB-A1 (5V/3A)       → Raspberry Pi 4 (15W)
USB-A2 (5V/3A)       → Vault SSD (always connected, 5W)

AC Outlet 1          → Spare
AC Outlet 2          → Spare

Raspberry Pi USB Ports

USB 3.0 Port 1 → Media SSD (personal or family, rotated)
USB 3.0 Port 2 → Spare (emergency USB, data transfer)
USB 2.0 Port 1 → Spare (wireless keyboard dongle if needed)
USB 2.0 Port 2 → Spare

Power Budget

Component             Power Draw    Running Total
─────────────────────────────────────────────────
Raspberry Pi 4        15W           15W
Beryl AX              12W           27W
Vault SSD             5W            32W
Media SSD (via Pi)    5W            37W
Laptop (charging)     65W           102W
Phone (charging)      20W           122W
─────────────────────────────────────────────────
Total                               122W / 200W
Headroom: 78W

Software Stack

Host OS & Services (Native)

Operating System:

  • Raspberry Pi OS Lite 64-bit (headless, no desktop environment)
  • Alternative: Ubuntu Server 22.04 LTS ARM64

Storage & Filesystems:

  • ZFS (OpenZFS) - Encrypted pools
    • vaultpg pool (Vault SSD, always connected)
    • mediapg pool (Media SSDs, rotated personal/family)
    • Native ZFS encryption with passphrase unlock
    • ARC memory capped (512MB-1GB maximum)

File Sharing:

  • NFS server (host-level, not containerized)
    • Exports /srv/mediapg to LAN (read-only)
    • Laptop and Onn boxes mount for media access

Sync & Automation:

  • systemd timers (scheduled jobs every 6 hours)
    • ZFS replication from Netgrimoire via syncoid
    • Git pulls for wiki/docs repositories
    • ntfy failure notifications

Networking:

  • Standard Linux networking
  • Docker and Docker Compose

Docker Containers

Required Stack:

  1. Wiki.js - Documentation mirror

    • Read-only wiki pulling from Forgejo
    • Git backend with SSH deploy key (read-only)
    • Works fully offline after sync
    • Port: 3000
  2. PostgreSQL - Wiki.js database backend

    • Stored on Vault SSD
    • Tuned for 16K recordsize (Postgres optimal)
  3. Jellyfin - Media server

    • Direct play ONLY (transcoding disabled)
    • Serves H.264/AAC pre-encoded media
    • Accessible from Onn boxes and laptop
    • Port: 8096

Optional Containers:

  1. File Browser - Read-only web UI

    • Quick LAN access to vault/media without SSH
    • Port: 8080
  2. Dozzle - Container log viewer

    • Simple Docker log viewer for debugging
    • Port: 9999

Network Architecture

Operating Modes

Home Base (Netgrimoire LAN):

  • Direct LAN connectivity
  • VPN not required
  • Fast local synchronization
  • All services accessible

Travel (Online):

  • All traffic routed via WireGuard VPN to Netgrimoire
  • DNS and ad blocking handled by Beryl AX router
  • Primary DNS: Netgrimoire (via VPN)
  • Fallback DNS: Public resolvers

Travel (Offline):

  • Full local access to all services
  • Wiki, files, and media available
  • No synchronization until connectivity returns
  • DNS handled locally by router

Router Configuration (Beryl AX)

DNS & Ad Blocking:

  • AdGuard Home enabled on router
  • Acts as primary DNS for all clients
  • Blocks ads and trackers network-wide

DNS Behavior:

  • Primary DNS: Netgrimoire (via VPN when available)
  • Fallback DNS: Public resolvers (1.1.1.1, 9.9.9.9)
  • Local DNS entries:
    • pocket-grimoire.local
    • wiki.pocket-grimoire.local
    • media.pocket-grimoire.local

VPN Behavior:

  • WireGuard client configured
  • When VPN available: All traffic tunneled to Netgrimoire
  • When VPN unavailable: Normal WAN routing

Directory Structure

/srv/pocket-grimoire/              # Main application root (on Vault SSD)
├── stacks/                        # Docker Compose files
│   ├── wikijs/
│   │   ├── docker-compose.yml
│   │   └── .env
│   ├── jellyfin/
│   │   ├── docker-compose.yml
│   │   └── .env
│   └── filebrowser/               # Optional
│       └── docker-compose.yml
├── data/                          # Persistent container data
│   ├── postgres/                  # PostgreSQL data
│   ├── wikijs/                    # Wiki.js data
│   ├── jellyfin/                  # Jellyfin metadata/config
│   └── filebrowser/               # File browser config
├── repos/                         # Git repository mirrors
│   └── wiki/                      # Wiki content from Forgejo
└── keys/                          # SSH keys
    ├── forgejo_wiki_ro            # Read-only wiki deploy key
    └── zfs_pull_ro                # ZFS replication key

/srv/vaultpg/                      # Vault SSD ZFS mount
└── (mirrors from Netgrimoire)

/srv/mediapg/                      # Media SSD ZFS mount (rotated)
└── library/                       # H.264 encoded media
    ├── movies/
    └── tv/

/usr/local/sbin/                   # System scripts
├── pocketgrimoire-sync.sh         # Main sync script
└── pocketgrimoire-zfs-pull.sh     # ZFS replication script

/etc/                              # Config files
├── pocketgrimoire-sync.env        # Secrets (ntfy tokens)
├── exports                        # NFS exports
└── systemd/system/
    ├── pocketgrimoire-sync.service
    └── pocketgrimoire-sync.timer

Installation Instructions

1. Base OS Installation

Download Raspberry Pi OS:

# 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:

# SSH into Pi
ssh user@pocket-grimoire.local

# Update system
sudo apt update && sudo apt upgrade -y

# Set timezone
sudo timedatectl set-timezone America/Chicago

# Configure locale
sudo raspi-config
# System Options → Locale → en_US.UTF-8

2. Install ZFS

# Install ZFS utilities
sudo apt install -y zfsutils-linux

# Verify ZFS is working
sudo zpool list

3. Configure ZFS Pools

Important: Replace /dev/sdX with your actual device identifiers. Use lsblk to identify drives.

Create Vault Pool (Always Connected):

# Identify Vault SSD
lsblk

# Create encrypted ZFS pool
sudo zpool create -o ashift=12 \
  -O encryption=on \
  -O keylocation=prompt \
  -O keyformat=passphrase \
  -O compression=lz4 \
  -O atime=off \
  -O recordsize=1M \
  -m /srv/vaultpg \
  vaultpg /dev/sdX

# Create datasets
sudo zfs create -o recordsize=16K vaultpg/wiki-pg    # PostgreSQL
sudo zfs create vaultpg/repos                         # Git repos
sudo zfs create vaultpg/pocket-grimoire              # App data

Create Media Pool (Rotated):

# For Personal Media SSD
sudo zpool create -o ashift=12 \
  -O encryption=on \
  -O keylocation=prompt \
  -O keyformat=passphrase \
  -O compression=lz4 \
  -O atime=off \
  -O recordsize=1M \
  -m /srv/mediapg \
  mediapg /dev/sdY

sudo zfs create mediapg/library
sudo zfs create mediapg/library/movies
sudo zfs create mediapg/library/tv

# For Family Media SSD (unencrypted)
# When swapping drives:
sudo zpool export mediapg
# Connect family SSD
sudo zpool create -o ashift=12 \
  -O compression=lz4 \
  -O atime=off \
  -O recordsize=1M \
  -m /srv/mediapg \
  mediapg /dev/sdY

Cap ZFS ARC Memory:

# Create /etc/modprobe.d/zfs.conf
sudo nano /etc/modprobe.d/zfs.conf

# Add this line (for 8GB Pi, cap at 1GB):
options zfs zfs_arc_max=1073741824

# Reboot to apply
sudo reboot

Configure ZFS to Wait for Passphrase on Boot:

# Edit /etc/systemd/system/zfs-load-key.service
sudo nano /etc/systemd/system/zfs-load-key.service

Add:

[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:

sudo systemctl daemon-reload
sudo systemctl enable zfs-load-key.service

4. Install Docker

# 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

# 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:

sudo exportfs -ra
sudo systemctl restart nfs-server
sudo systemctl enable nfs-server

# Verify
sudo exportfs -v

6. Install System Packages

# 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:

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:

nano /srv/pocket-grimoire/stacks/wikijs/.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:

nano /srv/pocket-grimoire/stacks/wikijs/docker-compose.yml
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:

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:

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:

nano /srv/pocket-grimoire/stacks/jellyfin/.env
TZ=America/Chicago
PUID=1000
PGID=1000
JELLYFIN_PORT=8096
COMPOSE_PROJECT_NAME=pocketgrimoire_jellyfin

Create Docker Compose file:

nano /srv/pocket-grimoire/stacks/jellyfin/docker-compose.yml
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:

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:

mkdir -p /srv/pocket-grimoire/stacks/filebrowser
mkdir -p /srv/pocket-grimoire/data/filebrowser

Create Docker Compose file:

nano /srv/pocket-grimoire/stacks/filebrowser/docker-compose.yml
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:

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:

mkdir -p /srv/pocket-grimoire/stacks/dozzle
nano /srv/pocket-grimoire/stacks/dozzle/docker-compose.yml
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:

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:

    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

sudo nano /etc/pocketgrimoire-sync.env
NTFY_URL="https://ntfy.YOUR_DOMAIN/pocket-grimoire"
NTFY_TOKEN="YOUR_NTFY_TOKEN_HERE"  # Optional
HOSTNAME_TAG="$(hostname -s)"
sudo chmod 600 /etc/pocketgrimoire-sync.env

Create Main Sync Script

sudo nano /usr/local/sbin/pocketgrimoire-sync.sh
#!/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
sudo chmod +x /usr/local/sbin/pocketgrimoire-sync.sh
sudo mkdir -p /var/lib/pocketgrimoire

Create ZFS Replication Script

sudo nano /usr/local/sbin/pocketgrimoire-zfs-pull.sh
#!/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}"
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

sudo nano /etc/systemd/system/pocketgrimoire-sync.service
[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

sudo nano /etc/systemd/system/pocketgrimoire-sync.timer
[Unit]
Description=Run Pocket Grimoire sync every 6 hours

[Timer]
OnBootSec=10min
OnUnitActiveSec=6h
Persistent=true

[Install]
WantedBy=timers.target

Enable and Start Timer

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:

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:

#!/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:

ffprobe input.mkv 2>&1 | grep -E "Video:|Audio:"

Verify direct play compatibility:

# 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

# 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

# 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

# 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

# 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

# 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

# Ensure all SSDs are labeled:
# - VAULT (always stays connected)
# - MEDIA-PERSONAL (for personal trips)
# - MEDIA-FAMILY (for family visits)

7. Pack Emergency Items

  • Spare MicroSD card (Pi recovery)
  • USB card reader
  • Micro-HDMI to HDMI cable
  • USB Ethernet adapter
  • Extra cables (USB-C, HDMI)
  • Flashlight
  • Small screwdriver

8. Document Passphrases

  • ZFS encryption passphrases (written down, secured)
  • WiFi credentials for travel router
  • Jellyfin admin password
  • Wiki.js admin password

Deployment Procedure

Hotel/Travel Location Setup:

Physical Setup (5 minutes)

  1. Unpack Pocket Grimoire enclosure
  2. Connect Beryl AX to hotel WiFi (configure via phone app)
  3. Connect Pi to Beryl AX via Ethernet
  4. Plug Anker Prime into wall outlet
  5. Connect all USB devices to Anker Prime
  6. Power on (wait 2-3 minutes for boot)

ZFS Unlock (2 minutes)

# SSH into Pi
ssh user@pocket-grimoire.local

# If ZFS pools didn't auto-unlock, unlock manually
sudo zfs load-key vaultpg
sudo zfs mount -a

sudo zfs load-key mediapg
sudo zfs mount -a

# Verify pools mounted
zfs list

Verify Services (2 minutes)

# 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)

# 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

# Check pool status
sudo zpool status

# Import pool manually
sudo zpool import -a

# Load encryption keys
sudo zfs load-key vaultpg
sudo zfs load-key mediapg

# Mount all
sudo zfs mount -a

# If corruption detected
sudo zpool scrub vaultpg
sudo zpool scrub mediapg

Docker Containers Not Starting

# 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:

    ffprobe /srv/mediapg/library/movies/example.mkv
    
  4. If incorrect codec, re-encode:

    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

# 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

# Check container status
docker ps | grep wikijs

# Check logs
docker logs pocketgrimoire_wikijs
docker logs pocketgrimoire_db

# Restart Wiki.js stack
cd /srv/pocket-grimoire/stacks/wikijs
docker compose restart

# Check database
docker exec -it pocketgrimoire_db psql -U wikijs -d wikijs -c "\dt"

Sync Failures

# 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

# Check temperature
vcgencmd measure_temp

# Normal: <60°C idle, <70°C under load
# Warning: >70°C
# Critical: >80°C

# If overheating:
# 1. Ensure passive heatsink case is properly installed
# 2. Verify Pi is not in enclosed space (needs airflow)
# 3. Check if transcoding is occurring (should never happen)
# 4. Check for runaway processes
htop

Shutdown Procedure

Proper shutdown to protect encrypted ZFS pools:

From SSH

# SSH into Pi
ssh user@pocket-grimoire.local

# Stop Docker containers
cd /srv/pocket-grimoire/stacks/wikijs
docker compose down

cd /srv/pocket-grimoire/stacks/jellyfin
docker compose down

# Optional: Stop other containers
cd /srv/pocket-grimoire/stacks/filebrowser
docker compose down

# Unmount and export ZFS pools
sudo zfs unmount -a
sudo zpool export vaultpg
sudo zpool export mediapg

# Shutdown Pi
sudo shutdown -h now

# Wait 30 seconds for complete shutdown
# Red LED will turn off when safe to unplug

Emergency Shutdown

If SSH is unavailable:

  1. Unplug Ethernet cable from Pi (stops network activity)
  2. Wait 10 seconds
  3. Unplug power from Anker Prime
  4. ZFS pools may need recovery on next boot (usually auto-repairs)

Note: ZFS is resilient, but proper shutdown is always better.


Maintenance

Weekly (While at Home)

# 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

# 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

# Check for any errors in logs
journalctl -p err -b
tail -n 500 /var/log/pocketgrimoire-sync.log

# Verify ZFS pool health
sudo zpool status

# Check SSD health
sudo smartctl -a /dev/sdX

# Review and clear old sync logs if needed
sudo truncate -s 0 /var/log/pocketgrimoire-sync.log

Service Access Summary

When connected to Pocket Grimoire network:

Wiki.js:         http://pocket-grimoire.local:3000
Jellyfin:        http://pocket-grimoire.local:8096
File Browser:    http://pocket-grimoire.local:8080
Dozzle:          http://pocket-grimoire.local:9999
SSH:             ssh user@pocket-grimoire.local
NFS Media:       nfs://pocket-grimoire.local/srv/mediapg
Router Admin:    http://192.168.8.1

Resource Profile

Idle (At Home, Syncing)

Wiki.js + PostgreSQL:   ~250MB RAM
Jellyfin (idle):        ~150MB RAM
ZFS ARC (capped):       ~512MB RAM
System overhead:        ~200MB RAM
─────────────────────────────────
Total:                  ~1.1GB / 8GB RAM
CPU: <5%
Temperature: Cool (<60°C)

Media Playback (Direct Play)

Jellyfin (serving):     ~200MB RAM
NFS:                    ~50MB RAM
No transcoding:         0 CPU spike
─────────────────────────────────
Total:                  ~1.4GB / 8GB RAM
CPU: <10%
Temperature: Cool (<65°C)

The Pi should remain cool and quiet during all operations.


Security Notes

Encryption

  • Both SSDs use native ZFS encryption
  • Passphrases required on boot (manual unlock)
  • Family media SSD is unencrypted (for portability/sharing)
  • SSH keys are stored on encrypted Vault SSD

Network Security

  • All services bound to LAN only (not exposed to WAN)
  • Beryl AX handles firewall and VPN routing
  • No services accept connections from internet directly
  • WireGuard VPN to Netgrimoire when online

Physical Security

  • Pocket Grimoire is a physical device - keep secure
  • Encrypted SSDs protect data at rest
  • Require passphrase on boot (prevents unauthorized access)
  • Keep ZFS passphrases separate from device

Backup Strategy

  • Pocket Grimoire is a mirror, not primary storage
  • All data originates from Netgrimoire (source of truth)
  • ZFS replication provides redundancy
  • Can rebuild Pocket Grimoire from Netgrimoire if needed

Appendix A: System Specifications

Raspberry Pi 4 (8GB)

  • CPU: Broadcom BCM2711, Quad-core Cortex-A72 @ 1.5GHz
  • RAM: 8GB LPDDR4-3200
  • Storage: MicroSD (OS) + 2× USB 3.0 SSDs (data)
  • Network: Gigabit Ethernet + WiFi 5 (802.11ac)
  • Power: 5V/3A via USB-C (15W)

GL.iNet Beryl AX (GL-MT3000)

  • CPU: MediaTek MT7981B, Dual-core ARM Cortex-A53 @ 1.3GHz
  • RAM: 512MB DDR4
  • WiFi: WiFi 6 (802.11ax) dual-band
  • VPN: WireGuard, OpenVPN
  • Ports: 1× WAN, 1× LAN, 1× USB 3.0
  • Power: USB-C, 12W max

Anker Prime 200W (Model A2683)

  • Total Output: 200W
  • USB-C Ports: 4× (100W max each)
  • USB-A Ports: 2× (5V/3A, 15W max each)
  • AC Outlets: 2×
  • Surge Protection: Yes

Storage Configuration

  • SSD #1 (Vault): 1-2TB, encrypted ZFS
  • SSD #2 (Personal Media): 2TB+, encrypted ZFS
  • SSD #3 (Family Media): 2TB+, unencrypted ZFS
  • Total capacity: 5-6TB (2 active at a time)

Appendix B: Quick Reference Commands

System Status

# Check ZFS pools
sudo zpool status

# Check mounted filesystems
df -h

# Check memory usage
free -h

# Check temperature
vcgencmd measure_temp

# Check Docker containers
docker ps

# Check system load
htop

Service Management

# 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

# 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

# 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

# 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

Official Documentation

Netgrimoire Resources

  • Main documentation: (link to your Netgrimoire Wiki)
  • Forgejo instance: (link to your Forgejo)
  • ntfy instance: (link to your ntfy server)

Tools & Utilities


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.