# 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 by `plex32_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** ```yaml 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/dri` device access plus supplementary group membership for `video` (GID 44) and `render` (GID 993). Without `group_add`, Plex silently falls back to software transcoding even with hardware acceleration enabled in settings. ### **Key Docker Commands** ```bash # 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: ```bash 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/config` is used by `plex32_backup.sh` as the backup destination β€” not by the Plex container. `/mnt/plex/transcode` exists in fstab but Plex uses the local `/opt/plex/transcode` directory 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** β€” `ignoreip` includes `127.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`: ```ini [DEFAULT] ignoreip = 127.0.0.1/8 ::1 192.168.150.0/24 ``` ### **System Timezone** - Timezone corrected to `America/New_York` on 2026-06-21 β€” was previously `Etc/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: ```bash # 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:** ```bash 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:** ```bash sudo smbios-token-ctl --activate --token-id=0x00a3 ``` **Verify current state:** ```bash sudo smbios-token-ctl --dump-tokens | grep -i -A2 "AC Power Recovery" ``` > Note: `smbios-token-ctl --dump-tokens` throws a harmless `StopIteration` traceback 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: ```bash 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 single `cp` to 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)** ```bash # 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: ```bash 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:/bin` at the top of the script so behavior is consistent under cron regardless of invoking user/environment. - The script now logs the full `docker pull` output/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 into `ALL_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-character `IFS` splitting. 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/dri` device passthrough and `group_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/config` NFS 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 from `Etc/UTC` on 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.sh` on NAS08). - SSH key from KingDezigns001 β†’ PLEX32 configured β€” enables passwordless remote launcher. - **AC Power Recovery set to "On"** (2026-06-30) via SMBIOS token `0x00a3` using `smbios-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. - `ncshare` group (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.sh` fixed 2026-07-12** β€” added explicit `PATH` export to fix intermittent `docker pull` failures under root's cron environment; script now logs real `docker pull` error 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.sh` fixed 2026-07-12** β€” fixed the "Issues Detected" email block rendering each word on its own line, caused by a multi-character `IFS='. '` split; issues are now newline-delimited and read back with a `while read` loop. --- # βœ”οΈ End of File