# Plex Media Server Docker User Guide

> **Plex Media Server in a container**, the official way. `plexinc/pms-docker`
> is the canonical Docker image published and maintained by Plex, Inc. for
> running Plex Media Server (PMS) on any Docker-capable host: a Linux server,
> a NAS (Synology, QNAP, unRAID), a small homelab box, or a Kubernetes cluster
> via the bundled Helm chart. This guide covers everything a home user, NAS
> operator, or sysadmin needs to install, configure, and operate a containerized
> Plex server end-to-end.

## 1. Quick Start

**Plex Media Server** organizes your personal video, music, and photo
collections and streams them to any Plex-compatible client (TV, phone, web,
console, set-top box). The `plexinc/pms-docker` image is the official
containerized build, refreshed on every PMS release for `linux/amd64`,
`linux/arm64`, and `linux/arm/v7` (32-bit ARM with NEON, e.g., Raspberry Pi 3).

**Key features:**

- Official multi-arch Docker image, published by Plex, Inc.
- Three networking modes: **host**, **bridge**, **macvlan** (compose
  templates included for all three)
- Auto-update via the `beta` or `public` tags, or pin to a specific PMS
  version for stability
- Built-in healthcheck that probes `http://localhost:32400/identity`
- Intel Quick Sync (`/dev/dri`) and NVIDIA GPU hardware-transcoding
  passthrough
- Auto-detected DVB tuners (`/dev/dvb`) for Plex Live TV
- s6-overlay process supervision (graceful shutdown, configurable UID/GID,
  device-group permission setup)
- Official Helm chart (`charts/plex-media-server`) for Kubernetes deployments
- unRAID community-app template (`plex-unRAID.xml`) included

**Prerequisites:**

- Docker Engine 20.10+ (Linux host strongly preferred)
- A Plex.tv account
- Around 10 GB of free disk for `/config` (large libraries can grow to
  100+ GB) plus headroom on `/data` for your media

**Fastest path to a running server** (host networking, single command):

```bash
docker run -d \
  --name plex \
  --network=host \
  --restart=unless-stopped \
  -e TZ="America/New_York" \
  -e PLEX_CLAIM="claim-xxxxxxxxxxxxxxxxxxxx" \
  -v /opt/plex/config:/config \
  -v /opt/plex/transcode:/transcode \
  -v /mnt/media:/data \
  plexinc/pms-docker
```

Expected output:

```text
Unable to find image 'plexinc/pms-docker:latest' locally
latest: Pulling from plexinc/pms-docker
... (layers)
Status: Downloaded newer image for plexinc/pms-docker:latest
a1b2c3d4e5f6...
```

Open `http://<host-ip>:32400/web` in any browser to finish setup. The
server is already claimed under your Plex account because you supplied a
`PLEX_CLAIM` token.

> [!TIP]
> Get a fresh claim token from [plex.tv/claim](https://www.plex.tv/claim)
> right before running the command. Tokens expire after roughly four
> minutes, so generate it last. Once the server is claimed, the variable
> is ignored on every subsequent start, so you do not have to remove it
> from your compose file.

**What to try next:**

- Pick a permanent install method (Compose, Helm, unRAID) in section 2.
- Tune environment variables and volume mounts in section 3.
- Configure hardware transcoding, choose a networking mode, and turn on
  auto-updates in section 4.
- Run through the 8 most common day-2 tasks in section 5.

---

## 2. Installation

### System Requirements

| Requirement | Minimum | Recommended |
|---|---|---|
| Docker Engine | 20.10+ | Latest stable on a Linux host |
| Host OS | Linux, macOS, Windows (with caveats) | Linux (Ubuntu, Debian, Fedora, Synology DSM, unRAID) |
| Architecture | `amd64`, `arm64`, or `arm/v7` (NEON) | `amd64` for transcoding, `arm64` for low-power NAS |
| RAM | 2 GB | 4 GB+ for transcoding |
| `/config` disk | 2 GB | 50-200 GB for large libraries (poster art, indexes) |
| `/transcode` disk | 10 GB | SSD or `tmpfs` for fast transcodes |
| Network port | TCP `32400` | Plus UDP discovery ports for full LAN features |

> [!WARNING]
> Running this image on **Docker Desktop for Windows or macOS** is not
> recommended for production use. Volumes mounted from the host pass
> through SMB/9PFS, neither of which honor file locking the way SQLite
> needs. The Plex database **will** corrupt sooner or later on those
> volume types. If you must run it there, store `/config` inside a
> Docker-managed named volume rather than a host bind mount.

### Method 1: `docker run`

The repo ships three reference Compose templates (bridge, host, macvlan)
that document the exact ports and volumes. The same options translate to
`docker run` flags directly.

=== "Host networking"
    ```bash
    docker run -d \
      --name plex \
      --network=host \
      --restart=unless-stopped \
      -e TZ="America/New_York" \
      -e PLEX_CLAIM="<claimToken>" \
      -v /opt/plex/config:/config \
      -v /opt/plex/transcode:/transcode \
      -v /mnt/media:/data \
      plexinc/pms-docker
    ```

=== "Bridge networking"
    ```bash
    docker run -d \
      --name plex \
      --restart=unless-stopped \
      -p 32400:32400/tcp \
      -p 8324:8324/tcp \
      -p 32469:32469/tcp \
      -p 1900:1900/udp \
      -p 32410:32410/udp \
      -p 32412:32412/udp \
      -p 32413:32413/udp \
      -p 32414:32414/udp \
      -e TZ="America/New_York" \
      -e PLEX_CLAIM="<claimToken>" \
      -e ADVERTISE_IP="http://192.168.1.100:32400/" \
      -h PlexServer \
      -v /opt/plex/config:/config \
      -v /opt/plex/transcode:/transcode \
      -v /mnt/media:/data \
      plexinc/pms-docker
    ```

=== "Macvlan networking"
    ```bash
    docker run -d \
      --name plex \
      --network=physical \
      --ip=192.168.1.50 \
      --restart=unless-stopped \
      -e TZ="America/New_York" \
      -e PLEX_CLAIM="<claimToken>" \
      -h PlexServer \
      -v /opt/plex/config:/config \
      -v /opt/plex/transcode:/transcode \
      -v /mnt/media:/data \
      plexinc/pms-docker
    ```

### Method 2: Docker Compose

The repo provides three Compose templates: `docker-compose-host.yml.template`,
`docker-compose-bridge.yml.template`, and `docker-compose-macvlan.yml.template`.
Copy one, fill in the placeholders, and run `docker compose up -d`.

=== "Host networking"
    ```yaml
    services:
      plex:
        image: plexinc/pms-docker
        container_name: plex
        restart: unless-stopped
        network_mode: host
        environment:
          - TZ=America/New_York
          - PLEX_CLAIM=<claimToken>
        volumes:
          - /opt/plex/config:/config
          - /opt/plex/transcode:/transcode
          - /mnt/media:/data
    ```

=== "Bridge networking"
    ```yaml
    services:
      plex:
        image: plexinc/pms-docker
        container_name: plex
        restart: unless-stopped
        hostname: PlexServer
        ports:
          - 32400:32400/tcp
          - 8324:8324/tcp
          - 32469:32469/tcp
          - 1900:1900/udp
          - 32410:32410/udp
          - 32412:32412/udp
          - 32413:32413/udp
          - 32414:32414/udp
        environment:
          - TZ=America/New_York
          - PLEX_CLAIM=<claimToken>
          - ADVERTISE_IP=http://192.168.1.100:32400/
        volumes:
          - /opt/plex/config:/config
          - /opt/plex/transcode:/transcode
          - /mnt/media:/data
    ```

### Method 3: Kubernetes via the Official Helm Chart

The repository ships a first-class Helm chart at
`charts/plex-media-server` (chart version `1.5.0`, app version `1.43.0`).

```bash
helm repo add plex https://raw.githubusercontent.com/plexinc/pms-docker/gh-pages
helm repo update
helm show values plex/plex-media-server > values.yaml
# edit values.yaml as needed
helm upgrade --install plex plex/plex-media-server \
  --namespace plex --create-namespace \
  --values values.yaml
```

The chart deploys a **single-replica StatefulSet** with a
`PersistentVolumeClaim` for `/config` (default `2Gi`, almost certainly too
small for any real library — bump it).

> [!IMPORTANT]
> Plex Media Server is fundamentally a single-instance application —
> the SQLite database does not support multiple writers. The chart is
> intentionally a single-replica StatefulSet. Do not try to scale it
> horizontally; you will corrupt the database.

### Method 4: unRAID

An unRAID community-applications template (`plex-unRAID.xml`) is included
in the repo. Install via the **Community Applications** plugin, search for
**Plex Media Server**, pick the official `plexinc/pms-docker` template,
and fill in the form. The template defaults to host networking, mounts
`/config`, `/transcode`, and `/data`, and exposes `PLEX_CLAIM`, `PLEX_UID`
(`99`), `PLEX_GID` (`100`), and `VERSION` as form fields.

### Post-Install Verification

Confirm the container is healthy:

```bash
docker ps --filter name=plex
```

```text
CONTAINER ID   IMAGE                STATUS                   PORTS    NAMES
a1b2c3d4e5f6   plexinc/pms-docker   Up 2 minutes (healthy)            plex
```

Hit the identity endpoint directly:

```bash
docker exec plex curl -s http://localhost:32400/identity
```

```text
<?xml version="1.0" encoding="UTF-8"?>
<MediaContainer size="0" claimed="1" machineIdentifier="..." version="1.43.0..."/>
```

> [!NOTE]
> The image declares `HEALTHCHECK --interval=5s --timeout=2s --retries=20`
> against `/healthcheck.sh`, which calls
> `curl http://localhost:32400/identity`. If `docker ps` reports
> `(unhealthy)`, the PMS process inside the container has not started
> or has crashed — check `docker logs plex` first.

---

## 3. Configuration

### Environment Variables

These are the variables the image's init scripts and the underlying PMS
binary actually consume. Anything not in this table is not honored.

| Variable | When applied | Default | What it does |
|---|---|---|---|
| `TZ` | Every start | (unset) | Container timezone (e.g., `Europe/London`, `America/Los_Angeles`). Used by PMS for scheduled scans and DVR. |
| `PLEX_CLAIM` | First run only | (unset) | Claim token from [plex.tv/claim](https://www.plex.tv/claim). Exchanged once with `plex.tv` for a permanent server token; ignored afterward. |
| `PLEX_CLAIM_FILE` | First run only | (unset) | Path *inside the container* to a file holding the claim token. Useful with Docker secrets or Kubernetes Secrets. |
| `ADVERTISE_IP` | First run only | (unset) | Comma-separated list of full URLs the server should advertise (e.g., `http://192.168.1.100:32400/`). Required for **bridge** networking. |
| `ALLOWED_NETWORKS` | First run only | (unset) | CIDR list of networks allowed to access PMS without authentication. Example: `192.168.1.0/24,10.0.0.0/8`. |
| `PLEX_UID` | Every start | (image default) | Numeric UID assigned to the in-container `plex` user (set via `usermod -o -u`). Match your host owner. |
| `PLEX_GID` | Every start | (image default) | Numeric GID for the `plex` group (`groupmod -o -g`). |
| `CHANGE_CONFIG_DIR_OWNERSHIP` | Every start | `true` | If `true`, runs `chown -h plex:plex` against `/config` and `/transcode`. Set `false` to skip on big libraries. |
| `HOSTNAME` | Every start | (auto) | Container hostname. Most users prefer the `-h <name>` Docker flag. Not relevant in host networking. |
| `PLEX_UPDATE_CHANNEL` | Every start | (auto) | Override the update channel for `beta`/`public` tags: `8` = beta, `16` = public. |
| `DEBUG` | Every start | `false` | Set to `true` for `set -x` shell tracing in init scripts when troubleshooting startup. |

> [!TIP]
> Run `id $(whoami)` on the host to find your own UID/GID, then set
> `PLEX_UID` and `PLEX_GID` to match. This is by far the most common
> fix for "Plex can't see my media" complaints.

### Volume Mounts

| Container path | Required? | What lives here |
|---|---|---|
| `/config` | Yes | The PMS database, preferences, plugins, metadata, posters. Path inside is `Library/Application Support/Plex Media Server/`. **Must** be on a filesystem that supports POSIX file locking. |
| `/transcode` | Recommended | Scratch space for the transcoder. Sized for active streams (tens of GB). Map to fast local storage or `tmpfs` for best results. |
| `/data` | Yes | Where you mount your media. You can mount multiple host directories under `/data/movies`, `/data/tv`, `/data/music`, etc. |

> [!CAUTION]
> Never put `/config` on NFS, SMB/CIFS, 9PFS, or a Windows host bind mount.
> SQLite requires byte-range locking; those filesystems silently
> mis-implement it and the database **will** corrupt. The Plex
> community has been bitten by this for years — assume it applies to you.

### Network Ports

| Port | Protocol | Used for |
|---|---|---|
| `32400` | TCP | Web UI, API, all client traffic. **Required.** |
| `8324` | TCP | Roku Plex Companion control |
| `32469` | TCP | DLNA server |
| `1900` | UDP | DLNA / SSDP discovery |
| `32410` | UDP | GDM (Plex auto-discovery) |
| `32412` | UDP | GDM |
| `32413` | UDP | GDM |
| `32414` | UDP | GDM |

> [!NOTE]
> Host and macvlan networking expose every port on the host
> automatically. Bridge networking requires explicit `-p`/`ports`
> mappings — that is why bridge mode also needs `ADVERTISE_IP`.

For the full configuration reference (Helm values, init-script
behavior, build args, internal env vars), see the
**Plex Media Server Docker Administrator & Developer Guide**.

---

## 4. Core Usage

### Choosing a Networking Mode

| Mode | Pros | Cons | Picks for you |
|---|---|---|---|
| **host** | Simplest. Auto-discovery works. Plex sees real client IPs. Remote-access auto-config works. | Container shares host's network namespace, so you can't run two Plex servers on one host without changing PMS ports. | Most users on a dedicated Plex box |
| **bridge** | Container is isolated; can run alongside other services that bind 32400 | You must manually map every port and set `ADVERTISE_IP`. Plex sees Docker's gateway IP, not real clients (bandwidth heuristics suffer). Remote access requires manual port forwarding. | Multi-tenant Docker hosts where 32400 is already taken |
| **macvlan** | Container gets its own LAN IP — looks like a separate device on the network | Requires a `macvlan` network created at the Docker level. Host cannot talk to the container directly. | Power users; corporate-style segmented LANs |

> [!IMPORTANT]
> In bridge mode, Plex sees all clients as coming from the Docker gateway
> (often `172.17.0.1`). Configure **Settings > Server > Network > LAN
> Networks** with your real LAN CIDR (e.g., `192.168.1.0/24`) so PMS
> applies LAN-bandwidth rules instead of WAN-bandwidth rules.

### Image Tags and Auto-Updates

| Tag | Behavior |
|---|---|
| `latest` | A specific PMS version, frozen at the time the image was built. To update, pull a new image. |
| `<version>` (e.g., `1.43.0.10492-121068a07`) | Pinned to that exact PMS build. Stable, reproducible, recommended for production. |
| `public` | No PMS binary baked in. On every container start, `cont-init.d/50-plex-update` queries `https://plex.tv/downloads/details/5` and installs the latest **public** release. |
| `beta` | Same as `public` but pulls the **beta** channel. Requires Plex Pass. Falls back gracefully when the account isn't subscribed. |

> [!WARNING]
> The `public` and `beta` tags **download and install Plex on every
> container start**. That means slower startup, a network requirement,
> and a moving target across restarts. For a stable home server,
> pin to a specific version tag.

Update a fixed-version container:

```bash
docker pull plexinc/pms-docker:latest
docker stop plex && docker rm plex
# re-run your original docker run / compose up -d
```

Update a `beta`/`public` container — just restart it:

```bash
docker restart plex
```

### Hardware Transcoding (Intel Quick Sync)

If your CPU has an Intel iGPU with Quick Sync Video, pass `/dev/dri`
through. The image's `cont-init.d/45-plex-hw-transcode-and-connected-tuner`
script automatically detects character devices in `/dev/dri` (and
`/dev/dvb` for tuners), reads each device's GID, creates a matching
group inside the container if one doesn't exist, and adds the `plex`
user to it.

```bash
docker run -d \
  --name plex \
  --network=host \
  --device=/dev/dri:/dev/dri \
  -e TZ="America/New_York" \
  -e PLEX_CLAIM="<claimToken>" \
  -v /opt/plex/config:/config \
  -v /opt/plex/transcode:/transcode \
  -v /mnt/media:/data \
  plexinc/pms-docker
```

Then in the Plex web app: **Settings > Server > Transcoder > Show
Advanced > Use hardware acceleration when available** (and **Use
hardware-accelerated video encoding**).

> [!NOTE]
> Hardware transcoding is a **Plex Pass** feature. Without a Plex Pass
> subscription, those toggles are disabled and the server falls back
> to software transcoding regardless of GPU passthrough.

### Hardware Transcoding (NVIDIA)

For NVIDIA GPU transcoding, install the
[NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/)
on the host and pass GPUs in:

```bash
docker run -d \
  --name plex \
  --network=host \
  --gpus all \
  -e TZ="America/New_York" \
  -e PLEX_CLAIM="<claimToken>" \
  -e NVIDIA_VISIBLE_DEVICES=all \
  -e NVIDIA_DRIVER_CAPABILITIES=compute,video,utility \
  -v /opt/plex/config:/config \
  -v /opt/plex/transcode:/transcode \
  -v /mnt/media:/data \
  plexinc/pms-docker
```

The Helm chart automates all of this when `pms.gpu.nvidia.enabled=true`.

### Headless Server Setup (No Claim Token)

If you start the container without `PLEX_CLAIM`, the first-run setup
wizard refuses to load remotely (Plex insists you connect from
`localhost`). Tunnel through SSH:

```bash
ssh user@plex-host -L 32400:plex-host:32400 -N
```

Then open `http://localhost:32400/web` on your laptop.

### Container Management Cheatsheet

| Action | Command |
|---|---|
| Start | `docker start plex` |
| Stop | `docker stop plex` |
| Restart (also re-runs auto-update on `beta`/`public`) | `docker restart plex` |
| Shell in | `docker exec -it plex /bin/bash` |
| Live logs | `docker logs -f plex` |
| Healthcheck | `docker exec plex /healthcheck.sh && echo OK` |
| Fine-grain service control | `docker exec plex /plex_service.sh -r` (`-u` up, `-d` down, `-r` restart) |

---

## 5. Common Tasks

### Recipe 1: First-Time Plex Pass Claim Flow

**Goal:** Bring up a brand-new server, claim it under your Plex account,
and complete the setup wizard.

**Prerequisites:** A Plex.tv account (Plex Pass optional).

1. Visit [plex.tv/claim](https://www.plex.tv/claim) while logged in.
   Copy the `claim-...` token.
2. Within four minutes, run the Quick Start command from section 1
   (or paste the token into your compose file and `docker compose up -d`).
3. Watch the logs for the claim handshake:

   ```bash
   docker logs -f plex 2>&1 | grep -i claim
   ```

   ```text
   Attempting to obtain server token from claim token
   Token obtained successfully
   ```

4. Open `http://<host-ip>:32400/web`. The web UI should already show
   **Welcome to Plex** — finish the wizard (server name, libraries,
   remote access).
5. **Verify:** `docker logs plex` ends with
   `Plex Media Server first run setup complete`.

**If it fails:** A `Failed to obtain server token` line means the claim
expired before exchange — re-grab it and restart.

### Recipe 2: Mount a NAS Media Share

**Goal:** Stream media that lives on a separate NAS over NFS.

1. Mount the NAS share on the Docker host (NOT inside the container —
   the container should see it as a local bind mount):

   ```bash
   sudo mkdir -p /mnt/nas-media
   echo 'nas.lan:/volume1/media /mnt/nas-media nfs ro,vers=4,_netdev 0 0' \
     | sudo tee -a /etc/fstab
   sudo mount /mnt/nas-media
   ```

2. Bind-mount it as `/data` in your compose file:

   ```yaml
   volumes:
     - /opt/plex/config:/config
     - /opt/plex/transcode:/transcode
     - /mnt/nas-media:/data:ro
   ```

3. Restart Plex; add libraries pointed at `/data/movies`, `/data/tv`, etc.

> [!CAUTION]
> Never mount the NAS as `/config`. NFS does not support SQLite locking
> reliably and will corrupt the Plex database. NAS for media (`/data`)
> only — keep `/config` on local disk.

### Recipe 3: Docker Compose with Watchtower Auto-Updates

**Goal:** Have a separate container watch for new `plexinc/pms-docker`
images and rebuild Plex automatically.

```yaml
services:
  plex:
    image: plexinc/pms-docker:latest
    container_name: plex
    restart: unless-stopped
    network_mode: host
    environment:
      - TZ=America/New_York
    volumes:
      - /opt/plex/config:/config
      - /opt/plex/transcode:/transcode
      - /mnt/media:/data
    labels:
      - "com.centurylinklabs.watchtower.enable=true"

  watchtower:
    image: containrrr/watchtower
    container_name: watchtower
    restart: unless-stopped
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      - WATCHTOWER_CLEANUP=true
      - WATCHTOWER_LABEL_ENABLE=true
      - WATCHTOWER_SCHEDULE=0 0 4 * * *  # 4am daily
```

> [!TIP]
> Combine Watchtower with `image: plexinc/pms-docker:latest` rather
> than the `public`/`beta` tags. Watchtower handles image churn at
> the registry level; the dynamic tags handle it at startup. Mixing
> both creates redundant downloads.

### Recipe 4: GPU Passthrough (Intel iGPU and NVIDIA)

**Goal:** Move transcoding off the CPU.

=== "Intel Quick Sync"
    ```yaml
    services:
      plex:
        image: plexinc/pms-docker
        network_mode: host
        devices:
          - /dev/dri:/dev/dri
        environment:
          - TZ=America/New_York
        volumes:
          - /opt/plex/config:/config
          - /opt/plex/transcode:/transcode
          - /mnt/media:/data
    ```

=== "NVIDIA"
    ```yaml
    services:
      plex:
        image: plexinc/pms-docker
        network_mode: host
        runtime: nvidia
        deploy:
          resources:
            reservations:
              devices:
                - capabilities: [gpu]
        environment:
          - TZ=America/New_York
          - NVIDIA_VISIBLE_DEVICES=all
          - NVIDIA_DRIVER_CAPABILITIES=compute,video,utility
        volumes:
          - /opt/plex/config:/config
          - /opt/plex/transcode:/transcode
          - /mnt/media:/data
    ```

After enabling, toggle **Use hardware acceleration when available** and
**Use hardware-accelerated video encoding** in **Settings > Server >
Transcoder > Show Advanced**. The container's `cont-init.d/45-…` script
handles the device-group plumbing automatically.

### Recipe 5: Back Up the Plex Database

**Goal:** Take a clean, restorable snapshot of `/config`.

1. Stop the container so SQLite is quiescent:

   ```bash
   docker stop plex
   ```

2. Tar `/config` (preserving permissions):

   ```bash
   tar -czpf plex-config-$(date +%F).tar.gz \
     --exclude='Library/Application Support/Plex Media Server/Cache' \
     --exclude='Library/Application Support/Plex Media Server/Crash Reports' \
     -C /opt/plex/config .
   ```

3. Restart:

   ```bash
   docker start plex
   ```

> [!CAUTION]
> Do **not** copy `/config` while the container is running. SQLite
> WAL files can be in an inconsistent state; the resulting backup
> may load but show silent corruption days later when an unrelated
> table is touched.

### Recipe 6: Migrate from a Native Install to Docker

**Goal:** Move an existing bare-metal Plex installation into the
container without losing the library, watch state, or metadata.

1. On the native host, locate the Plex data directory:
   - **Linux package:** `/var/lib/plexmediaserver/Library/Application Support/`
   - **macOS:** `~/Library/Application Support/Plex Media Server`
   - **Windows:** `%LOCALAPPDATA%\Plex Media Server`

2. Stop the native Plex service so the database is quiet
   (`sudo systemctl stop plexmediaserver`, or quit the macOS menu-bar
   app, or stop the Windows service).

3. Recreate the expected layout under your container `/config` host path:

   ```bash
   sudo mkdir -p '/opt/plex/config/Library/Application Support'
   sudo cp -a '/var/lib/plexmediaserver/Library/Application Support/Plex Media Server' \
     '/opt/plex/config/Library/Application Support/'
   sudo chown -R 1000:1000 /opt/plex/config
   ```

4. Bring up the container with that path mounted as `/config` — and
   without a `PLEX_CLAIM` (the database is already claimed):

   ```bash
   docker compose up -d
   ```

5. Wait for the healthcheck, then verify your libraries appear in
   the web UI exactly as before. Disable or uninstall the native
   service only after you confirm the container is happy.

> [!IMPORTANT]
> The container's `plex` user defaults to UID/GID `1000`. Adjust the
> `chown` command if your native install used a different owner — or
> set `PLEX_UID`/`PLEX_GID` in compose to match the existing files.

### Recipe 7: Pin to a Specific Version for Stability

**Goal:** Stop being on the moving `latest` tag and lock to a known-good
PMS build.

1. Pick a known-good version tag from the
   [Docker Hub tags page](https://hub.docker.com/r/plexinc/pms-docker/tags)
   (e.g., `1.43.0.10492-121068a07`).

2. Update your compose file:

   ```yaml
   services:
     plex:
       image: plexinc/pms-docker:1.43.0.10492-121068a07
   ```

3. Recreate the container:

   ```bash
   docker compose up -d
   ```

4. **Verify** with `docker exec plex curl -s http://localhost:32400/identity`
   — the `version=` attribute should match the pinned tag.

> [!TIP]
> Pinned tags are also the right choice for offline or air-gapped
> installs. Both `public` and `beta` reach out to `plex.tv` on every
> start; pinned tags don't.

### Recipe 8: Run on Synology DSM or unRAID

**Goal:** Use the official image on a NAS appliance.

=== "Synology DSM (Container Manager / Portainer)"
    1. **Container Manager > Registry**, search `plexinc/pms-docker`,
       download `latest` (or a pinned tag).
    2. Create a container, use **bridge** networking, map ports
       `32400/tcp`, `1900/udp`, `32400-32414/udp`.
    3. Mount three host folders to `/config`, `/transcode`, `/data`.
    4. Add environment variables `TZ`, `PLEX_CLAIM`,
       `ADVERTISE_IP=http://<dsm-ip>:32400/`.
    5. Run; complete setup at `http://<dsm-ip>:32400/web`.

=== "unRAID"
    1. **Apps > Community Applications > search "Plex Media Server"**
       and pick the official `plexinc/pms-docker` template (the repo
       ships `plex-unRAID.xml` for this).
    2. The template defaults to host networking and pre-configures
       `/config`, `/transcode`, `/data`, `PLEX_CLAIM`, `PLEX_UID=99`,
       `PLEX_GID=100`, and `VERSION`.
    3. Set `PLEX_CLAIM`, point `/data` at your `/mnt/user/media`
       share, and click **Apply**.

> [!NOTE]
> Synology's default Container Manager UI does not support
> `--network=host` — you have to use bridge mode and set
> `ADVERTISE_IP`. unRAID supports host networking out of the box,
> which is why the bundled template uses it.

---

## 6. Troubleshooting

### Diagnostic Decision Tree

```text
Container won't start
  ├─ docker logs plex
  │    ├─ "Could not get install version"
  │    │   └─ network blocked, or invalid TAG. Pin a version.
  │    ├─ "Failed to fetch update"
  │    │   └─ download truncated or DNS failure
  │    └─ "s6-supervise (child): fatal: unable to exec run: Permission denied"
  │        └─ RHEL/Fedora docker patch — add  -v /run
  │
Container starts but (unhealthy)
  ├─ docker exec plex curl http://localhost:32400/identity
  │    ├─ "Connection refused" → PMS process didn't start
  │    └─ XML returned       → healthcheck transient, wait 1 min
  │
Healthy but clients can't see it
  ├─ Bridge networking?     → set ADVERTISE_IP, forward 32400
  ├─ Host networking?       → host firewall (ufw/firewalld)
  └─ Macvlan?               → host can't talk to container by design
```

### Error: `s6-supervise (child): fatal: unable to exec run: Permission denied`

**Cause:** Fedora, CentOS Stream, RHEL, and AlmaLinux ship a Docker
build that mounts `/run` non-executable. s6-overlay puts its
service-supervision sockets there.

**Solution:** Add a tmpfs `/run`:

```bash
docker run ... -v /run plexinc/pms-docker
```

Or in compose:

```yaml
volumes:
  - /run
```

**Prevention:** Always include `-v /run` on RHEL-family hosts.

### Error: `Could not get install version`

**Cause:** Container is on the `beta` or `public` tag and cannot reach
`plex.tv` to look up the latest build, or the URL parameters generated
from `version.txt` (architecture / channel / distro) don't resolve.

**Solution:**

1. Check connectivity from inside: `docker exec plex curl -I https://plex.tv`
2. If using `beta`, confirm the linked Plex account has Plex Pass.
3. Switch to a pinned version tag for offline-resilient startup.

**Prevention:** Use a pinned tag for production.

### Error: `Failed to fetch update`

**Cause:** The downloaded `.deb` from `plex.tv` was either smaller than
10 KB or the `curl` exit code was non-zero. The image's
`installFromRawUrl()` function checks both.

**Solution:**

1. Restart: `docker restart plex` (transient network issues recover).
2. Persistent? Build locally with `--build-arg URL=https://...` and a
   known-good `.deb` URL.

**Prevention:** Pin to a baked-in version tag.

### Error: Database corruption (slow scans, crashes after weeks)

**Cause:** `/config` is on NFS, SMB, 9PFS, or a Docker Desktop bind
mount — none of which expose POSIX locking the way SQLite expects.

**Solution:**

1. Stop the container.
2. Move `/config` to local block storage (ext4, XFS, ZFS, or a
   Docker named volume on the same).
3. Restart.

**Prevention:** The single hardest "must-do" rule with this image —
`/config` on local block storage **only**.

### Error: Cannot reach Plex from outside the LAN (bridge mode)

**Cause:** Plex is double-NAT'd (Docker bridge + your home router) and
can't auto-configure port forwarding.

**Solution:**

1. Set `ADVERTISE_IP=http://<host-public-or-LAN-ip>:32400/` in the
   container env.
2. Forward TCP `32400` on your router to the Docker host.
3. **Settings > Server > Remote Access** in the web UI — manually set
   the external port if it differs.

**Prevention:** Use host networking (no double NAT).

### Error: Plex can't read media — `Permission denied` in scan logs

**Cause:** The in-container `plex` user's UID/GID does not match the
ownership of files in your bind-mounted `/data`.

**Solution:**

1. On the host: `id $(whoami)` — note `uid=1000(jane) gid=1000(jane)`.
2. Set `PLEX_UID=1000`, `PLEX_GID=1000` in the env block.
3. Restart the container — the `40-plex-first-run` script re-runs
   `usermod`/`groupmod`.

**Prevention:** Always set `PLEX_UID`/`PLEX_GID` to match your
media-owning host user.

### Error: Setup wizard refuses to load on a remote browser

**Cause:** Plex enforces a localhost-only setup wizard until the server
is claimed. With no `PLEX_CLAIM`, the only way to claim is from a
browser whose origin is `localhost`.

**Solution:**

1. Add `PLEX_CLAIM` to the compose env block and recreate; or
2. SSH-tunnel to the server:

   ```bash
   ssh user@server -L 32400:server:32400 -N
   ```

   Then open `http://localhost:32400/web` on your laptop.

**Prevention:** Always include `PLEX_CLAIM` on first run.

---

## 7. FAQ

**Q: Where do I get a claim token?**

[plex.tv/claim](https://www.plex.tv/claim) while signed into your Plex
account. The token is roughly four minutes long, single-use, and is
exchanged for a permanent server token by `cont-init.d/40-plex-first-run`
on the next container start.

**Q: Which networking mode should I pick?**

If nothing else is competing for port `32400`, use **host**. Bridge
adds friction (port mapping, `ADVERTISE_IP`, double-NAT remote-access
problems) for very little gain on a single-purpose Plex box.

**Q: What architectures are supported?**

The official image is published for `linux/amd64`, `linux/arm64`, and
`linux/arm/v7` (32-bit ARM with NEON, e.g., Raspberry Pi 3/4 in 32-bit
mode). The container's `installBinary.sh` selects `linux-x86_64`,
`linux-aarch64`, or `linux-armv7hf_neon` from the Plex download API
based on `${TARGETARCH}`/`${TARGETPLATFORM}`.

**Q: How do updates work?**

- **Pinned tag** (e.g., `1.43.0.10492-121068a07`): static; pull a new
  image to update.
- **`latest`:** static at build time; pull when a new build lands.
- **`public` / `beta`:** dynamic; the container runs
  `cont-init.d/50-plex-update` on every start, queries
  `https://plex.tv/downloads/details/5`, and installs the latest `.deb`.

**Q: Do I need a Plex Pass?**

No for basic streaming and library management. Yes for hardware
transcoding, mobile sync, DVR/Live TV, and the `beta` channel.

**Q: Can I run multiple media directories?**

Yes — repeat `-v` flags or list multiple lines in `volumes:`:

```yaml
volumes:
  - /mnt/movies:/data/movies
  - /mnt/tv:/data/tv
  - /mnt/music:/data/music
```

Then in the web UI, point each library at its own subdirectory under
`/data`.

**Q: What does `CHANGE_CONFIG_DIR_OWNERSHIP` do?**

By default the container runs `chown -h plex:plex` against `/config`
and `/transcode` on every start. For a 100 GB+ library this can
add a few minutes of startup time. If you've already set ownership
correctly on the host, set `CHANGE_CONFIG_DIR_OWNERSHIP=false`.

**Q: How do I back up my library?**

Stop the container, tar `/config` (skip `Cache/` and `Crash Reports/`),
restart. See recipe 5.

**Q: Does this image auto-update?**

Only on the `public` and `beta` tags. Standard tags freeze the
binary at build time; you must pull a new image. Use Watchtower if
you want hands-off updates with a pinned-tag style.

**Q: Can I run this on Kubernetes?**

Yes. Use the bundled Helm chart at `charts/plex-media-server`. It
deploys a single-replica StatefulSet, optionally with an `nginx`
Ingress, NVIDIA GPU support, and an rclone sidecar for cloud-mounted
media. See the Admin Guide for the full values reference.

**Q: Where do the init scripts live, in case I want to read them?**

Inside the container, at `/etc/cont-init.d/40-plex-first-run`,
`/etc/cont-init.d/45-plex-hw-transcode-and-connected-tuner`, and
`/etc/cont-init.d/50-plex-update`. They are part of the s6-overlay
init lifecycle. The shared shell library is `/plex-common.sh`.

**Q: How do I file a bug?**

Open an issue at
[github.com/plexinc/pms-docker](https://github.com/plexinc/pms-docker/issues).
Include the output of `docker version`, `docker logs plex`,
your `docker run`/compose, and (if relevant) the host distro and
kernel.

---

_Generated April 2026 from plexinc/pms-docker at 1cef65e. AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it. Unofficial; not affiliated with or reviewed by the project._
