--- # Docker Compose for Arrs Media Stack # Adapted from Dr. Frankenstein's guide for VPS deployment # Generated by Ansible - Do not edit manually version: '3.8' services: sonarr: image: linuxserver/sonarr:latest container_name: sonarr environment: - PUID={{ docker_uid }} - PGID={{ docker_gid }} - TZ={{ timezone }} - UMASK=022 volumes: - {{ docker_root }}/sonarr:/config - {{ media_root }}:/data ports: {% if bind_to_tailscale_only %} - "{{ tailscale_bind_ip }}:{{ ports.sonarr }}:8989/tcp" # Tailscale only {% else %} - "{{ ports.sonarr }}:8989/tcp" # All interfaces {% endif %} networks: - arrs_network security_opt: - no-new-privileges:true restart: always healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8989/ping"] interval: 30s timeout: 10s retries: 3 start_period: 40s labels: - "com.centurylinklabs.watchtower.enable=true" radarr: image: linuxserver/radarr:latest container_name: radarr environment: - PUID={{ docker_uid }} - PGID={{ docker_gid }} - TZ={{ timezone }} - UMASK=022 volumes: - {{ docker_root }}/radarr:/config - {{ media_root }}:/data ports: {% if bind_to_tailscale_only %} - "{{ tailscale_bind_ip }}:{{ ports.radarr }}:7878/tcp" # Tailscale only {% else %} - "{{ ports.radarr }}:7878/tcp" # All interfaces {% endif %} networks: - arrs_network security_opt: - no-new-privileges:true restart: always healthcheck: test: ["CMD", "curl", "-f", "http://localhost:7878/ping"] interval: 30s timeout: 10s retries: 3 start_period: 40s labels: - "com.centurylinklabs.watchtower.enable=true" lidarr: image: linuxserver/lidarr:latest container_name: lidarr environment: - PUID={{ docker_uid }} - PGID={{ docker_gid }} - TZ={{ timezone }} - UMASK=022 volumes: - {{ docker_root }}/lidarr:/config - {{ media_root }}:/data ports: {% if bind_to_tailscale_only %} - "{{ tailscale_bind_ip }}:{{ ports.lidarr }}:8686/tcp" # Tailscale only {% else %} - "{{ ports.lidarr }}:8686/tcp" # All interfaces {% endif %} networks: - arrs_network security_opt: - no-new-privileges:true restart: always healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8686/ping"] interval: 30s timeout: 10s retries: 3 start_period: 40s labels: - "com.centurylinklabs.watchtower.enable=true" bazarr: image: linuxserver/bazarr:latest container_name: bazarr environment: - PUID={{ docker_uid }} - PGID={{ docker_gid }} - TZ={{ timezone }} - UMASK=022 volumes: - {{ docker_root }}/bazarr:/config - {{ media_root }}:/data ports: {% if bind_to_tailscale_only %} - "{{ tailscale_bind_ip }}:{{ ports.bazarr }}:6767/tcp" # Tailscale only {% else %} - "{{ ports.bazarr }}:6767/tcp" # All interfaces {% endif %} networks: - arrs_network security_opt: - no-new-privileges:true restart: always healthcheck: test: ["CMD", "curl", "-f", "http://localhost:6767/ping"] interval: 30s timeout: 10s retries: 3 start_period: 40s labels: - "com.centurylinklabs.watchtower.enable=true" prowlarr: image: linuxserver/prowlarr:latest container_name: prowlarr environment: - PUID={{ docker_uid }} - PGID={{ docker_gid }} - TZ={{ timezone }} - UMASK=022 volumes: - {{ docker_root }}/prowlarr:/config ports: {% if bind_to_tailscale_only %} - "{{ tailscale_bind_ip }}:{{ ports.prowlarr }}:9696/tcp" # Tailscale only {% else %} - "{{ ports.prowlarr }}:9696/tcp" # All interfaces {% endif %} networks: - arrs_network security_opt: - no-new-privileges:true restart: always healthcheck: test: ["CMD", "curl", "-f", "http://localhost:9696/ping"] interval: 30s timeout: 10s retries: 3 start_period: 40s labels: - "com.centurylinklabs.watchtower.enable=true" whisparr: image: ghcr.io/hotio/whisparr container_name: whisparr environment: - PUID={{ docker_uid }} - PGID={{ docker_gid }} - TZ={{ timezone }} - UMASK=022 volumes: - {{ docker_root }}/whisparr:/config - {{ media_root }}:/data - {{ media_root }}/xxx:/data/xxx # Adult content directory ports: {% if bind_to_tailscale_only %} - "{{ tailscale_bind_ip }}:{{ ports.whisparr }}:6969/tcp" # Tailscale only {% else %} - "{{ ports.whisparr }}:6969/tcp" # All interfaces {% endif %} networks: - arrs_network security_opt: - no-new-privileges:true restart: always healthcheck: test: ["CMD", "curl", "-f", "http://localhost:6969/ping"] interval: 30s timeout: 10s retries: 3 start_period: 40s labels: - "com.centurylinklabs.watchtower.enable=true" sabnzbd: image: linuxserver/sabnzbd:latest container_name: sabnzbd environment: - PUID={{ docker_uid }} - PGID={{ docker_gid }} - TZ={{ timezone }} - UMASK=022 {% if vpn_enabled and sabnzbd_vpn_enabled %} - WEBUI_PORT=8081 # Use different port when through VPN to avoid qBittorrent conflict {% endif %} volumes: - {{ docker_root }}/sabnzbd:/config - {{ media_root }}/downloads:/downloads - {{ media_root }}/downloads/incomplete:/incomplete-downloads {% if vpn_enabled and sabnzbd_vpn_enabled %} network_mode: "service:gluetun" # Route through VPN depends_on: - gluetun {% else %} ports: {% if bind_to_tailscale_only %} - "{{ tailscale_bind_ip }}:{{ ports.sabnzbd }}:8080/tcp" # Tailscale only {% else %} - "{{ ports.sabnzbd }}:8080/tcp" # All interfaces {% endif %} networks: - arrs_network {% endif %} security_opt: - no-new-privileges:true restart: always healthcheck: {% if vpn_enabled and sabnzbd_vpn_enabled %} test: ["CMD", "curl", "-f", "http://localhost:8081/api?mode=version"] {% else %} test: ["CMD", "curl", "-f", "http://localhost:8080/api?mode=version"] {% endif %} interval: 30s timeout: 10s retries: 3 start_period: 40s labels: - "com.centurylinklabs.watchtower.enable=true" plex: image: linuxserver/plex:latest container_name: plex environment: - PUID={{ docker_uid }} - PGID={{ docker_gid }} - TZ={{ timezone }} - VERSION=docker - PLEX_CLAIM={{ plex_claim_token | default('') }} volumes: - {{ docker_root }}/plex:/config - {{ media_root }}/movies:/movies:ro - {{ media_root }}/tv:/tv:ro - {{ media_root }}/music:/music:ro ports: {% if plex_public_access %} - "{{ ports.plex }}:32400/tcp" # Public access for direct streaming {% elif bind_to_tailscale_only %} - "{{ tailscale_bind_ip }}:{{ ports.plex }}:32400/tcp" # Tailscale only {% else %} - "{{ ports.plex }}:32400/tcp" # All interfaces {% endif %} networks: - arrs_network security_opt: - no-new-privileges:true restart: always healthcheck: test: ["CMD", "curl", "-f", "http://localhost:32400/web"] interval: 30s timeout: 10s retries: 3 start_period: 60s labels: - "com.centurylinklabs.watchtower.enable=true" tautulli: image: linuxserver/tautulli:latest container_name: tautulli environment: - PUID={{ docker_uid }} - PGID={{ docker_gid }} - TZ={{ timezone }} volumes: - {{ docker_root }}/tautulli:/config ports: {% if bind_to_tailscale_only %} - "{{ tailscale_bind_ip }}:{{ ports.tautulli }}:8181/tcp" # Tailscale only {% else %} - "{{ ports.tautulli }}:8181/tcp" # All interfaces {% endif %} networks: - arrs_network security_opt: - no-new-privileges:true restart: always healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8181/status"] interval: 30s timeout: 10s retries: 3 start_period: 40s labels: - "com.centurylinklabs.watchtower.enable=true" jellyseerr: image: fallenbagel/jellyseerr:latest container_name: jellyseerr environment: - LOG_LEVEL=debug - TZ={{ timezone }} volumes: - {{ docker_root }}/jellyseerr:/app/config ports: {% if bind_to_tailscale_only %} - "{{ tailscale_bind_ip }}:{{ ports.jellyseerr }}:5055/tcp" # Tailscale only {% else %} - "{{ ports.jellyseerr }}:5055/tcp" # All interfaces {% endif %} networks: - arrs_network security_opt: - no-new-privileges:true restart: always healthcheck: test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:5055/api/v1/status"] interval: 30s timeout: 10s retries: 3 start_period: 40s labels: - "com.centurylinklabs.watchtower.enable=true" {% if vpn_enabled %} gluetun: image: qmcgaw/gluetun:latest container_name: gluetun cap_add: - NET_ADMIN devices: - /dev/net/tun:/dev/net/tun environment: - VPN_SERVICE_PROVIDER={{ vpn_provider | default('') }} - VPN_TYPE={{ vpn_type | default('openvpn') }} {% if vpn_type == 'wireguard' %} - WIREGUARD_PRIVATE_KEY={{ wireguard_private_key | default('') }} - WIREGUARD_ADDRESSES={{ wireguard_addresses | default('') }} - WIREGUARD_PUBLIC_KEY={{ wireguard_public_key | default('') }} - VPN_ENDPOINT_IP={{ wireguard_endpoint.split(':')[0] | default('') }} - VPN_ENDPOINT_PORT={{ wireguard_endpoint.split(':')[1] | default('51820') }} {% else %} - OPENVPN_USER={{ openvpn_user | default('') }} - OPENVPN_PASSWORD={{ openvpn_password | default('') }} {% if vpn_provider == 'custom' %} - OPENVPN_CUSTOM_CONFIG=/gluetun/custom.conf {% endif %} {% endif %} {% if vpn_provider != 'custom' and vpn_type != 'wireguard' %} - SERVER_COUNTRIES={{ vpn_countries | default('') }} {% endif %} - FIREWALL_OUTBOUND_SUBNETS={{ docker_network_subnet }} - FIREWALL_VPN_INPUT_PORTS=8080{% if sabnzbd_vpn_enabled %},8081{% endif %} # Allow WebUI access - FIREWALL=on # Enable firewall kill switch - DOT=off # Disable DNS over TLS to prevent leaks - BLOCK_MALICIOUS=on # Block malicious domains - BLOCK_ADS=off # Keep ads blocking off to avoid issues - UNBLOCK= # No unblocking needed - TZ={{ timezone }} volumes: - {{ docker_root }}/gluetun:/gluetun ports: {% if bind_to_tailscale_only %} - "{{ tailscale_bind_ip }}:{{ ports.sabnzbd }}:8081/tcp" # SABnzbd WebUI through VPN (Tailscale only) - "{{ tailscale_bind_ip }}:{{ ports.deluge }}:8112/tcp" # Deluge WebUI through VPN (Tailscale only) {% else %} - "{{ ports.sabnzbd }}:8081/tcp" # SABnzbd WebUI through VPN (all interfaces) - "{{ ports.deluge }}:8112/tcp" # Deluge WebUI through VPN (all interfaces) {% endif %} networks: - arrs_network security_opt: - no-new-privileges:true restart: always healthcheck: test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://www.google.com/"] interval: 60s timeout: 30s retries: 3 start_period: 120s labels: - "com.centurylinklabs.watchtower.enable=true" {% endif %} deluge: image: linuxserver/deluge:latest container_name: deluge environment: - PUID={{ docker_uid }} - PGID={{ docker_gid }} - TZ={{ timezone }} - UMASK=022 - DELUGE_LOGLEVEL=error volumes: - {{ docker_root }}/deluge:/config - {{ media_root }}/downloads:/downloads {% if vpn_enabled %} network_mode: "service:gluetun" # Route through VPN depends_on: - gluetun {% else %} ports: {% if bind_to_tailscale_only %} - "{{ tailscale_bind_ip }}:{{ ports.deluge }}:8112/tcp" # Tailscale only {% else %} - "{{ ports.deluge }}:8112/tcp" # All interfaces {% endif %} networks: - arrs_network {% endif %} security_opt: - no-new-privileges:true restart: always healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8112/"] interval: 30s timeout: 10s retries: 3 start_period: 60s labels: - "com.centurylinklabs.watchtower.enable=true" # TubeArchivist stack - YouTube archiving tubearchivist-es: image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0 container_name: tubearchivist-es environment: - "ELASTIC_PASSWORD=verysecret" - "ES_JAVA_OPTS=-Xms1g -Xmx1g" - "xpack.security.enabled=true" - "discovery.type=single-node" - "path.repo=/usr/share/elasticsearch/data/snapshot" volumes: - {{ docker_root }}/tubearchivist/es:/usr/share/elasticsearch/data networks: - arrs_network security_opt: - no-new-privileges:true restart: always healthcheck: test: ["CMD", "curl", "-u", "elastic:verysecret", "-f", "http://localhost:9200/_cluster/health"] interval: 30s timeout: 10s retries: 3 start_period: 60s labels: - "com.centurylinklabs.watchtower.enable=true" tubearchivist-redis: image: redis/redis-stack-server:latest container_name: tubearchivist-redis volumes: - {{ docker_root }}/tubearchivist/redis:/data networks: - arrs_network security_opt: - no-new-privileges:true restart: always healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 30s timeout: 10s retries: 3 start_period: 30s labels: - "com.centurylinklabs.watchtower.enable=true" tubearchivist: image: bbilly1/tubearchivist:latest container_name: tubearchivist environment: - ES_URL=http://tubearchivist-es:9200 - REDIS_CON=redis://tubearchivist-redis:6379 - HOST_UID={{ docker_uid }} - HOST_GID={{ docker_gid }} - TA_HOST=http://{{ tailscale_bind_ip }}:{{ ports.tubearchivist }} - TA_USERNAME=tubearchivist - TA_PASSWORD=verysecret - ELASTIC_PASSWORD=verysecret - TZ={{ timezone }} volumes: - {{ media_root }}/youtube:/youtube - {{ docker_root }}/tubearchivist/cache:/cache ports: {% if bind_to_tailscale_only %} - "{{ tailscale_bind_ip }}:{{ ports.tubearchivist }}:8000/tcp" # Tailscale only {% else %} - "{{ ports.tubearchivist }}:8000/tcp" # All interfaces {% endif %} networks: - arrs_network depends_on: - tubearchivist-es - tubearchivist-redis security_opt: - no-new-privileges:true restart: always healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8000/health/"] interval: 30s timeout: 10s retries: 3 start_period: 60s labels: - "com.centurylinklabs.watchtower.enable=true" {% if watchtower_enabled %} watchtower: image: containrrr/watchtower:1.7.1 container_name: watchtower environment: - TZ={{ timezone }} - WATCHTOWER_SCHEDULE={{ watchtower_schedule }} - WATCHTOWER_CLEANUP={{ watchtower_cleanup | lower }} - WATCHTOWER_LABEL_ENABLE=true - WATCHTOWER_INCLUDE_RESTARTING=true - DOCKER_API_VERSION=1.44 volumes: - /var/run/docker.sock:/var/run/docker.sock - {{ docker_root }}/watchtower:/config networks: - arrs_network security_opt: - no-new-privileges:true restart: always labels: - "com.centurylinklabs.watchtower.enable=false" {% endif %} {% if log_rotation_enabled %} logrotate: image: blacklabelops/logrotate:latest container_name: logrotate environment: - LOGS_DIRECTORIES=/var/lib/docker/containers /logs - LOGROTATE_INTERVAL=daily - LOGROTATE_COPIES=7 - LOGROTATE_SIZE=100M volumes: - /var/lib/docker/containers:/var/lib/docker/containers:ro - {{ docker_root }}/logs:/logs networks: - arrs_network restart: always labels: - "com.centurylinklabs.watchtower.enable=true" {% endif %} networks: arrs_network: driver: bridge ipam: config: - subnet: {{ docker_network_subnet }} gateway: {{ docker_network_gateway }} volumes: sonarr_config: driver: local radarr_config: driver: local lidarr_config: driver: local bazarr_config: driver: local prowlarr_config: driver: local