40 KiB
| 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
vaultpgpool (Vault SSD, always connected)mediapgpool (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/mediapgto LAN (read-only) - Laptop and Onn boxes mount for media access
- Exports
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:
-
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
-
PostgreSQL - Wiki.js database backend
- Stored on Vault SSD
- Tuned for 16K recordsize (Postgres optimal)
-
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:
-
File Browser - Read-only web UI
- Quick LAN access to vault/media without SSH
- Port: 8080
-
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.localwiki.pocket-grimoire.localmedia.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:
-
Disable User Registration:
- Administration → Users → Settings
- Disable "Allow self-registration"
-
Configure Read-Only Permissions:
- Administration → Groups
- Edit "Guests" group (or create "Readers" group)
- Permissions:
- ✓ Read pages
- ✗ Create pages
- ✗ Edit pages
- ✗ Delete pages
- ✗ Upload files
-
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
- Remote:
-
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 -
Add Deploy Key to Forgejo:
- Copy public key
- Forgejo → Repository → Settings → Deploy Keys
- Add key (read-only access)
-
Import Content:
- Administration → Storage → Git
- Click "Import Content" or "Force Sync"
- Verify pages appear
Jellyfin Configuration
Critical: Disable All Transcoding
-
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)
-
Dashboard → Libraries:
- Add Library:
/media/library/movies - Content type: Movies
- Add Library:
/media/library/tv - Content type: TV Shows
- Add Library:
-
Dashboard → Networking:
- Published Server URL:
http://pocket-grimoire.local:8096 - Enable automatic port mapping: No
- Published Server URL:
-
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)
- Unpack Pocket Grimoire enclosure
- Connect Beryl AX to hotel WiFi (configure via phone app)
- Connect Pi to Beryl AX via Ethernet
- Plug Anker Prime into wall outlet
- Connect all USB devices to Anker Prime
- 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)
- Power on Onn streaming box
- Connect to hotel TV via HDMI
- Configure Onn to connect to Beryl AX WiFi network
- Install Jellyfin app on Onn (if not already installed)
- Open Jellyfin app
- Add server:
http://pocket-grimoire.local:8096 - 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
- Check power LED on Pi (should be solid red)
- Check ACT LED (should blink green during boot)
- If no LEDs: Check USB-C cable and Anker USB-A port
- 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
-
During playback, click info icon
-
If "Transcoding" appears:
- Media is not H.264/AAC
- Re-encode media before next trip
- Do NOT allow transcoding on Pi (will overheat/crash)
-
Verify media codec:
ffprobe /srv/mediapg/library/movies/example.mkv -
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
- Access Beryl AX admin panel:
http://192.168.8.1 - Navigate to: Internet → Repeater
- Scan for hotel WiFi networks
- Connect (may require captive portal login)
- 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:
- Unplug Ethernet cable from Pi (stops network activity)
- Wait 10 seconds
- Unplug power from Anker Prime
- 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
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.