---
name: plex-setup
description: Deploy and configure Plex Media Server on a self-hosted Linux server — Docker, GPU hardware transcoding, network configuration, media library management, and troubleshooting.
version: 1.1.0
author: Hermes Agent
license: MIT
platforms: [linux]
metadata:
  hermes:
    tags: [plex, media-server, docker, nvidia, transcoding, self-hosted]
    category: self-hosting
related_skills: [docker-gpu-acceleration, samba-nas]
---

# Plex Media Server Setup

Deploy and manage Plex Media Server via Docker on a self-hosted Linux server. Covers first-run setup, GPU hardware transcoding, network configuration, media library management, sonic analysis, Plex API interaction, and troubleshooting.

## When to Use

- User asks to set up Plex on their homelab/server
- User wants hardware-accelerated transcoding (NVIDIA, Intel QuickSync)
- User wants to add media drives to an existing Plex container
- User asks about sonic analysis, Plexamp DJ, or "sonically similar" features
- User needs to query or manage Plex programmatically via API
- User is migrating Plex to a new server

## Prerequisites

- Docker + docker-compose-v2 installed
- NVIDIA driver installed (for NVIDIA GPU transcoding) OR Intel GPU for QuickSync
- nvidia-container-toolkit configured (for NVIDIA GPUs)
- Plex account and an active Plex Pass (for hardware transcoding)

## Setup Steps

### 1. Create directory structure

```bash
mkdir -p ~/docker/plex/config
```

- `config/` — Persistent Plex data (database, metadata, preferences)

### 2. Create docker-compose.yml

```yaml
services:
  plex:
    image: plexinc/pms-docker:latest
    container_name: plex
    network_mode: host
    environment:
      - TZ=America/New_York
      - PLEX_CLAIM=claim-<your-claim-token>
      - ADVERTISE_IP=http://<server-lan-ip>:32400/
    volumes:
      - /home/ray/docker/plex/config:/config
      - /dev/shm:/transcode
    devices:
      - /dev/dri:/dev/dri
    restart: unless-stopped
```

**Key configuration details:**

- **`network_mode: host`** — Required for DLNA/GDM discovery, remote access, and automatic local network detection. Without host mode, Plex can't broadcast its presence on the LAN.
- **`PLEX_CLAIM`** — One-time token from https://plex.tv/claim. Expires ~5 minutes after generation. Set it, start the container once, then remove it (optional but recommended after first run).
- **`ADVERTISE_IP`** — Must be the server's actual LAN IP (check with `ip -4 addr show | grep -oP '(?<=inet\s)\d+\.\d+\.\d+\.\d+' | grep -v 127.0.0.1`). Required for proper remote access configuration. DO NOT guess this — verify the IP first.
- **`/dev/dri`** — Device passthrough for hardware transcoding. Works for both NVIDIA and Intel GPUs. NVIDIA also needs the `nvidia` Docker runtime in some configurations, but `/dev/dri` alone covers VA-API/NVDEC for Plex.
- **`/dev/shm:/transcode`** — RAM-backed transcode directory. Prevents SSD wear from heavy transcoding and improves performance. Ensure the server has enough RAM (minimum 2GB free for 4K transcodes).

### 3. Get a claim token and start

1. Visit https://plex.tv/claim and copy the token (expires in ~5 minutes)
2. Add `PLEX_CLAIM=claim-<token>` to the compose file
3. Start the container:
   ```bash
   cd ~/docker/plex && docker compose up -d
   ```
4. Verify it's healthy:
   ```bash
   docker ps --filter name=plex
   docker logs plex | tail -10
   ```
5. Look for "Token obtained successfully" in logs — confirms the server is linked to your Plex account

### 4. Access and configure

Open `http://<server-ip>:32400/web` in a browser. The server should appear as claimed and ready. If it doesn't automatically detect:
- Check `ADVERTISE_IP` is correct
- Restart the container with updated config

### 5. Add media libraries (after first run)

Once the server is running and claimed, add media mounts as volumes. **Do this after the first run** — get the server healthy and accessible first, then add media:

```yaml
volumes:
  - /home/ray/docker/plex/config:/config
  - /dev/shm:/transcode
  - /mnt/media/Videos:/data/media/Movies:ro
  - /mnt/media/Music:/data/media/Music:ro
  - /mnt/wd-passport/Videos:/data/media/WDVideos:ro
```

**Pitfall:** Binding mount paths into `/data/media/` is convention, not requirement. Plex lets you point at any path inside the container. Use whatever layout makes sense, then add libraries via the Plex web UI pointing at the container paths.

Recreate the container to apply the new volume mounts (restart won't pick them up):
```bash
docker compose up -d plex
```

**Why:** `docker compose restart` reuses the existing container config — new volume mounts won't appear. `up -d` recreates with the updated compose file, which is required for volume, device, or environment changes.

### 6. Verify hardware transcoding

1. Play a media file that requires transcoding (high-bitrate 4K, or audio format the client doesn't support)
2. Check Plex Dashboard (web UI top-right activity icon) or:
   ```bash
   # In the Plex logs
   docker logs plex 2>&1 | grep -i "hwaccel\|transcode\|nvidia\|nvdec"
   ```
3. On a successful HW transcode, you should see lines indicating NVDEC/NVENC or VA-API usage

## Pitfalls

- **`PLEX_CLAIM` expired** — The token is only valid for ~5 minutes. If you didn't start the container in time, remove `PLEX_CLAIM` from the compose, restart, and the server remains unclaimed. To re-claim: generate a new token, re-add it, run `docker compose up -d` (NOT restart — `restart` doesn't re-read env vars, only `up -d` does), then remove the token after success.
- **`ADVERTISE_IP` wrong** — If you guess the IP, remote access and local discovery will fail. Always check `ip -4 addr show` and set it to the correct LAN IP before first start.
- **`libusb_init failed` in logs** — Harmless. Means no USB TV tuner is attached. Ignore it.
- **`chown: missing operand after 'plex:plex'` in logs** — Cosmetic during first init. Harmless.
- **Hardware transcoding not working** — Check that `/dev/dri` is accessible inside the container: `docker exec plex ls -la /dev/dri`. If the device doesn't exist, verify NVIDIA driver is installed and the container was created with `--device /dev/dri`. For NVIDIA specifically, you may also need `runtime: nvidia` in the compose or `--gpus all` on `docker run`.
- **Plex not showing on local network** — Host network mode usually fixes this. If still not visible, check firewall rules (ufw/iptables blocking UDP 1900 for SSDP).
- **Media mount paths** — Use `:ro` (read-only) for all media mounts. Plex only reads from these directories; write access is unnecessary and risks accidental file modification.
- **`docker compose restart` vs `up -d`** — `compose restart` reuses the existing container config and only restarts the process. `compose up -d` recreates the container with any config changes. Use `up -d` when you change `environment`, `devices`, or `volumes`; use `restart` for a simple process restart.
- **Plexamp DJ blocked after sonic analysis** — Even with completed analysis, Plexamp DJ features may still show *"Requires Sonic Analysis"*. Top causes: (1) `MusicAnalysisBehavior` server pref is `manual` instead of `scheduled` — Plexamp reads this server-level flag and blocks DJ when it's not `scheduled`. Fix: `PUT /:/prefs?MusicAnalysisBehavior=scheduled`. (2) Plexamp cached old server state — force-quit and reopen. (3) Sonic analysis indexes need reload — restart the Plex container. See `references/plex-management.md` for full troubleshooting.
- **Plex API token mangled by bash shell** — Plex tokens like `fjCWcQF_trzJHgYKhRXV` contain underscores and other chars that bash may mangle in shell string expansion. If `curl -s "http://...?X-Plex-Token=$TOKEN"` returns 401 but the token is correct, the shell is the culprit. **Use Python urllib instead**: read the token with `xml.etree.ElementTree`, build the URL with `urllib.request.Request`, and the token passes through unmolested. The `curl` command works fine when the token is copied literally — the problem is bash variable interpolation, not the API itself.
- **Compilation folders showing as "Various Artists"** — When Plex scans a parent folder containing multiple compilation subfolders, it can cross-contaminate album grouping even with `respectTags=true`. **Fix: add each subfolder as an individual library location instead of the parent.** Process: (1) Insert rows into `section_locations` table in the Plex SQLite DB (`com.plexapp.plugins.library.db`) with `library_section_id` and `root_path` for each subfolder. (2) Delete the old parent location row. (3) Restart Plex container and trigger a library refresh. This isolates each compilation folder so they appear as distinct entities in Plex. Use Python + sqlite3 module (not the `sqlite3` CLI, which may not be installed).
- **`respectTags` change has no effect on existing items** — Changing `respectTags` from false→true (or vice versa) does NOT cause Plex to re-read embedded tags on already-matched albums. "Refresh Metadata" in the web UI and `GET /refresh?force=1` will appear to do nothing. You MUST either touch all files to update their mtime (so Plex sees them as "changed") then run a forced scan, or perform the Plex Dance (remove location→scan→empty trash→re-add location→scan) to reprocess from scratch. See `references/plex-management.md` for both workflows.

## Verification Checklist

- [ ] Container is `Up` and `healthy` in `docker ps`
- [ ] `http://<server-ip>:32400/web` loads the Plex web interface
- [ ] Server appears as claimed (linked to Plex account)
- [ ] `/dev/dri` is accessible inside the container
- [ ] Media libraries are scannable after adding mounts
- [ ] Hardware transcoding is active (check Plex Dashboard during a transcode)
- [ ] Remote access is configured (if desired)

## Managing Plex After Setup

For programmatic management — API endpoints, sonic analysis, database queries, butler task scheduling, tag-based artist/album grouping, file permission troubleshooting (DOSATTRIB xattrs), and the \"Various Artists\" fix — see **`references/plex-management.md`**. For Plex 1.43.x `--analyze-loudness` regression / DJ Friendgänger blockage, see **`references/plex-1.43-loudness-debug.md`** (full investigation + diagnostic workflow).\n\nBundled scripts:\n- `scripts/fix-compilation-tags.py` — add `compilation=1` + `albumartist=Various Artists` to untagged tracks (for singles/compilations that should appear as standalone entries)\n- `scripts/strip-various-artists.py` — REMOVE `album_artist=Various Artists` and empty `album_artist` from files so Plex uses the track-level `artist` tag (for tracks wrongly grouped under \"Various Artists\")

Covers:
- Plex HTTP API (sections, preferences, activities, tokens)
- Sonic analysis: enabling, triggering immediately, monitoring progress
- Butler scheduling and forced task execution
- Database inspection (fingerprint state, activity history)
- Common pitfalls (fpcalc myths, butler timing, progress interpretation)
