# ============================================================================= # Torrent Stack (media) - VPN, *arr stack & media tooling # # Split from compose.yml on 2026-09-11 - verbatim service blocks. # Shared top-level networks:/volumes: are repeated in each subfile so it also # works standalone (docker compose -f media.yml up -d). Identical copies merge # cleanly under compose.yml's `include:` with no change to the live config. # Keep ./ paths, container names and /mnt data as-is. # ============================================================================= networks: #caddy_net: # external: true tdarr-net: driver: bridge default: name: portainer_network volumes: #caddy_config: #caddy_data: # external: true #myspeed: # external: true portainer_data: name: portainer_data redis-data: driver: local services: # --------------------------------------------------------------------------- # VPN & Download # --------------------------------------------------------------------------- gluetun: image: ${GLUETUN_IMAGE} container_name: gluetun env_file: - .env - ./env/.gluetun.env cap_add: - NET_ADMIN ports: - 8877:8000/tcp - ${QBITTORRENT_PORT}:${QBITTORRENT_PORT} - ${PROWLARR_PORT}:${PROWLARR_PORT} - ${SONARR_PORT}:${SONARR_PORT} - ${RADARR_PORT}:${RADARR_PORT} - ${LIDARR_PORT}:${LIDARR_PORT} - ${BAZARR_PORT}:${BAZARR_PORT} volumes: - ./gluetun-data:/tmp/gluetun:rw #- /etc/localtime:/etc/localtime:ro #- /etc/timezone:/etc/timezone:ro environment: - VPN_SERVICE_PROVIDER=protonvpn - VPN_TYPE=wireguard - HTTP_CONTROL_SERVER=ON - WIREGUARD_MTU=1280 - VPN_PORT_FORWARDING=on - VPN_PORT_FORWARDING_PROVIDER=protonvpn - SERVER_COUNTRIES=Netherlands - NETWORK_LOCAL_ADDRESSES=192.168.50.0/24,172.16.0.0/12 - FIREWALL_OUTBOUND_SUBNETS=192.168.50.0/24 - HEALTH_SERVER_ADDRESS=0.0.0.0:8877 - HEALTH_TARGET_ADDRESSES=${EXTERNAL_IPV4}:${PLEX_PFWD_PORT} - TZ=${TIMEZONE} - DNS_UPSTREAM_RESOLVER_TYPE=doh healthcheck: test: [ "CMD-SHELL", "wget -qO- '${WEB_PROTOCOL}://${EXTERNAL_IPV4}:${PLEX_PFWD_PORT}/identity' | grep -q 'MediaContainer' || exit 1", ] interval: 1m30s timeout: 15s retries: 3 start_period: 45s security_opt: - no-new-privileges:true mem_limit: 256m cpus: 1.0 devices: - /dev/net/tun:/dev/net/tun restart: no qbittorrent: image: ${QBITTORRENT_IMAGE} container_name: qbittorrent network_mode: "container:gluetun" depends_on: gluetun: condition: service_healthy volumes: - ./qbittorrent:/config - ${DISK1}:${DISK1} - ${DISK2}:${DISK2} - ./gluetun-data:/tmp/gluetun #- /etc/localtime:/etc/localtime:ro #- /etc/timezone:/etc/timezone:ro environment: - PUID=${HOST_PUID:-1000} - PGID=${HOST_PGID:-1000} - TZ=${TIMEZONE} - WEBUI_PORT=${QBITTORRENT_PORT} labels: - "autoheal=true" healthcheck: test: [ "CMD-SHELL", "curl -sf '${WEB_PROTOCOL}://${EXTERNAL_IPV4}:${PLEX_PFWD_PORT}/identity' | grep -q 'MediaContainer' || exit 1", ] interval: 1m timeout: 10s retries: 3 start_period: 30s security_opt: - no-new-privileges:true mem_limit: 512m cpus: 1.0 restart: unless-stopped port-updater: image: curlimages/curl:8.21.0 container_name: port-updater network_mode: "container:gluetun" depends_on: gluetun: condition: service_healthy volumes: - ./gluetun-data:/tmp/gluetun:ro entrypoint: ["/bin/sh", "-c"] command: - | trap 'exit 0' SIGTERM; while true; do while [ ! -s /tmp/gluetun/forwarded_port ]; do echo "Waiting for Gluetun to provide a port..." sleep 5 done read -r PORT_VAL < /tmp/gluetun/forwarded_port; CLEAN_PORT=$$(echo "$$PORT_VAL" | tr -d '\r\n '); if [ -n "$$CLEAN_PORT" ]; then echo "Updating qBit to port: $$CLEAN_PORT"; sleep 10; curl -s -X POST -d "json={\"listen_port\":$$CLEAN_PORT}" http://localhost:${QBITTORRENT_PORT}/api/v2/app/setPreferences; sleep 3600 & wait $$!; else sleep 10 fi done labels: - "autoheal=true" healthcheck: test: [ "CMD-SHELL", "curl -sf '${WEB_PROTOCOL}://${EXTERNAL_IPV4}:${PLEX_PFWD_PORT}/identity' | grep -q 'MediaContainer' || exit 1", ] interval: 1m timeout: 10s retries: 3 start_period: 30s security_opt: - no-new-privileges:true read_only: true tmpfs: - /tmp mem_limit: 64m cpus: 0.25 restart: unless-stopped # --------------------------------------------------------------------------- # *arr Stack # --------------------------------------------------------------------------- prowlarr: image: ${PROWLARR_IMAGE} container_name: prowlarr network_mode: "container:gluetun" depends_on: gluetun: condition: service_healthy volumes: - ./prowlarr:/config - ./notify.sh:/notify.sh:ro #- /etc/localtime:/etc/localtime:ro #- /etc/timezone:/etc/timezone:ro environment: - PUID=${HOST_PUID:-1000} - PGID=${HOST_PGID:-1000} - TZ=${TIMEZONE} labels: - "autoheal=true" healthcheck: test: [ "CMD-SHELL", "curl -sf '${WEB_PROTOCOL}://${EXTERNAL_IPV4}:${PLEX_PFWD_PORT}/identity' | grep -q 'MediaContainer' || exit 1", ] interval: 1m timeout: 10s retries: 3 start_period: 30s security_opt: - no-new-privileges:true mem_limit: 256m cpus: 1.0 restart: unless-stopped sonarr: image: ${SONARR_IMAGE} container_name: sonarr network_mode: "container:gluetun" depends_on: gluetun: condition: service_healthy volumes: - ./sonarr:/config - ${DISK1}:${DISK1} - ${DISK2}:${DISK2} #- /etc/localtime:/etc/localtime:ro #- /etc/timezone:/etc/timezone:ro environment: - PUID=${HOST_PUID:-1000} - PGID=${HOST_PGID:-1000} - TZ=${TIMEZONE} labels: - "autoheal=true" healthcheck: test: [ "CMD-SHELL", "curl -sf '${WEB_PROTOCOL}://${EXTERNAL_IPV4}:${PLEX_PFWD_PORT}/identity' | grep -q 'MediaContainer' || exit 1", ] interval: 1m timeout: 10s retries: 3 start_period: 60s security_opt: - no-new-privileges:true mem_limit: 256m cpus: 1.0 restart: unless-stopped radarr: image: ${RADARR_IMAGE} container_name: radarr network_mode: "container:gluetun" depends_on: gluetun: condition: service_healthy volumes: - ./radarr:/config - ${DISK1}:${DISK1} - ${DISK2}:${DISK2} #- /etc/localtime:/etc/localtime:ro #- /etc/timezone:/etc/timezone:ro environment: - PUID=${HOST_PUID:-1000} - PGID=${HOST_PGID:-1000} - TZ=${TIMEZONE} labels: - "autoheal=true" healthcheck: test: [ "CMD-SHELL", "curl -sf '${WEB_PROTOCOL}://${EXTERNAL_IPV4}:${PLEX_PFWD_PORT}/identity' | grep -q 'MediaContainer' || exit 1", ] interval: 1m timeout: 10s retries: 3 start_period: 30s security_opt: - no-new-privileges:true mem_limit: 256m cpus: 1.0 restart: unless-stopped lidarr: image: ${LIDARR_IMAGE} container_name: lidarr network_mode: "container:gluetun" depends_on: gluetun: condition: service_healthy volumes: - ./lidarr:/music - ./lidarr:/config - ${DISK1}:${DISK1} - ${DISK2}:${DISK2} #- /etc/localtime:/etc/localtime:ro #- /etc/timezone:/etc/timezone:ro environment: - PUID=${HOST_PUID:-1000} - PGID=${HOST_PGID:-1000} - TZ=${TIMEZONE} labels: - "autoheal=true" healthcheck: test: [ "CMD-SHELL", "curl -sf '${WEB_PROTOCOL}://${EXTERNAL_IPV4}:${PLEX_PFWD_PORT}/identity' | grep -q 'MediaContainer' || exit 1", ] interval: 1m timeout: 10s retries: 3 start_period: 30s security_opt: - no-new-privileges:true mem_limit: 256m cpus: 1.0 restart: unless-stopped bazarr: image: ${BAZARR_IMAGE} container_name: bazarr network_mode: "container:gluetun" depends_on: gluetun: condition: service_healthy volumes: - ./bazarr:/config - ${DISK1}:${DISK1} - ${DISK2}:${DISK2} #- /etc/localtime:/etc/localtime:ro #- /etc/timezone:/etc/timezone:ro environment: - PUID=${HOST_PUID:-1000} - PGID=${HOST_PGID:-1000} - TZ=${TIMEZONE} labels: - "autoheal=true" healthcheck: test: [ "CMD-SHELL", "curl -sf '${WEB_PROTOCOL}://${EXTERNAL_IPV4}:${PLEX_PFWD_PORT}/identity' | grep -q 'MediaContainer' || exit 1", ] interval: 1m timeout: 10s retries: 3 start_period: 30s security_opt: - no-new-privileges:true mem_limit: 256m cpus: 1.0 restart: unless-stopped # --------------------------------------------------------------------------- # Media # --------------------------------------------------------------------------- tautulli: image: ${TAUTULLI_IMAGE} container_name: tautulli #networks: # - caddy_net ports: - ${TAUTULLI_PORT}:${TAUTULLI_PORT} volumes: - ./tautulli:/config #- /etc/localtime:/etc/localtime:ro #- /etc/timezone:/etc/timezone:ro environment: - PUID=${HOST_PUID:-1000} - PGID=${HOST_PGID:-1000} - TZ=${TIMEZONE} security_opt: - no-new-privileges:true mem_limit: 256m cpus: 1.0 restart: unless-stopped tdarr: image: ${TDARR_IMAGE} container_name: tdarr networks: - tdarr-net ports: - ${TDARR_PORT}:${TDARR_PORT} - ${TDARR_NODE_PORT}:${TDARR_NODE_PORT} volumes: - ./tdarr/server:/app/server - ./tdarr/configs:/app/configs - ./tdarr/temp:/temp - ${DISK1}:${DISK1} - ${DISK2}:${DISK2} #- /etc/localtime:/etc/localtime:ro #- /etc/timezone:/etc/timezone:ro environment: - TZ=${TIMEZONE} - PUID=${HOST_PUID:-1000} - PGID=${HOST_PGID:-1000} - UMASK_SET=002 - serverIP=0.0.0.0 security_opt: - no-new-privileges:true mem_limit: 512m cpus: 2.0 restart: unless-stopped tdarr-node: image: ${TDARR_NODE_IMAGE} container_name: tdarr-node networks: - tdarr-net volumes: - ./tdarr/configs:/app/configs - ./tdarr/logs:/app/logs - ./tdarr/temp:/temp - ${DISK1}:${DISK1} - ${DISK2}:${DISK2} #- /etc/localtime:/etc/localtime:ro #- /etc/timezone:/etc/timezone:ro environment: - TZ=${TIMEZONE} - PUID=${HOST_PUID:-1000} - PGID=${HOST_PGID:-1000} - nodeID=${TDARR_NODE_ID} - nodeIP=0.0.0.0 - serverIP=tdarr - serverPort=${TDARR_NODE_PORT} security_opt: - no-new-privileges:true mem_limit: 4g cpus: 4.0 devices: - /dev/dri:/dev/dri restart: unless-stopped seerr: image: ${SEERR_IMAGE} container_name: seerr # networks: # - caddy_net ports: - ${SEERR_PORT}:${SEERR_PORT} volumes: - ./seerr:/app/config #- /etc/localtime:/etc/localtime:ro #- /etc/timezone:/etc/timezone:ro environment: - LOG_LEVEL=info - TZ=${TIMEZONE} - PUID=${HOST_PUID:-1000} - PGID=${HOST_PGID:-1000} labels: - "autoheal=true" init: true working_dir: "/app" healthcheck: test: [ "CMD-SHELL", "wget -qO- '${WEB_PROTOCOL}://${EXTERNAL_IPV4}:${PLEX_PFWD_PORT}/identity' | grep -q 'MediaContainer' || exit 1", ] interval: 1m timeout: 10s retries: 3 start_period: 60s security_opt: - no-new-privileges:true mem_limit: 256m cpus: 1.0 restart: unless-stopped spotizerr: image: spotizerrphoenix/spotizerr user: "1000:1000" # Spotizerr user:group ids volumes: # Ensure these directories and the .cache file exist and are writable by the container user - ./spotizerr:/app/data # data directory, contains config, creds, watch, history - ${DISK1}/media/Music/spotizerr:/app/downloads # downloads directory, contains downloaded files - ./spotizerr/logs:/app/logs # logs directory, contains logs - ./spotizerr/.cache:/app/.cache # cache file ports: # Port to expose the app on - 7171:7171 container_name: spotizerr restart: unless-stopped env_file: # Ensure you have a .env file in the root of the project, with the correct values - ./.env depends_on: - redis redis: image: redis:alpine container_name: spotizerr-redis restart: unless-stopped env_file: - .env volumes: - redis-data:/data command: sh -c 'redis-server --requirepass "$REDIS_PASSWORD" --appendonly yes' # Anubis anti-bot proxy for Seerr (uncomment to enable): #seerr-anubis: # image: ghcr.io/techarohq/anubis:latest # container_name: seerr-anubis # networks: # - caddy_net # environment: # - BIND=:55055 # - TARGET=http://seerr:${SEERR_PORT} # security_opt: # - no-new-privileges:true # mem_limit: 128m # cpus: 0.5 # restart: unless-stopped