Skip to main content

3. Docker Compose File

Okey then! So you have created an NFS share, created the Trash Guides folder structure inside of that NFS share then now it is time to use that to setup the docker compose file. The compose file comes from different websites and it is surely not the only way to do this. However, it is a simple one that creates one docker network called  internal proxy and uses gluetun as a VPN container. Furthermore, we will use the open ports 4001 - 4012 for the applications that we are running. You can keep the original official port numbers but I like to keep it structured instead of random port numbers. With all of that said the compose file looks like this

compose.yml
services:
# Sonarr - TV Shows
  sonarr:
    image: lscr.io/linuxserver/sonarr:latest
    container_name: sonarr
    environment:
      - PUID=${PUID}
      - PGID=${PGID}
      - TZ=${TZ} 
    volumes:
      - ./sonarr/config:/config
      - ${ROOT_MEDIA_PATH}:/data # Access to /data/downloads and /data/media/tv
    ports:
      - 4001:8989
    restart: unless-stopped
    networks:
      - internal-proxy

  # Radarr - Movies
  radarr:
    image: lscr.io/linuxserver/radarr:latest
    container_name: radarr
    environment:
      - PUID=${PUID}
      - PGID=${PGID}
      - TZ=${TZ}
    volumes:
      - ./radarr/config:/config
      - ${ROOT_MEDIA_PATH}:/data # Access to /data/downloads and /data/media/movies
    ports:
      - 4002:7878
    restart: unless-stopped
    networks:
      - internal-proxy

# Prowlarr - Indexer centralized management
  prowlarr:
    image: lscr.io/linuxserver/prowlarr:latest
    container_name: prowlarr
    environment:
      - PUID=${PUID}
      - PGID=${PGID}
      - TZ=${TZ}
    volumes:
      - ./prowlarr/config:/config
    ports:
      - 4003:9696
    restart: unless-stopped
    networks:
      - internal-proxy

# Profilarr
  profilarr:
    image: ghcr.io/dictionarry-hub/profilarr:latest
    container_name: profilarr
    restart: unless-stopped
    ports:
      - "4004:6868"
    volumes:
      - ./config:/config
    environment:
      - PUID=${PUID}
      - PGID=${PGID}
      - UMASK=022
      - TZ=${TZ}
    depends_on:
      - radarr
      - sonarr

# FlareSolverr - Bypasses Cloudflare protection for Indexers
  flaresolverr:
    image: ghcr.io/flaresolverr/flaresolverr:latest
    container_name: flaresolverr
    environment:
      - LOG_LEVEL=${LOG_LEVEL:-info}
      - TZ=${TZ}
    ports:
      - 4005:8191
    restart: unless-stopped
    networks:
      - internal-proxy

# bazarr for subtitles
  bazarr:
    image: lscr.io/linuxserver/bazarr:latest
    container_name: bazarr
    environment:
      - PUID=${PUID}
      - PGID=${PGID}
      - TZ=${TZ}
    volumes:
      - ./bazarr/config:/config
      - ${ROOT_MEDIA_PATH}:/data
    ports:
      - 4006:6767
    restart: unless-stopped
    networks:
      - internal-proxy

  maintainerr:
    image: ghcr.io/maintainerr/maintainerr:latest # or ghcr.io/maintainerr/maintainerr:development (to test unreleased changes)
    container_name: maintainerr
    user: 1000:1000
    volumes:
      - type: bind
        source: /mnt/data
        target: /opt/data
#          - type: bind # uncomment for the leftover-folder cleanup: your library, at the same path Radarr/Sonarr report it at
#            source: /path/to/media
#            target: /path/to/media
    environment:
      - TZ=Europe/Amterdam
#          - BASE_PATH=/maintainerr # uncomment if you're serving maintainerr from a subdirectory
#          - UI_HOSTNAME=:: # uncomment if you want to listen on IPv6 instead (default 0.0.0.0)
#          - UI_PORT=4007 # uncomment to change the UI port (default 6246)
#          - GITHUB_TOKEN=ghp_yourtoken # Optional: GitHub Personal Access Token for higher API rate limits (60/hr without, 5000/hr with token)
    ports:
      - 4007:6246
    restart: unless-stopped
    healthcheck: # already baked into the image; included here so you can tune it
      test: ['CMD', '/opt/app/healthcheck.sh']
      interval: 30s
      timeout: 5s
      start_period: 40s
      retries: 3
    networks:
      - internal-proxy

  seerr:
    image: ghcr.io/seerr-team/seerr:latest
    init: true
    container_name: seerr
    environment:
      - PUID=${PUID}
      - PGID=${PGID}
      - LOG_LEVEL=debug
      - TZ=${TZ}
      - PORT=5055 #optional
    ports:
      - 4011:5055
    volumes:
      - ./seerrconfig:/app/config
    healthcheck:
      test: wget --no-verbose --tries=1 --spider http://10.69.1.103:4011/api/v1/settings/public || exit 1
      start_period: 20s
      timeout: 3s
      interval: 15s
      retries: 3
    restart: unless-stopped
    networks:
      - internal-proxy


  qbittorrent:
    image: lscr.io/linuxserver/qbittorrent
    container_name: qbittorrent
    network_mode: "service:gluetun"  # Routes all traffic through Gluetun
    depends_on:
      gluetun:
        condition: service_healthy # Wait for VPN to be up first
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=${TZ}
      - WEBUI_PORT=4009
      - WEBUI_ADDRESS=0.0.0.0
      - WEBUI_EXTERNAL_ACCESS=true
    volumes:
      - ./qbittorrent:/config
      - ${ROOT_MEDIA_PATH}:/data
    restart: unless-stopped

  nzbget:
    image: lscr.io/linuxserver/nzbget:latest
    container_name: nzbget
    environment:
      - PUID=${PUID}
      - PGID=${PGID}
      - TZ=${TZ}
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - ./nzbget:/config
      - /mnt/data:/data
    ports:
      - 4008:6789
    restart: unless-stopped
    networks:
      - internal-proxy

  jellydash:
    image: ghcr.io/themartz90/jellydash:latest
    restart: unless-stopped
    ports:
      - "${APP_PORT:-8080}:80"
    environment:
      PUID: "${PUID}"
      PGID: "${PGID}"
      TZ: "${TZ}"
      APP_ENV: "${APP_ENV:-production}"
      APP_DEBUG: "${APP_DEBUG:-false}"
      APP_TIMEZONE: "${APP_TIMEZONE:-UTC}"
      # Bundled MariaDB below. Using your own database server instead? Point
      # DB_HOST at it and drop the db service in docker-compose.override.yml.
      DB_HOST: "${DB_HOST:-db}"
      DB_PORT: "${DB_PORT:-3306}"
      DB_DRIVER: mysqli
      DB_NAME: "${DB_NAME:-jellydash}"
      DB_USER: "${DB_USER:-jellydash}"
      DB_PASS: "${DB_PASS:-change-me}"
      JELLYFIN_URL: "${JELLYFIN_URL}"
      JELLYFIN_API_TOKEN: "${JELLYFIN_API_TOKEN}"
      JELLYFIN_VERIFY_SSL: "${JELLYFIN_VERIFY_SSL:-true}"
      LIBRARIES_CACHE_TTL: "${LIBRARIES_CACHE_TTL:-300}"
      # Jellyseerr (optional; the page hides until these are set)
      JELLYSEER_URL: "${JELLYSEER_URL:-}"
      JELLYSEER_API_TOKEN: "${JELLYSEER_API_TOKEN:-}"
      JELLYSEER_VERIFY_SSL: "${JELLYSEER_VERIFY_SSL:-true}"
      SEERR_POLL_INTERVAL: "${SEERR_POLL_INTERVAL:-120}"
      SEERR_NOTIFY_ENABLED: "${SEERR_NOTIFY_ENABLED:-true}"
      # Background poller (history recording, cache warming, alerts)
      POLLER_ENABLED: "${POLLER_ENABLED:-true}"
      POLL_INTERVAL: "${POLL_INTERVAL:-30}"
      # Web Push notifications (optional; generate keys once, see README)
      PUSH_ENABLED: "${PUSH_ENABLED:-true}"
      VAPID_PUBLIC_KEY: "${VAPID_PUBLIC_KEY:-}"
      VAPID_PRIVATE_KEY: "${VAPID_PRIVATE_KEY:-}"
      VAPID_SUBJECT: "${VAPID_SUBJECT:-mailto:[email protected]}"
      PUSH_IGNORE_USERS: "${PUSH_IGNORE_USERS:-}"
      # Public base URL (optional): external alert links point back here.
      APP_URL: "${APP_URL:-}"
      # Extra notification channels; each is active once its config is set.
      TELEGRAM_BOT_TOKEN: "${TELEGRAM_BOT_TOKEN:-}"
      TELEGRAM_CHAT_ID: "${TELEGRAM_CHAT_ID:-}"
      PUSHOVER_APP_TOKEN: "${PUSHOVER_APP_TOKEN:-}"
      PUSHOVER_USER_KEY: "${PUSHOVER_USER_KEY:-}"
      DISCORD_WEBHOOK_URL: "${DISCORD_WEBHOOK_URL:-}"
      # Libraries hidden from Trending/Most Watched (also editable in Settings)
      TRENDING_EXCLUDE_LIBRARIES: "${TRENDING_EXCLUDE_LIBRARIES:-}"
      # Optional login gate (recommended when internet-facing)
      AUTH_ENABLED: "${AUTH_ENABLED:-false}"
      AUTH_ADMIN_USER: "${AUTH_ADMIN_USER:-}"
      AUTH_ADMIN_PASSWORD: "${AUTH_ADMIN_PASSWORD:-}"
    depends_on:
      db:
        condition: service_healthy
    volumes:
      - ./jellydash_cache:/var/www/html/cache
      - ./jellydash_runtime_cache:/var/www/html/var/cache
      - ./jellydash_logs:/var/www/html/var/log
      - ./jellydash_uploads:/var/www/html/public/uploads
      # Drop-in feature modules (see docs/MODULES.md), e.g.:
      # - ./my-modules/downloads:/var/www/html/modules/downloads:ro
    networks:
      - internal-proxy

  db:
    image: mariadb:11
    restart: unless-stopped
    environment:
      MARIADB_RANDOM_ROOT_PASSWORD: "yes"
      MARIADB_DATABASE: "${DB_NAME:-jellydash}"
      MARIADB_USER: "${DB_USER:-jellydash}"
      MARIADB_PASSWORD: "${DB_PASS:-change-me}"
      PUID: "${PUID}"
      PGID: "${PGID}"
      TZ: "${TZ}"
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
      interval: 5s
      timeout: 5s
      retries: 12
    volumes:
      - ./jellydash_data:/var/lib/mysql
    networks:
      - internal-proxy

  gluetun:
    image: qmcgaw/gluetun:latest
    container_name: gluetun
    cap_add:
      - NET_ADMIN
    environment:
      - PUID=${PUID}
      - PGID=${PGID}
      - TZ=${TZ}
      - VPN_SERVICE_PROVIDER=${VPN_SERVICE_PROVIDER}
      - VPN_TYPE=${VPN_TYPE}
      - WIREGUARD_PRIVATE_KEY=${WIREGUARD_PRIVATE_KEY}
      - WIREGUARD_ADDRESSES=${WIREGUARD_ADDRESSES}
      - VPN_PORT_FORWARDING=${VPN_PORT_FORWARDING}
    ports:
      - 8888:8888 # Health check and control
      - 4009:4009
#      - 4009:6881 # qBittorrent P2P port
#      - 4009:6881/udp
    restart: unless-stopped
    networks:
      - internal-proxy

networks:
  internal-proxy:
    driver: bridge

 

and the associated environment file looks like this

.env
# User and Group IDs for Permissions
PUID=1000
PGID=1000
TZ=Europe/Amsterdam

# Global Paths for Download Client and *Arr Apps
DOWNLOADS_PATH=/mnt/data
MEDIA_PATH=/mnt/data
ROOT_MEDIA_PATH=/mnt/data

# Network Settings
PROXY_NETWORK=internal-proxy

VPN_SERVICE_PROVIDER=protonvpn
VPN_TYPE=wireguard
WIREGUARD_PRIVATE_KEY=private-key
WIREGUARD_ADDRESSES=x.x.x.x/32
VPN_PORT_FORWARDING=yes

# Jellydash configuration.
# ------------------------------------------------------------------
# Basics
# ------------------------------------------------------------------

# Leave as production. The local mode is only for development.
APP_ENV=production
APP_DEBUG=false

# The port Jellydash runs on, so http://your-host:8080
APP_PORT=4010

# Your timezone (IANA name, e.g. Europe/Prague). Used for timestamps
# and the daily statistics boundaries.
APP_TIMEZONE=Europe/Amsterdam

# Public address of your dashboard. Optional, can stay empty.
# When set, notification alerts include a link back to the app.
APP_URL=

#Database

DB_HOST=db
DB_PORT=3306
DB_NAME=jellydash
DB_USER=jellydash
# Required. Pick any password, the bundled database is created with it.
DB_PASS=9u2983fjosdjhf9823jf9jisodf

# ------------------------------------------------------------------
# Jellyfin (required)
# ------------------------------------------------------------------

# Required. The address of your Jellyfin server.
JELLYFIN_URL=https://jellyfin.example.org

# Required. An API key from Jellyfin > Dashboard > API Keys. Use an admin
# key, some statistics need to read library paths.
JELLYFIN_API_TOKEN=jellyfin-api-token

# Set false only when your Jellyfin runs on self-signed HTTPS.
JELLYFIN_VERIFY_SSL=true

# How long the Libraries page cache lives, in seconds. The default is fine.
LIBRARIES_CACHE_TTL=300

# ------------------------------------------------------------------
# Jellyseerr (optional, can stay empty)
# ------------------------------------------------------------------
# Fill these and the Jellyseerr page appears in the menu, including alerts
# for new requests. API key: Jellyseerr > Settings > General > API Key.

JELLYSEER_URL=
JELLYSEER_API_TOKEN=
JELLYSEER_VERIFY_SSL=true

# Seconds between request syncs, and whether new requests send an alert.
SEERR_POLL_INTERVAL=120
SEERR_NOTIFY_ENABLED=true

# ------------------------------------------------------------------
# Background poller
# ------------------------------------------------------------------
# Records play history even when nobody has the dashboard open, and sends
# the notifications. You want this on.

POLLER_ENABLED=true

# Seconds between Jellyfin checks. 30 is a good balance.
POLL_INTERVAL=30

# ------------------------------------------------------------------
# Notifications (all optional, can stay empty)
# ------------------------------------------------------------------
# Alerts when someone starts playing and when a new Jellyseerr request
# comes in. A channel is on as soon as its values are filled. Setup steps
# for every channel are in the README.

# Master switch for all notification channels.
PUSH_ENABLED=true

# Jellyfin usernames that never trigger an alert, comma-separated.
# Typically your own. Also editable later in Settings inside the app.
PUSH_IGNORE_USERS=

# ------------------------------------------------------------------
# Login (optional)
# ------------------------------------------------------------------
# Off by default. Turn it on when the dashboard is reachable from the
# internet. The admin user is created automatically on the next start.

AUTH_ENABLED=false
AUTH_ADMIN_USER=jkodfk
# The password needs at least 8 characters, shorter ones are rejected.
AUTH_ADMIN_PASSWORD=tJT9off2KfCN2Ug0x8CI

# ------------------------------------------------------------------
# Statistics
# ------------------------------------------------------------------

# Jellyfin libraries to hide from Trending and Most Watched, comma-separated
# names. Also editable later in Settings inside the app.
TRENDING_EXCLUDE_LIBRARIES=

 


I keep every tunable value in a .env file rather than hardcoding it in the Compose file. Ports, paths, and the user IDs all live in one place, which makes the Compose file readable and easy to reuse. I also run every service on an external network called media_network, which means the network is created once, outside of Compose, and the stack attaches to it. That keeps the network stable even when I tear the stack down and bring it back up.

Sweet! So you want to install the arr stack on your self hosted device. Well, as with any app you need decide which app workflow deployment you want to use. Here, we focus on the docker compose deployment using a dedicated VM. Now, personally I like to run the arr stack in a dedicated VM and Jellyfin in another Docker VM. This gives you the possibility to move your Jellyfin instance around easily to different hosts and if something is wrong with your arr stack then your users can still enjoy the already downloaded media. Eventually, it is up to you and the amount of resources you have at your disposal what your decision will be.

1. Create a new docker vm by using the proxmox helper scripts ;) Or if you want to continue using your existing docker vm then you need 

2. 

Create a project directory and the .env file:

Hottio Decluttar

Whisparr is the Sonarr/Radarr equivalent for adult content 

Whisper-AI generates subtitles on the fly for your media

compose.yml
services:
# Sonarr - TV Shows
  sonarr:
    image: lscr.io/linuxserver/sonarr:latest
    container_name: sonarr
    environment:
      - PUID=${PUID}
      - PGID=${PGID}
      - TZ=${TZ}
    volumes:
      - ./sonarr/config:/config
      - ${ROOT_MEDIA_PATH}:/data # Access to /data/downloads and /data/media/tv
    ports:
      - 8989:8989
    restart: unless-stopped
    networks:
      - internal-proxy

  # Radarr - Movies
  radarr:
    image: lscr.io/linuxserver/radarr:latest
    container_name: radarr
    environment:
      - PUID=${PUID}
      - PGID=${PGID}
      - TZ=${TZ}
    volumes:
      - ./radarr/config:/config
      - ${ROOT_MEDIA_PATH}:/data # Access to /data/downloads and /data/media/movies
    ports:
      - 7878:7878
    restart: unless-stopped
    networks:
      - internal-proxy

networks:
  internal-proxy:
    driver: bridge

# Prowlarr - Indexer centralized management
  prowlarr:
    image: lscr.io/linuxserver/prowlarr:latest
    container_name: prowlarr
    environment:
      - PUID=${PUID}
      - PGID=${PGID}
      - TZ=${TZ}
    volumes:
      - ./prowlarr/config:/config
    ports:
      - 9696:9696
    restart: unless-stopped
    networks:
      - internal-proxy

# Profilarr & Decluttarr (Config only, no media access needed usually)
  profilarr:
    image: santiagosayshey/profilarr:latest
    container_name: profilarr
    environment:
      - TZ=${TZ}
    volumes:
      - ./profilarr/config:/config
    ports:
      - 6868:6868
    restart: unless-stopped
    networks:
      - internal-proxy
    depends_on:
      - sonarr
      - radarr

# FlareSolverr - Bypasses Cloudflare protection for Indexers
  flaresolverr:
    image: ghcr.io/flaresolverr/flaresolverr:latest
    container_name: flaresolverr
    environment:
      - LOG_LEVEL=${LOG_LEVEL:-info}
      - TZ=${TZ}
    ports:
      - 8191:8191
    restart: unless-stopped
    networks:
      - internal-proxy
	  
# Gluetun - VPN Client
  gluetun:
    image: qmcgaw/gluetun:latest
    container_name: gluetun
    cap_add:
      - NET_ADMIN
    environment:
      - PUID=${PUID}
      - PGID=${PGID}
      - TZ=${TZ}
      - VPN_SERVICE_PROVIDER=${VPN_SERVICE_PROVIDER}
      - VPN_TYPE=${VPN_TYPE}
      - WIREGUARD_PRIVATE_KEY=${WIREGUARD_PRIVATE_KEY}
      - WIREGUARD_ADDRESSES=${WIREGUARD_ADDRESSES}
      - VPN_PORT_FORWARDING=${VPN_PORT_FORWARDING}
    ports:
      - 8888:8888 # Health check and control
      - 6881:6881 # qBittorrent P2P port
      - 6881:6881/udp
    restart: unless-stopped
    networks:
      - internal-proxy

# If using Gluetun, use qbittorrent from linuxserver io, NOT the hotio image below!
  qbittorrent:
    image: lscr.io/linuxserver/qbittorrent
    network_mode: "service:gluetun" # The "Magic" line
    depends_on:
      gluetun:
        condition: service_healthy # Wait for VPN to be up first
	  
# Lidarr - Music
  lidarr:
    image: lscr.io/linuxserver/lidarr:latest
    container_name: lidarr
    environment:
      - PUID=${PUID}
      - PGID=${PGID}
      - TZ=${TZ}
    volumes:
      - ./lidarr/config:/config
      - ${ROOT_MEDIA_PATH}:/data
    ports:
      - 8686:8686
    restart: unless-stopped
    networks:
      - internal-proxy

  # Bazarr - Subtitles
  bazarr:
    image: lscr.io/linuxserver/bazarr:latest
    container_name: bazarr
    environment:
      - PUID=${PUID}
      - PGID=${PGID}
      - TZ=${TZ}
    volumes:
      - ./bazarr/config:/config
      - ${ROOT_MEDIA_PATH}:/data
    ports:
      - 6767:6767
    restart: unless-stopped
    networks:
      - internal-proxy

  # whisper-ai - Local Subtitle Generation (Requires GPU)
  whisper-ai:
    image: ${WHISPER_IMAGE_URL} # Use a pre-built image like "jellyfin-whisper-lab/whisper-container"
    container_name: whisper-ai
    environment:
      - PUID=${PUID}
      - PGID=${PGID}
      - TZ=${TZ}
    volumes:
      - ./whisper/config:/config
      - ${ROOT_MEDIA_PATH}:/data
    # Enable GPU access if you have one, or comment out the 'deploy' section for CPU-only:
    # deploy:
    #   resources:
    #     reservations:
    #       devices:
    #         - driver: nvidia
    #           count: all
    #           capabilities: [gpu]
    restart: unless-stopped
    networks:
      - internal-proxy

  # Decluttarr - Queue Cleaner
  decluttarr:
    image: hotio/decluttarr:latest
    container_name: decluttarr
    environment:
      - PUID=${PUID}
      - PGID=${PGID}
      - TZ=${TZ}
      - DECLUTTARR_CRON="*/15 * * * *" 
      # API Keys/URLs for Sonarr, Radarr, etc. go here post-setup!
    volumes:
      - ./decluttarr/config:/config
    restart: unless-stopped
    networks:
      - internal-proxy

  nzbget:
    image: lscr.io/linuxserver/nzbget:latest
    container_name: nzbget
    environment:
      - PUID=${PUID}
      - PGID=${PGID}
      - TZ=${TZ}
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - ./nzbget:/config
      - /mnt/data:/data
    restart: unless-stopped
    networks:
      - internal-proxy

  jellydash:
    image: ghcr.io/themartz90/jellydash:latest
    restart: unless-stopped
    ports:
      - "${APP_PORT:-8080}:80"
    environment:
      APP_ENV: "${APP_ENV:-production}"
      APP_DEBUG: "${APP_DEBUG:-false}"
      APP_TIMEZONE: "${APP_TIMEZONE:-UTC}"
      # Bundled MariaDB below. Using your own database server instead? Point
      # DB_HOST at it and drop the db service in docker-compose.override.yml.
      DB_HOST: "${DB_HOST:-db}"
      DB_PORT: "${DB_PORT:-3306}"
      DB_DRIVER: mysqli
      DB_NAME: "${DB_NAME:-jellydash}"
      DB_USER: "${DB_USER:-jellydash}"
      DB_PASS: "${DB_PASS:-change-me}"
      JELLYFIN_URL: "${JELLYFIN_URL}"
      JELLYFIN_API_TOKEN: "${JELLYFIN_API_TOKEN}"
      JELLYFIN_VERIFY_SSL: "${JELLYFIN_VERIFY_SSL:-true}"
      LIBRARIES_CACHE_TTL: "${LIBRARIES_CACHE_TTL:-300}"
      # Jellyseerr (optional; the page hides until these are set)
      JELLYSEER_URL: "${JELLYSEER_URL:-}"
      JELLYSEER_API_TOKEN: "${JELLYSEER_API_TOKEN:-}"
      JELLYSEER_VERIFY_SSL: "${JELLYSEER_VERIFY_SSL:-true}"
      SEERR_POLL_INTERVAL: "${SEERR_POLL_INTERVAL:-120}"
      SEERR_NOTIFY_ENABLED: "${SEERR_NOTIFY_ENABLED:-true}"
      # Background poller (history recording, cache warming, alerts)
      POLLER_ENABLED: "${POLLER_ENABLED:-true}"
      POLL_INTERVAL: "${POLL_INTERVAL:-30}"
      # Web Push notifications (optional; generate keys once, see README)
      PUSH_ENABLED: "${PUSH_ENABLED:-true}"
      VAPID_PUBLIC_KEY: "${VAPID_PUBLIC_KEY:-}"
      VAPID_PRIVATE_KEY: "${VAPID_PRIVATE_KEY:-}"
      VAPID_SUBJECT: "${VAPID_SUBJECT:-mailto:[email protected]}"
      PUSH_IGNORE_USERS: "${PUSH_IGNORE_USERS:-}"
      # Public base URL (optional): external alert links point back here.
      APP_URL: "${APP_URL:-}"
      # Extra notification channels; each is active once its config is set.
      TELEGRAM_BOT_TOKEN: "${TELEGRAM_BOT_TOKEN:-}"
      TELEGRAM_CHAT_ID: "${TELEGRAM_CHAT_ID:-}"
      PUSHOVER_APP_TOKEN: "${PUSHOVER_APP_TOKEN:-}"
      PUSHOVER_USER_KEY: "${PUSHOVER_USER_KEY:-}"
      DISCORD_WEBHOOK_URL: "${DISCORD_WEBHOOK_URL:-}"
      # Libraries hidden from Trending/Most Watched (also editable in Settings)
      TRENDING_EXCLUDE_LIBRARIES: "${TRENDING_EXCLUDE_LIBRARIES:-}"
      # Optional login gate (recommended when internet-facing)
      AUTH_ENABLED: "${AUTH_ENABLED:-false}"
      AUTH_ADMIN_USER: "${AUTH_ADMIN_USER:-}"
      AUTH_ADMIN_PASSWORD: "${AUTH_ADMIN_PASSWORD:-}"
    depends_on:
      db:
        condition: service_healthy
    volumes:
      - ./jellydash_cache:/var/www/html/cache
      - ./jellydash_runtime_cache:/var/www/html/var/cache
      - ./jellydash_logs:/var/www/html/var/log
      - ./jellydash_uploads:/var/www/html/public/uploads
      # Drop-in feature modules (see docs/MODULES.md), e.g.:
      # - ./my-modules/downloads:/var/www/html/modules/downloads:ro

  db:
    image: mariadb:11
    restart: unless-stopped
    environment:
      MARIADB_RANDOM_ROOT_PASSWORD: "yes"
      MARIADB_DATABASE: "${DB_NAME:-jellydash}"
      MARIADB_USER: "${DB_USER:-jellydash}"
      MARIADB_PASSWORD: "${DB_PASS:-change-me}"
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
      interval: 5s
      timeout: 5s
      retries: 12
    volumes:
      - ./jellydash_data:/var/lib/mysql

 

Gluetun

Source: ThomasWildeTech Arr stack docker compose file

Media Server Janitors

https://corelab.tech/maintainerr-declutarr-media-automation-guide/