homelab/ansible/ansible-old/playbooks/docker/deploy_plex_standalone.yml

449 lines
16 KiB
YAML

---
# playbooks/docker/deploy_plex_standalone.yml
#
# Purpose:
# Deploy the full Plex media stack on a standalone Docker host (statler).
# Includes: Plex, Radarr, Sonarr, SABnzbd, Overseerr, Wizarr, and their
# Authentik proxy outposts.
#
# Architecture:
# All containers share the proxy-net bridge network. Traefik-kop on statler
# reads container labels and publishes routes to Heimdall's Redis, where
# the external Traefik picks them up.
# Plex config is served from the TNAS share at /mnt/homelab/apps/plex/data.
# Media (TV/Movies/Downloads) is served from /mnt/media (TNAS Volume2).
# Service configs (Radarr, Sonarr, etc.) are served from /mnt/homelab/apps.
#
# Pre-requisites:
# - NFS shares mounted on target host (mount_nfs_shares.yml must have run):
# /mnt/homelab (TNAS Volume1/appdata)
# /mnt/media (TNAS Volume2/media)
# - traefik-kop-agent must be running on the target host.
# - vault_plex_claim and vault_authentik_token_* must be present and decrypted.
#
# Usage:
# ansible-playbook -i inventory/hosts.ini playbooks/docker/deploy_plex_standalone.yml \
# -e "target_host=statler"
#
# Tear down a single service (example):
# ansible-playbook ... -e "target_host=statler plex_deploy_state=absent"
#
# Verification after deploy:
# docker ps on statler
# curl http://10.0.0.210:32400/identity
# redis-cli -h 10.0.0.151 keys 'traefik/*sonarr*'
- name: Deploy Plex media stack on standalone Docker host
hosts: "{{ target_host | default('statler') }}"
become: true
gather_facts: false
vars_files:
- ../../group_vars/all.yml
vars:
plex_network: "proxy-net"
plex_config_dir: "/mnt/homelab/apps/plex/data"
plex_tv_dir: "/mnt/media/tvshows"
plex_movies_dir: "/mnt/media/movies"
media_base: "/mnt/media"
sabnzbd_config_dir: "/mnt/homelab/apps/sabnzbd/data"
sonarr_config_dir: "/mnt/homelab/apps/sonarr/data"
radarr_config_dir: "/mnt/homelab/apps/radarr/data"
overseerr_config_dir: "/mnt/homelab/apps/overseerr/data"
wizarr_config_dir: "/mnt/homelab/apps/wizarr/data/database"
tasks:
# --------------------------------------------------
# STEP 0: Safety assertions
# --------------------------------------------------
- name: Assert target_host is explicit and safe
ansible.builtin.assert:
that:
- target_host is defined
- target_host | length > 0
- target_host not in ['all', '*', 'ubuntu_lab', 'docker_hosts', 'swarm_hosts']
fail_msg: >-
Invalid target_host scope. Use an explicit host, e.g.:
-e "target_host=statler"
run_once: true
delegate_to: localhost
- name: Assert required secrets are available and decrypted
ansible.builtin.assert:
that:
- vault_plex_claim is defined
- vault_plex_claim | trim | length > 0
- vault_plex_claim is not search('^\$ANSIBLE_VAULT;')
- vault_authentik_token_sonarr is defined
- vault_authentik_token_sonarr | trim | length > 0
- vault_authentik_token_sonarr is not search('^\$ANSIBLE_VAULT;')
- vault_authentik_token_radarr is defined
- vault_authentik_token_radarr | trim | length > 0
- vault_authentik_token_radarr is not search('^\$ANSIBLE_VAULT;')
- vault_authentik_token_sabnzbd is defined
- vault_authentik_token_sabnzbd | trim | length > 0
- vault_authentik_token_sabnzbd is not search('^\$ANSIBLE_VAULT;')
fail_msg: >-
One or more required secrets are unavailable or not decrypted.
Required: vault_plex_claim, vault_authentik_token_sonarr,
vault_authentik_token_radarr, vault_authentik_token_sabnzbd.
- name: Assert TNAS Plex config directory is mounted and accessible
ansible.builtin.stat:
path: "{{ plex_config_dir }}"
register: _plex_config_stat
- name: Fail if TNAS Plex config path does not exist
ansible.builtin.assert:
that:
- _plex_config_stat.stat.exists
- _plex_config_stat.stat.isdir
fail_msg: >-
{{ plex_config_dir }} does not exist or is not a directory.
Ensure the TNAS NFS share is mounted: run mount_nfs_shares.yml first.
- name: Assert media NFS shares are mounted
ansible.builtin.stat:
path: "{{ item }}"
register: _media_stat
loop:
- "{{ plex_tv_dir }}"
- "{{ plex_movies_dir }}"
- name: Fail if media paths are not mounted
ansible.builtin.assert:
that:
- item.stat.exists
- item.stat.isdir
fail_msg: >-
Media path {{ item.item }} is not accessible on {{ inventory_hostname }}.
Ensure /mnt/media NFS share is mounted: run mount_nfs_shares.yml first.
loop: "{{ _media_stat.results }}"
# --------------------------------------------------
# STEP 1: Ensure proxy-net bridge network exists
# --------------------------------------------------
- name: Ensure proxy-net bridge network exists
community.docker.docker_network:
name: "{{ plex_network }}"
driver: bridge
state: present
# --------------------------------------------------
# STEP 2: Ensure service config directories exist on appdata mount
# WHY these dirs are on /mnt/homelab: shared appdata policy for statler
# services while keeping explicit paths in deployment automation.
# --------------------------------------------------
- name: Ensure local service config directories exist
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: "1000"
group: "1000"
mode: '0755'
loop:
- "{{ sabnzbd_config_dir }}"
- "{{ sonarr_config_dir }}"
- "{{ radarr_config_dir }}"
- "{{ overseerr_config_dir }}"
- "{{ wizarr_config_dir }}"
# --------------------------------------------------
# STEP 3: Plex
# --------------------------------------------------
- name: Deploy Plex Media Server
community.docker.docker_container:
name: plex
image: lscr.io/linuxserver/plex:latest
pull: always
restart_policy: unless-stopped
state: "{{ plex_deploy_state | default('started') }}"
published_ports:
- "32400:32400"
env:
PUID: "1000"
PGID: "1000"
TZ: America/New_York
PLEX_CLAIM: "{{ vault_plex_claim }}"
VERSION: docker
volumes:
- "{{ plex_config_dir }}:/config"
- "{{ plex_tv_dir }}:/tv"
- "{{ plex_movies_dir }}:/movies"
networks:
- name: "{{ plex_network }}"
memory: 4g
cpus: 2.0
# --------------------------------------------------
# STEP 4: SABnzbd + outpost
# --------------------------------------------------
- name: Deploy SABnzbd
community.docker.docker_container:
name: sabnzbd
image: lscr.io/linuxserver/sabnzbd:4.5.5-ls239
pull: always
restart_policy: unless-stopped
state: "{{ plex_deploy_state | default('started') }}"
published_ports:
- "8155:8080"
env:
PUID: "1000"
PGID: "1000"
TZ: America/New_York
volumes:
- "{{ sabnzbd_config_dir }}:/config"
- "{{ media_base }}/incoming/downloads-sab/complete:/downloads"
- "{{ media_base }}/incoming/downloads-sab/incomplete:/incomplete-downloads"
- "{{ media_base }}/incoming/downloads-sab/history:/history"
networks:
- name: "{{ plex_network }}"
labels:
homepage.name: SABnzbd
homepage.icon: si:sabnzbd
homepage.url: https://sab.castaldifamily.com
homepage.description: Usenet downloader
memory: 1g
cpus: 0.5
- name: Deploy Authentik outpost for SABnzbd
community.docker.docker_container:
name: authentik-outpost-sabnzbd
image: ghcr.io/goauthentik/proxy:2025.10.3
pull: always
restart_policy: unless-stopped
state: "{{ plex_deploy_state | default('started') }}"
published_ports:
- "9004:9000"
- "9447:9443"
env:
AUTHENTIK_HOST: https://sso.castaldifamily.com
AUTHENTIK_INSECURE: "false"
AUTHENTIK_TOKEN: "{{ vault_authentik_token_sabnzbd }}"
AUTHENTIK_HOST_BROWSER: https://sso.castaldifamily.com
networks:
- name: "{{ plex_network }}"
labels:
traefik.enable: "true"
traefik.http.routers.sabnzbd.entrypoints: websecure
traefik.http.routers.sabnzbd.rule: "Host(`sab.castaldifamily.com`)"
traefik.http.routers.sabnzbd.tls: "true"
traefik.http.routers.sabnzbd.tls.certresolver: cloudflare
traefik.http.services.sabnzbd.loadbalancer.server.port: "9004"
memory: 256m
cpus: 0.25
# --------------------------------------------------
# STEP 5: Sonarr + outpost
# --------------------------------------------------
- name: Deploy Sonarr
community.docker.docker_container:
name: sonarr
image: lscr.io/linuxserver/sonarr:4.0.16.2944-ls300
pull: always
restart_policy: unless-stopped
state: "{{ plex_deploy_state | default('started') }}"
published_ports:
- "8989:8989"
env:
PUID: "1000"
PGID: "1000"
TZ: America/New_York
volumes:
- "{{ sonarr_config_dir }}:/config"
- "{{ plex_tv_dir }}:/tv"
- "{{ media_base }}/incoming/downloads-sab/complete/sonarr:/downloads/sonarr"
networks:
- name: "{{ plex_network }}"
labels:
homepage.name: Sonarr
homepage.icon: si:sonarr
homepage.url: https://sonarr.castaldifamily.com
homepage.description: TV Shows
memory: 1g
cpus: 0.5
- name: Deploy Authentik outpost for Sonarr
community.docker.docker_container:
name: authentik-outpost-sonarr
image: ghcr.io/goauthentik/proxy:2025.10.3
pull: always
restart_policy: unless-stopped
state: "{{ plex_deploy_state | default('started') }}"
published_ports:
- "9001:9000"
- "9444:9443"
env:
AUTHENTIK_HOST: https://sso.castaldifamily.com
AUTHENTIK_INSECURE: "false"
AUTHENTIK_TOKEN: "{{ vault_authentik_token_sonarr }}"
AUTHENTIK_HOST_BROWSER: https://sso.castaldifamily.com
networks:
- name: "{{ plex_network }}"
labels:
traefik.enable: "true"
traefik.http.routers.sonarr.entrypoints: websecure
traefik.http.routers.sonarr.rule: "Host(`sonarr.castaldifamily.com`)"
traefik.http.routers.sonarr.tls: "true"
traefik.http.routers.sonarr.tls.certresolver: cloudflare
traefik.http.services.sonarr.loadbalancer.server.port: "9001"
memory: 256m
cpus: 0.25
# --------------------------------------------------
# STEP 6: Radarr + outpost
# --------------------------------------------------
- name: Deploy Radarr
community.docker.docker_container:
name: radarr
image: lscr.io/linuxserver/radarr:6.0.4.10291-ls289
pull: always
restart_policy: unless-stopped
state: "{{ plex_deploy_state | default('started') }}"
published_ports:
- "7878:7878"
env:
PUID: "1000"
PGID: "1000"
TZ: America/New_York
volumes:
- "{{ radarr_config_dir }}:/config"
- "{{ plex_movies_dir }}:/movies"
- "{{ media_base }}/incoming/downloads-sab/complete/radarr:/downloads/radarr"
networks:
- name: "{{ plex_network }}"
labels:
homepage.name: Radarr
homepage.icon: si:radarr
homepage.url: https://radarr.castaldifamily.com
homepage.description: Movies & shows
memory: 1g
cpus: 0.5
- name: Deploy Authentik outpost for Radarr
community.docker.docker_container:
name: authentik-outpost-radarr
image: ghcr.io/goauthentik/proxy:2025.10.3
pull: always
restart_policy: unless-stopped
state: "{{ plex_deploy_state | default('started') }}"
published_ports:
- "9002:9000"
- "9445:9443"
env:
AUTHENTIK_HOST: https://sso.castaldifamily.com
AUTHENTIK_INSECURE: "false"
AUTHENTIK_TOKEN: "{{ vault_authentik_token_radarr }}"
AUTHENTIK_HOST_BROWSER: https://sso.castaldifamily.com
AUTHENTIK_INSECURE_SKIP_VERIFY: "false"
TRUST_PROXY_HEADERS: "true"
networks:
- name: "{{ plex_network }}"
labels:
traefik.enable: "true"
traefik.http.routers.radarr.entrypoints: websecure
traefik.http.routers.radarr.rule: "Host(`radarr.castaldifamily.com`)"
traefik.http.routers.radarr.tls: "true"
traefik.http.routers.radarr.tls.certresolver: cloudflare
traefik.http.services.radarr.loadbalancer.server.port: "9002"
memory: 256m
cpus: 0.25
# --------------------------------------------------
# STEP 7: Overseerr
# --------------------------------------------------
- name: Deploy Overseerr
community.docker.docker_container:
name: overseerr
image: lscr.io/linuxserver/overseerr:1.34.0
pull: always
restart_policy: unless-stopped
state: "{{ plex_deploy_state | default('started') }}"
published_ports:
- "8150:5055"
env:
PUID: "1000"
PGID: "1000"
TZ: America/New_York
volumes:
- "{{ overseerr_config_dir }}:/config"
networks:
- name: "{{ plex_network }}"
labels:
traefik.enable: "true"
traefik.http.routers.overseerr.entrypoints: websecure
traefik.http.routers.overseerr.rule: "Host(`overseerr.castaldifamily.com`)"
traefik.http.routers.overseerr.tls: "true"
traefik.http.routers.overseerr.tls.certresolver: cloudflare
traefik.http.routers.overseerr.service: overseerr
traefik.http.services.overseerr.loadbalancer.server.port: "8150"
homepage.name: Overseerr
homepage.icon: si:overseerr
homepage.url: https://overseerr.castaldifamily.com
homepage.description: Media request management
memory: 512m
cpus: 0.2
# --------------------------------------------------
# STEP 8: Wizarr
# NOTE: homelab_status=broken in source-compose. Deploying as-is; SSO
# integration requires a dedicated Authentik outpost token (not yet
# configured). DISABLE_BUILTIN_AUTH=True means the web UI will be
# unprotected until the outpost is wired up.
# --------------------------------------------------
- name: Deploy Wizarr
community.docker.docker_container:
name: wizarr
image: ghcr.io/wizarrrr/wizarr:v2025.12.0
pull: always
restart_policy: unless-stopped
state: "{{ plex_deploy_state | default('started') }}"
published_ports:
- "8157:5690"
env:
PUID: "1000"
PGID: "1000"
TZ: America/New_York
DISABLE_BUILTIN_AUTH: "True"
volumes:
- "{{ wizarr_config_dir }}:/data/database"
networks:
- name: "{{ plex_network }}"
labels:
traefik.enable: "true"
traefik.http.routers.wizarr.entrypoints: websecure
traefik.http.routers.wizarr.rule: "Host(`wizarr.castaldifamily.com`)"
traefik.http.routers.wizarr.tls: "true"
traefik.http.routers.wizarr.tls.certresolver: cloudflare
traefik.http.routers.wizarr.service: wizarr
traefik.http.services.wizarr.loadbalancer.server.port: "8157"
homepage.name: Wizarr
homepage.icon: si:wizarr
homepage.url: https://wizarr.castaldifamily.com
homepage.description: Media management
memory: 512m
cpus: 0.2
# --------------------------------------------------
# STEP 9: Summary
# --------------------------------------------------
- name: Show deployment summary
ansible.builtin.debug:
msg:
- "Plex media stack deployed to {{ inventory_hostname }}"
- "Plex config : {{ plex_config_dir }} (TNAS)"
- "Media : {{ media_base }} (TNAS)"
- "Network : {{ plex_network }}"
- "Services : plex, sabnzbd, sonarr, radarr, overseerr, wizarr"
- "Outposts : sabnzbd (9004), sonarr (9001), radarr (9002)"