20 KiB
PLEX32 — Plex Media Server
Device Type: Dell Wyse 5070 Thin Client
Hostname: plex32
IP Address: 192.168.150.45
VLAN: 50 — Lab / Servers
Last Updated: 2026-07-12
🧩 Role & Purpose
PLEX32 is the dedicated Plex Media Server for the KingDezigns network.
It replaced Plex on NAS08, migrating the service to a more capable x86 machine that supports advanced Plex Pass features including Sonic Radio — an AI-driven music playlist and discovery feature that requires CPU capabilities not available on Raspberry Pi hardware.
All media files remain stored on NAS08 and are accessed via NFS mounts. PLEX32 handles only transcoding and Plex server logic — it does not store media locally.
This device is the single Plex Media Server for the KingDezigns network and is publicly accessible via plex.kingdezigns.com through NPM on HAS.
🖥️ Hardware
- Dell Wyse 5070 Thin Client
- Intel Celeron J4105 — 1.50GHz (4 cores) — Intel Quick Sync supported
- 8GB RAM
- 32GB eMMC (OS drive — no SMART support, health monitored via filesystem)
- Gigabit Ethernet
Storage
| Device | Type | Capacity | Role |
|---|---|---|---|
| mmcblk0 | eMMC | 29.1GB | OS / system drive |
| /mnt/plex/config | NFS (NAS08) | — | Plex config backup destination only — not used by container |
| /mnt/plex/transcode | NFS (NAS08) | — | Transcode scratch (local at /opt/plex/transcode) |
| /mnt/plex/media | NFS (NAS08) | 1.8TB | Media library |
Note: Plex config and transcode directories are stored locally at
/opt/plex/— not on NFS. The NFS config mount (/mnt/plex/config) is used as the backup destination byplex32_backup.sh. Only the media library is actively used by the container.
🌐 Network Placement
- VLAN: 50 — Lab / Servers
- IP: 192.168.150.45
- Access Type: LAN / Cable
This placement ensures:
- Isolation from trusted user devices
- Access to NAS08 NFS shares over local VLAN 50
- Direct access to Pi‑hole DNS
- Reduced attack surface
- Accessible via NPM reverse proxy on HAS
📦 Primary Functions
Core Services
- Plex Media Server (Docker — linuxserver/plex:latest)
- Tautulli (Docker — linuxserver/tautulli:latest) — playback history, monitoring, and alerts
Plex Libraries
| Library | Path in Container | Source on NAS08 |
|---|---|---|
| Music | /media/03 - Music | /export/kingdezigns-public/03 - Music |
| Videos | /media/04 - Videos/Movies | /export/kingdezigns-public/04 - Videos/Movies |
| Photos | /media/02 - Photos | /export/kingdezigns-public/02 - Photos |
Plex Pass
- Lifetime Plex Pass — confirmed active on this server
- Enables: Sonic Radio, hardware transcoding (Intel Quick Sync), mobile sync, offline downloads
Library Codec Profile
| Codec | Count | Notes |
|---|---|---|
| HEVC Main 10 | ~90 | 10-bit HDR — Direct Streams on Roku, hardware transcode via Quick Sync if needed |
| H.264 High | 29 | Direct Play on all clients |
| Audio | 119 AAC | 100% AAC — never forces audio transcode |
| Subtitles | 1168 SRT + 279 PGS | PGS only burns in if manually selected — Subtitle Mode set to Manually Selected |
🐳 Docker Configuration
Compose File Location
~/docker/plex/docker-compose.yml
Compose Contents
services:
plex:
image: lscr.io/linuxserver/plex:latest
container_name: plex
network_mode: host
environment:
- PUID=1000
- PGID=1001
- TZ=America/New_York
- VERSION=docker
devices:
- /dev/dri:/dev/dri
group_add:
- "44"
- "993"
volumes:
- /opt/plex/config:/config
- /opt/plex/transcode:/transcode
- /mnt/plex/media:/media
restart: unless-stopped
tautulli:
image: lscr.io/linuxserver/tautulli:latest
container_name: tautulli
environment:
- PUID=1000
- PGID=1001
- TZ=America/New_York
volumes:
- /opt/tautulli/config:/config
ports:
- 8181:8181
restart: unless-stopped
devices + group_add: Required for Intel Quick Sync hardware transcoding. The container needs
/dev/dridevice access plus supplementary group membership forvideo(GID 44) andrender(GID 993). Withoutgroup_add, Plex silently falls back to software transcoding even with hardware acceleration enabled in settings.
Key Docker Commands
# Start all services
cd ~/docker/plex && docker compose up -d
# Stop all services
cd ~/docker/plex && docker compose down
# Update Plex
cd ~/docker/plex && docker compose pull && docker compose up -d
# View Plex logs
docker logs plex --tail 50
# View Tautulli logs
docker logs tautulli --tail 50
# Check container status
docker ps
🎬 Hardware Transcoding — Intel Quick Sync
The J4105 supports Intel Quick Sync via /dev/dri. Both decode and encode are offloaded to the GPU.
Verification
Check GPU access from inside the container:
docker exec -it plex id
# Should show groups 44 (video) and 993 (render/group01b4) in output
docker exec -it plex ls -la /dev/dri
# Should show card0 and renderD128
Verify during active transcode — Plex Dashboard should show:
4K (HEVC Main 10) (hw)
SD (HEVC) — Transcode (hw)
The (hw) suffix on both lines confirms Quick Sync is handling decode and encode.
Plex Settings
- Settings → Transcoder → Use hardware acceleration when available ✅
- Settings → Transcoder → Use hardware-accelerated video encoding ✅
- Settings → Transcoder → Transcoder temporary directory:
/transcode
📊 Tautulli
Tautulli provides playback history, live session monitoring, and email alerts.
- Web UI:
http://192.168.150.45:8181 - Config:
/opt/tautulli/config - Connected to: Plex at
192.168.150.45:32400
Active Notifications
| Trigger | Destination | Purpose |
|---|---|---|
| Plex Server Down | rufus.king@kingdezigns.com | Alert when Plex stops responding |
| Plex Server Back Up | rufus.king@kingdezigns.com | Confirmation when Plex recovers |
SMTP Configuration
Uses same Zoho SMTP credentials as all other KingDezigns email alerts:
- Server:
smtppro.zoho.com/ Port:465/ TLS enabled - From:
rufus.king@kingdezigns.com
📁 NFS Mounts
Configured in /etc/fstab — mounts automatically on boot:
192.168.150.35:/export/kingdezigns-all/Docker/plex/config /mnt/plex/config nfs defaults,_netdev 0 0
192.168.150.35:/export/kingdezigns-all/Docker/plex/transcode /mnt/plex/transcode nfs defaults,_netdev 0 0
192.168.150.35:/export/kingdezigns-public /mnt/plex/media nfs defaults,_netdev 0 0
Note:
/mnt/plex/configis used byplex32_backup.shas the backup destination — not by the Plex container./mnt/plex/transcodeexists in fstab but Plex uses the local/opt/plex/transcodedirectory instead.
🌍 External Access
- Domain:
plex.kingdezigns.com - Proxy: NPM on HAS (192.168.150.30) → 192.168.150.45:32400
- SSL: Let's Encrypt via NPM
- Port: 32400 (Plex default)
- Port forwarding: UCG Max forwards external 32400 → 192.168.150.45
🔒 Required Firewall Behavior
Inbound to PLEX32
- Allowed from VLAN 1 (Infrastructure) and VLAN 20 (Trusted) — global rules
- Allowed from VLAN 50 (local VLAN)
- External Plex traffic arrives via NPM on HAS (192.168.150.30)
Outbound from PLEX32
- Subject to VLAN 50's global final drop
- Cannot initiate to VLANs 1, 10, 20, 30, 40
- Allowed: local VLAN 50 traffic (NFS to NAS08, DNS to Pi-hole, SSH to NAS08 for backup)
Critical Requirements
- Must reach NAS08 (192.168.150.35) on NFS ports for media library and backup destination
- Must be reachable from HAS (192.168.150.30) for proxied external traffic
- Port 32400 must be forwarded on UCG Max for direct Plex remote access
🔐 Security Measures
- Fail2Ban enabled —
ignoreipincludes127.0.0.1/8,::1,192.168.150.0/24 - Docker-based service isolation
- VLAN-level containment
- No outbound access to other VLANs
Fail2Ban Configuration
/etc/fail2ban/jail.local:
[DEFAULT]
ignoreip = 127.0.0.1/8 ::1 192.168.150.0/24
System Timezone
- Timezone corrected to
America/New_Yorkon 2026-06-21 — was previouslyEtc/UTC - The UTC default caused cron jobs to fire 4 hours early (e.g. 2 AM UTC = 10 PM EDT)
- Always verify timezone after fresh installs:
timedatectl | grep "Time zone" - Fix:
sudo timedatectl set-timezone America/New_York
sudo Without Password
/etc/sudoers.d/rufusking-nopasswd:
rufusking ALL=(ALL) NOPASSWD: /usr/bin/apt-get update, /usr/bin/fail2ban-client status, /usr/bin/fail2ban-client status sshd, /sbin/reboot
Required by update check script (apt-get update), health report script (fail2ban-client), and remote reboot script (reboot).
SSH Key Authentication
PLEX32 has an SSH key configured to NAS08 — required for the backup script to fetch accurate disk stats without a password prompt:
# Key location on PLEX32
~/.ssh/id_ed25519
# Test
ssh rufusking@192.168.150.35 "echo NAS08 OK"
AC Power Recovery (Auto Power-On After Outage)
Configured 2026-06-30 after a power outage left PLEX32 powered off (manual power button press required to bring it back up).
Dell exposes AC Power Recovery as a writable SMBIOS token — this means it can be set remotely from within Linux, without ever entering BIOS setup.
Tooling:
sudo apt-get install -y libsmbios-bin smbios-utils
Relevant tokens (mutually exclusive — only one should be true):
| Token | Setting | Desired State |
|---|---|---|
0x00a1 |
AC Power Recovery Mode (Off) — stays off | false |
0x00a2 |
AC Power Recovery Mode (Last) — restores prior state | false |
0x00a3 |
AC Power Recovery Mode (On) — powers on automatically | true ✅ |
Command used to set it:
sudo smbios-token-ctl --activate --token-id=0x00a3
Verify current state:
sudo smbios-token-ctl --dump-tokens | grep -i -A2 "AC Power Recovery"
Note:
smbios-token-ctl --dump-tokensthrows a harmlessStopIterationtraceback after printing all tokens (hits a malformed token entry near the end of the table) — this does not affect the accuracy of the data already printed, including the AC Power Recovery tokens.
Result: PLEX32 will now power back on automatically as soon as AC power is restored after an outage — no physical power button press required. (HAS already had equivalent behavior configured.)
🗂️ Shared Permissions
PLEX32 uses the same ncshare group as NAS08 and NAS16 for consistency across the network.
| Item | Value |
|---|---|
| Group name | ncshare |
| GID | 1001 |
| Members | rufusking |
Verify with:
getent group ncshare
💾 Config Backup
Plex and Tautulli configs are backed up to NAS08 every 3 days via plex32_backup.sh.
Backup Script
- Location:
/usr/scripts/plex32/plex32_backup.sh - Method: tar locally to
/tmp/, then singlecpto NFS (rsync avoided — too slow over NFS for Plex's thousands of small metadata files) - Plex destination (NAS08):
/export/kingdezigns-all/Docker/plex/config/backups/plex/plex-config-YYYY-MM-DD.tar.gz - Tautulli destination (NAS08):
/export/kingdezigns-all/Docker/plex/config/backups/tautulli/tautulli-config-YYYY-MM-DD.tar.gz - Retention: 3 copies of each, older ones auto-deleted
- Excludes: Cache, Logs, Crash Reports
- Email: HTML report sent on completion — SUCCESS / PARTIAL / FAILED
- SMTP password:
/etc/plex32-smtp-pass(chmod 600, owned by rufusking) - Log:
/var/log/plex32-backup/backup-YYYYMMDD.log(90 day retention)
Restore Procedure (run on plex32)
# 1. Stop containers
cd ~/docker/plex && docker compose down
# 2. Copy archive from NAS08
cp /mnt/plex/config/backups/plex/plex-config-YYYY-MM-DD.tar.gz /tmp/
# 3. Clear current config
sudo rm -rf /opt/plex/config/Library
# 4. Extract backup
sudo tar -xzf /tmp/plex-config-YYYY-MM-DD.tar.gz -C /opt/plex
sudo chown -R rufusking:ncshare /opt/plex/config
# 5. Restart containers
cd ~/docker/plex && docker compose up -d
# 6. Verify
docker ps
# Both plex and tautulli should show Up
# Open http://192.168.150.45:32400/web and confirm library and watch history intact
# 7. Cleanup
rm /tmp/plex-config-YYYY-MM-DD.tar.gz
🖥️ Remote Launcher (KingDezigns001)
A launcher script on KingDezigns001 provides a menu-driven interface to run PLEX32 scripts remotely via SSH.
- Launcher script:
~/scripts/plex32_launcher.sh - Desktop shortcut:
~/Desktop/PLEX32-Launcher.desktop - SSH: Passwordless — ED25519 key from KingDezigns001 installed on PLEX32
Menu Options
| Option | Script |
|---|---|
| 1) Update Check | /usr/scripts/plex32/plex32_update_check.sh |
| 2) Health Report | /usr/scripts/plex32/plex32_health_report.sh |
| 3) Plex32 Backup | /usr/scripts/plex32/plex32_backup.sh |
📅 Scheduled Jobs (cron)
| Schedule | Script | Purpose |
|---|---|---|
| Daily at 7:00 AM | /usr/scripts/plex32/plex32_update_check.sh |
Check for OS, Plex, Docker, Fail2Ban updates — email report |
| Daily at 7:05 AM | /usr/scripts/plex32/plex32_health_report.sh |
Server health report — disk, NFS, memory, CPU, container status |
| Every 3 days at 2:00 AM | /usr/scripts/plex32/plex32_backup.sh |
Archive Plex and Tautulli configs to NAS08 — email report |
View cron jobs:
sudo crontab -l
Note: All three PLEX32 scheduled jobs run via root's crontab, not
rufusking's.
plex32_update_check.sh — Docker Pull Fix (2026-07-12)
The daily update check was intermittently reporting Plex Media Server = Check failed — could not reach registry even though lscr.io was reachable and disk space was healthy (8.6GB free of 26GB).
Root cause: The script's docker pull step runs under root's cron environment, which uses a minimal default PATH that didn't reliably match the interactive rufusking shell environment. The script also silently discarded the actual docker pull error output, so the true failure reason was never visible in the log or email — every failure was mislabeled with the same generic "could not reach registry" text regardless of cause.
Fix applied:
- Added an explicit
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binat the top of the script so behavior is consistent under cron regardless of invoking user/environment. - The script now logs the full
docker pulloutput/exit code to/var/log/plex32-updates/on failure, and includes it as a red error block in the email report, instead of showing a generic message.
Resolved — manual sudo run completed cleanly after the fix, and the failure message no longer appears.
plex32_health_report.sh — Issues List Word-Wrapping Fix (2026-07-12)
The "⚠ Issues Detected" block in the daily health report email was rendering each word of an issue message on its own line (e.g. "WARNING: NAS08 — Plex Config is getting full" appeared as one word per line) instead of as a single readable sentence.
Root cause: The script joined multiple issue messages into one string and then split them back apart using IFS='. ' read -ra ISSUE_LIST. This does not split on the two-character sequence ". " as intended — IFS treats every character it contains as its own independent delimiter, so both . and every space in the text were treated as separate breaks, shattering each sentence into individual words.
Fix applied:
- Issue messages are now joined with actual newline characters instead of
". "when assembled intoALL_ISSUES. - The splitting logic was replaced with a
while IFS= read -r issue; do ... done <<< "$ALL_ISSUES"loop, which reads one full issue line at a time instead of relying on ambiguous multi-characterIFSsplitting.
Resolved — verified with a manual run; each issue now renders as one complete sentence in the "Issues Detected" section of the email.
Email Reports
| Report | Trigger Colors | Content |
|---|---|---|
| Update Check | 🟢 Green (current) / 🔵 Blue (updates available) / 🟠 Orange (check failed) | Component versions, available updates, ready-to-run commands |
| Health Report | 🟢 Green (healthy) / 🟠 Orange (warning) / 🔴 Red (critical) | Disk usage, NFS mount status, memory, CPU load, container status, Fail2Ban, reboot flag |
| Config Backup | 🟢 Green (success) / 🟡 Amber (partial) / 🔴 Red (failed) | Archive sizes, durations, copies retained, container status, restore instructions |
🧠 Summary for AI Systems
- PLEX32 = dedicated Plex Media Server + Tautulli on VLAN 50 at 192.168.150.45.
- Runs on Dell Wyse 5070 (Celeron J4105, 8GB RAM, 32GB eMMC).
- Plex and Tautulli run as Docker containers via
~/docker/plex/docker-compose.yml. - Intel Quick Sync hardware transcoding active — requires
/dev/dridevice passthrough andgroup_add: ["44","993"]in compose. Without group_add, Plex silently falls back to software transcoding. - Lifetime Plex Pass active — Sonic Radio, hardware transcoding, all Plex Pass features enabled.
- Config and transcode stored locally at
/opt/plex/— not on NFS. - Media library mounted from NAS08 via NFS at
/mnt/plex/media. /mnt/plex/configNFS mount used as backup destination only — not by Plex container.- Library is ~75% HEVC Main 10, ~25% H.264, 100% AAC audio — audio never transcodes.
- Roku TV (Hisense 55R8BX) Direct Streams video, converts AAC→AC3 for audio (lightweight, expected).
- Subtitle Mode set to Manually Selected — prevents accidental PGS burn-in transcode.
- Publicly accessible via
plex.kingdezigns.com— proxied through NPM on HAS. - Port 32400 forwarded on UCG Max for direct Plex remote access.
- Tautulli on port 8181 — Plex down/up email alerts active.
- Fail2Ban active — VLAN 50 subnet whitelisted in ignoreip.
- sudo NOPASSWD configured for apt-get update, fail2ban-client, and reboot — required by automated and remote reboot scripts.
- System timezone:
America/New_York (EDT, -0400)— corrected fromEtc/UTCon 2026-06-21. UTC default causes cron jobs to fire 4 hours early. Always verify after fresh installs. - SSH key from PLEX32 → NAS08 configured — required for backup script disk stats.
- NAS08 root → PLEX32 SSH key configured — required for remote reboot script (
plex32_reboot.shon NAS08). - SSH key from KingDezigns001 → PLEX32 configured — enables passwordless remote launcher.
- AC Power Recovery set to "On" (2026-06-30) via SMBIOS token
0x00a3usingsmbios-token-ctl— PLEX32 now auto powers-on after AC power is restored following an outage. Configured remotely via SSH, no BIOS access required. Tool:libsmbios-bin/smbios-utils. - Config backup runs every 3 days at 2AM — tar + NFS copy, 3 copies retained, HTML email report.
ncsharegroup (GID 1001) configured for network-wide permission consistency.- eMMC has no SMART support — health monitored via daily health report email.
- No swap configured — 7.6GB RAM, consistently low utilization.
- Plex on NAS08 is permanently disabled (restart: no in compose).
- Migrated from NAS08 on 2026-06-18 — full library, watch history, and playlists preserved.
plex32_update_check.shfixed 2026-07-12 — added explicitPATHexport to fix intermittentdocker pullfailures under root's cron environment; script now logs realdocker pullerror output on failure instead of a generic "could not reach registry" message. All three PLEX32 scheduled jobs run under root's crontab.plex32_health_report.shfixed 2026-07-12 — fixed the "Issues Detected" email block rendering each word on its own line, caused by a multi-characterIFS='. 'split; issues are now newline-delimited and read back with awhile readloop.