If you are building a plex server raspberry pi setup in 2026, the days of relying on a Class 10 microSD card and hoping for the best are over. The Raspberry Pi 5 (8GB) paired with an NVMe HAT is the definitive baseline for a reliable, high-throughput media server. It eliminates the SD card I/O bottleneck that corrupts Plex metadata databases and provides the PCIe bandwidth required to serve multiple 4K Direct Play streams simultaneously.

This guide walks through the exact hardware bill of materials, I/O pin mappings, a custom Python thermal watchdog script, and the specific Docker error strings that crash Pi-based Plex deployments.

Hardware Spec Sheet: Which Pi for Plex?

Before ordering parts, you need to understand the transcoding and I/O limits of the Pi lineup. Plex relies heavily on single-core CPU performance for database indexing and RAM for caching metadata. Here is how the recent models actually perform under a real-world Docker workload.

Board Variant RAM Storage Interface 1080p Direct Play 1080p Transcode (to 720p) Approx. Price (2026)
Raspberry Pi 3B+ 1GB USB 2.0 / SD 1-2 streams 0 (Fails immediately) $35 (Used)
Raspberry Pi 4 (4GB) 4GB USB 3.0 / SD 3-4 streams 1 stream (Software) $55
Raspberry Pi 4 (8GB) 8GB USB 3.0 / SD 4-5 streams 1-2 streams (Software) $75
Raspberry Pi 5 (8GB) 8GB PCIe 2.0/3.0 / USB 3.2 8+ streams 2-3 streams (Software) $80
Bench Note: The Pi 5 lacks a dedicated hardware video encode/decode block (like Intel QuickSync). All transcoding is done via the ARM Cortex-A76 CPU. If your client devices (Apple TV, Nvidia Shield, modern Smart TVs) support Direct Play, the Pi 5 will barely break a sweat. If you force server-side transcoding, the CPU will hit 100% utilization rapidly.

Parts List & I/O Pin Mapping

This build targets the Raspberry Pi 5 (8GB) running Raspberry Pi OS Bookworm 64-bit. We are using the official M.2 HAT+ for storage and an active cooling case to manage the Pi 5's higher thermal design power (TDP).

Bill of Materials

  • Compute: Raspberry Pi 5 (8GB variant) - Do not use the 4GB variant for Docker + Plex; the OS and Docker daemon will eat 2GB alone.
  • Storage HAT: Raspberry Pi M.2 HAT+ (NVMe)
  • Drive: WD Blue SN580 1TB NVMe SSD (PCIe Gen 3, DRAM-less but HMB-supported, runs cool)
  • Enclosure/Cooling: Argon ONE V3 Pi 5 Aluminum Case (includes integrated PWM fan and power button logic)
  • Power: Official Raspberry Pi 27W USB-C PD Power Supply (Crucial: the Pi 5 will throttle PCIe and USB ports if it doesn't detect a 5A PD handshake).

I/O and Pin Mapping Table

While the Argon ONE case handles its own fan via an onboard MCU, if you are building a custom open-air rig or adding an I2C OLED status display to monitor Plex stream counts, you need to map the Pi 5's 40-pin header correctly. Note that the Pi 5 uses the RP1 southbridge chip, which changes some peripheral behaviors compared to the Pi 4.

Component Function BCM GPIO Physical Pin Notes / Constraints
PWM Fan (Custom) PWM Control GPIO 18 Pin 12 Hardware PWM0. Use 5V on Pin 2, GND on Pin 6.
PWM Fan (Custom) Tachometer GPIO 19 Pin 35 Requires pull-up resistor for stable RPM reading.
I2C OLED Display SDA (Data) GPIO 2 Pin 3 I2C1 bus. Ensure OLED is 3.3V logic tolerant.
I2C OLED Display SCL (Clock) GPIO 3 Pin 5 I2C1 bus. Default 100kHz, can be forced to 400kHz.
M.2 HAT+ PCIe Lane N/A J1 (PCIe FPC) Defaults to Gen 2.0. Gen 3.0 requires config.txt override.

Docker Deployment & Thermal Watchdog Code

Plex on ARM is best deployed via Docker using the LinuxServer.io Plex image. However, the Pi 5 can still thermal throttle during heavy metadata scanning. Instead of relying solely on the OS fan curve, we use a Python watchdog script. This script monitors CPU temperature, adjusts a custom PWM fan, and checks if the Plex container has crashed due to thermal or memory limits.

Target Environment: Raspberry Pi 5 8GB, Raspberry Pi OS Bookworm 64-bit, Python 3.11+. The script uses gpiozero, which automatically routes through the lgpio backend on the Pi 5.

import time
import subprocess
import logging
from gpiozero import PWMOutputDevice, CPUTemperature

# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

# Pin Definitions (BCM numbering)
FAN_PIN = 18  # Physical Pin 12 (Hardware PWM0)

# Initialize PWM Fan (100Hz is standard for 4-pin PC fans)
fan = PWMOutputDevice(FAN_PIN, frequency=100, initial_value=0.2)
cpu = CPUTemperature(min_temp=30, max_temp=85)

def get_plex_container_status():
    """Checks if the Plex Docker container is running or restarting."""
    try:
        # Using docker inspect to get the exact state
        cmd = ['docker', 'inspect', '-f', '{{.State.Status}}', 'plex']
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=5)
        if result.returncode == 0:
            return result.stdout.strip()
        return 'not_found'
    except subprocess.TimeoutExpired:
        logging.error('Docker daemon timed out. System I/O might be locked.')
        return 'error'
    except Exception as e:
        logging.error(f'Failed to query Docker: {e}')
        return 'error'

def manage_thermal_profile():
    """Adjusts fan speed based on CPU temp and Plex container state."""
    temp = cpu.temperature
    plex_state = get_plex_container_status()
    
    if plex_state == 'restarting':
        logging.critical('Plex container is in a restart loop! Check OOM or Port conflicts.')
        fan.value = 1.0 # Max cooling while debugging
        return

    # Hysteresis fan curve
    if temp >= 75:
        fan.value = 1.0
        logging.warning(f'CPU Critical: {temp}C. Fan at 100%. Plex state: {plex_state}')
    elif temp >= 65:
        fan.value = 0.7
    elif temp >= 55:
        fan.value = 0.4
    else:
        fan.value = 0.2 # Idle baseline

if __name__ == '__main__':
    logging.info('Plex Thermal Watchdog started on Pi 5...')
    try:
        while True:
            manage_thermal_profile()
            time.sleep(10) # Poll every 10 seconds
    except KeyboardInterrupt:
        logging.info('Watchdog interrupted. Spinning down fan.')
    finally:
        # Safe GPIO cleanup to prevent pin lockouts on Pi 5 RP1 chip
        fan.close()
        cpu.close()
        logging.info('GPIO resources released.')

Debugging: Exit Code 137 & Port Binding Failures

When running Plex in Docker on a Pi, you will inevitably hit container crashes. Here are the exact error strings you will see in docker logs plex or docker ps, ranked by likelihood, with their fixes.

Error 1: "Bind for 0.0.0.0:32400 failed: port is already allocated."

This happens when Docker attempts to map the host network port to the container, but the Pi's host OS is already using it.

  • Cause A (Most Likely): A zombie Plex container from a previous docker-compose down failure is still holding the port. Fix: Run docker container prune and reboot.
  • Cause B: Host network conflict with Avahi-daemon or a local DNS service. Fix: Check port usage with sudo lsof -i :32400 and kill the offending PID.
  • Cause C: You accidentally set network_mode: host in your docker-compose.yml while also defining ports: - 32400:32400. Fix: Remove the ports block if using host networking.

Error 2: Container Status Shows Exit Code 137

Exit code 137 is not a Plex error; it is a Linux kernel error. It means the process was killed by SIGKILL (signal 9), almost always by the Out-Of-Memory (OOM) Killer.

  • Cause A (Most Likely): Plex's metadata scanner (analyzing video files, generating thumbnails) consumed all 8GB of RAM, and the Linux OOM killer sacrificed the container to save the OS. Fix: Add swap space to the Pi 5 (sudo dphys-swapfile swapoff, edit CONF_SWAPSIZE=2048, then swap on), or limit Plex library scan concurrency in the Plex Web UI settings.
  • Cause B: Power supply brownout. If you are not using the official 27W PD supply, the Pi 5 will throttle and crash high-draw processes under load. Fix: Verify power with vcgencmd get_throttled. If it returns anything other than 0x0, your power delivery is failing.
The First 3 Things to Check When Plex Fails:
  1. Container Logs: docker logs --tail 50 plex (Look for database corruption or XML parsing errors).
  2. Kernel OOM Messages: dmesg | grep -i oom (Confirms if the kernel assassinated Plex due to RAM starvation).
  3. Thermal/Power Throttling: vcgencmd get_throttled (Confirms if the Pi 5 is dropping PCIe/USB clocks due to heat or low voltage).

Extending and Simplifying the Build

Not everyone needs an NVMe HAT, and some users want to push the Pi 5 far beyond standard media serving. Here is how to adapt this build to your specific constraints.

How to Simplify (Budget & Space Constraints)

If the M.2 HAT+ and NVMe drive push the budget too high, drop the PCIe storage entirely. Instead, use a Samsung T7 Shield 1TB USB 3.2 SSD connected to one of the blue USB 3.0 ports. Format it as ext4 (not NTFS or exFAT, which incur massive CPU overhead on Linux via FUSE drivers) and mount it via /etc/fstab. This cuts the storage cost by 40% while maintaining enough IOPS for Plex metadata and 4K Direct Play streaming.

How to Extend (Advanced Homelab Integration)

  • Remote Access without Port Forwarding: Install Tailscale on the Pi 5. It creates a secure WireGuard mesh network, allowing you to access your Plex server from your phone anywhere in the world without exposing port 32400 to the public internet or dealing with dynamic DNS.
  • PCIe Gen 3 Override: The Pi 5 M.2 HAT defaults to PCIe Gen 2.0 (500 MB/s). If your NVMe drive supports it, you can force Gen 3.0 (1000 MB/s) by adding dtparam=pciex1_gen=3 to your /boot/firmware/config.txt. Note that the Raspberry Pi Foundation only officially guarantees Gen 2 stability, but Gen 3 works flawlessly with high-quality drives like the WD SN580.
  • Add Frigate NVR: Because the Pi 5 has vastly improved USB bandwidth and CPU headroom, you can run a Google Coral USB Accelerator alongside Plex to power Frigate NVR for local AI security camera processing, consolidating your homelab into a single node.