🎬 ARR Suite Template Bootstrap - Complete Media Automation Stack Features: - 16 production services (Prowlarr, Sonarr, Radarr, Plex, etc.) - One-command Ansible deployment - VPN-protected downloads via Gluetun - Tailscale secure access - Production-ready security (UFW, Fail2Ban) - Automated backups and monitoring - Comprehensive documentation Ready for customization and deployment to any VPS. Co-authored-by: openhands <openhands@all-hands.dev>
575 lines
16 KiB
Django/Jinja
575 lines
16 KiB
Django/Jinja
---
|
|
# 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 |