x-info: github: https://github.com/linuxserver/docker-plex docs: https://docs.linuxserver.io/images/docker-plex/ changelog: https://github.com/plexinc/pms-docker/releases homelab_status: active last_updated: 2026-03-13 # Managed by Ansible — manual edits will be overwritten on next deploy. # Source: ansible/templates/stacks/plex.stack.yml # Deploy: ansible-playbook -i inventory/hosts.ini playbooks/docker/deploy_plex.yml # # SECRETS REQUIRED: # vault_plex_claim must be defined in group_vars/vault/all.yml. # Encrypt with: # ansible-vault encrypt_string 'claim-XXXX' --name 'vault_plex_claim' # PLEX_CLAIM is a bootstrap token used only on first server claim; it is # ignored by Plex on subsequent starts. # # DEVICE PASSTHROUGH NOTE: # Docker Swarm has limited support for 'devices:' in service specs (requires # Docker Engine 20.10+ and a single-node placement constraint). Hardware # transcoding (/dev/dri, /dev/dvb) is pinned to swarm-manager-1. If your # Swarm Engine does not support device passthrough, remove the 'devices:' # block and rely on CPU transcoding only. version: "3.9" services: plex: image: lscr.io/linuxserver/plex:1.42.2.10156-f737b826c-ls283 environment: - PUID=1000 - PGID=1000 - TZ=America/New_York - VERSION=docker - PLEX_CLAIM={{ vault_plex_claim }} # ADVERTISE_IP: comma-separated list; Plex clients select the best path. # External clients → Traefik HTTPS on port 443 (websecure entrypoint). # LAN clients → direct Swarm routing mesh on port 32400 (fast path, # no Traefik hop; required for SSDP/GDM discovery). - ADVERTISE_IP=https://plex.castaldifamily.com:443/,http://{{ edge_routing.swarm.bind_ip }}:32400/ # ACCESS POLICY (Option B — split-access): # LAN (10.0.0.0/24, 10.0.200.0/24): direct on port 32400 via Swarm routing mesh. # External: Traefik HTTPS only (see deploy.labels block below). # FIREWALL REQUIREMENT: block port 32400 from VLAN 30 (Guest) and VLAN 50 (IoT). ports: - "32400:32400" volumes: - /mnt/homelab/apps/plex/data:/config - /mnt/media/tvshows:/tv - /mnt/media/movies:/movies # WHY absolute paths: Swarm services have no well-defined working directory. # Relative paths (e.g. ./data) are unsafe in Swarm stacks. # # Device passthrough — requires Docker Engine >= 20.10 and single-node placement. # If a device is absent: Docker ignores it and Plex falls back to CPU transcoding. # Run deploy_plex.yml to see preflight warnings if GPU devices are absent. devices: - /dev/renderD128:/dev/renderD128 - /dev/dri:/dev/dri - /dev/dvb:/dev/dvb networks: - proxy-net # Top-level labels: used by homepage widget discovery (non-Swarm consumers). labels: - "homepage.name=Plex" - "homepage.icon=si:plex" - "homepage.url=https://plex.castaldifamily.com" - "homepage.description=Movies & shows" deploy: replicas: 1 placement: constraints: # WHY pinned to swarm-manager-1: media volumes and hardware device # nodes are local to this host. Update if media/GPU lives elsewhere. - node.hostname == swarm-manager-1 labels: # WHY deploy.labels (not top-level): traefik-kop reads Swarm *service* # labels via the Docker API. Top-level labels are on the container image, # not the Swarm service — traefik-kop will ignore them. - "traefik.enable=true" - "traefik.http.routers.plex.rule=Host(`plex.castaldifamily.com`)" - "traefik.http.routers.plex.entrypoints=websecure" - "traefik.http.routers.plex.tls=true" - "traefik.http.routers.plex.tls.certresolver=cloudflare" # WHY server.url (not server.port): routes external Traefik to the Swarm # routing mesh IP rather than guessing a container IP. Consistent with # gitea.stack.yml pattern. - "traefik.http.services.plex.loadbalancer.server.url=http://{{ edge_routing.swarm.bind_ip }}:32400" resources: limits: memory: 2G cpus: "2.0" restart_policy: condition: on-failure delay: 10s max_attempts: 3 window: 60s update_config: parallelism: 1 order: start-first failure_action: rollback delay: 10s monitor: 30s rollback_config: parallelism: 1 order: stop-first networks: proxy-net: external: true name: proxy-net