Files
arr-suite-template-bootstrap/templates/docker-compose.yml.j2
openhands 24f2cd64e9 Initial template repository
🎬 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>
2025-11-28 04:26:12 +00:00

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