113 lines
4.5 KiB
YAML

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