name: arr-stack ################################################################################ # Arr stack # gluetun WireGuard VPN client + firewall killswitch # qbittorrent torrent client, lives INSIDE gluetun's network namespace # prowlarr indexer manager # sonarr TV radarr movies bazarr subtitles # jellyfin media server # # AI / coding stack (local, no VPN) # bonsai Prism Ternary-Bonsai-27B via llama-server (OpenAI-compatible API) # open-webui Chat UI + RAG + tools + code interpreter # searxng Private metasearch for web RAG # forgejo Self-hosted Git (repos for coding + OpenHands) # openhands Optional autonomous coding agent (profile: coding) # # Lockdown model: # * qbittorrent uses `network_mode: service:gluetun` -> it has NO network # interface of its own. Every packet it sends/receives goes through # gluetun, and gluetun's built-in firewall only lets traffic out via the # WireGuard tunnel. Tunnel down = zero connectivity. No leaks possible. # * Do NOT publish qBittorrent's BitTorrent listening port on the host - # inbound peer connections must come through the tunnel (port forwarding), # never via your real IP. # * AI services stay on arr-net (LAN only). They do NOT route through gluetun. ################################################################################ networks: arr-net: driver: bridge services: # ------------------------------------------------------------- VPN gateway # gluetun: image: qmcgaw/gluetun:v3 container_name: gluetun restart: unless-stopped cap_add: - NET_ADMIN devices: - /dev/net/tun:/dev/net/tun networks: - arr-net ports: # Ports for anything running inside gluetun's namespace are published # HERE, on gluetun - not on the inner container. - 8080:8080/tcp # qBittorrent WebUI volumes: # Your provider's WireGuard config, renamed to wg0.conf # (see wireguard/wg0.conf.example and the README for required edits) - ./wireguard/wg0.conf:/gluetun/wireguard/wg0.conf:ro environment: VPN_SERVICE_PROVIDER: custom VPN_TYPE: wireguard TZ: ${TZ} # Lets your LAN reach the qBittorrent WebUI through gluetun's firewall FIREWALL_OUTBOUND_SUBNETS: ${LAN_SUBNET} # --- optional: provider port forwarding (better swarm connectivity) --- # FIREWALL_VPN_INPUT_PORTS: "12345" # port your provider assigned you # VPN_PORT_FORWARDING: "on" # for providers with NAT-PMP API # VPN_PORT_FORWARDING_PROVIDER: protonvpn # --------------------------------------------------------- torrent client # qbittorrent: image: lscr.io/linuxserver/qbittorrent:latest container_name: qbittorrent restart: unless-stopped network_mode: service:gluetun # <-- the entire isolation trick depends_on: gluetun: condition: service_healthy # start only once the tunnel is up environment: PUID: ${PUID} PGID: ${PGID} TZ: ${TZ} WEBUI_PORT: "8080" volumes: - ${CONFIG_DIR}/qbittorrent:/config - ${DATA_DIR}:/data # single shared tree -> hardlinks # -------------------------------------------------------------- indexers # prowlarr: image: lscr.io/linuxserver/prowlarr:latest container_name: prowlarr restart: unless-stopped networks: [arr-net] ports: - 9696:9696 environment: &arr-env PUID: ${PUID} PGID: ${PGID} TZ: ${TZ} volumes: - ${CONFIG_DIR}/prowlarr:/config # -------------------------------------------------------------------- TV # sonarr: image: lscr.io/linuxserver/sonarr:latest container_name: sonarr restart: unless-stopped networks: [arr-net] ports: - 8989:8989 environment: *arr-env volumes: - ${CONFIG_DIR}/sonarr:/config - ${DATA_DIR}:/data # ----------------------------------------------------------------- movies # radarr: image: lscr.io/linuxserver/radarr:latest container_name: radarr restart: unless-stopped networks: [arr-net] ports: - 7878:7878 environment: *arr-env volumes: - ${CONFIG_DIR}/radarr:/config - ${DATA_DIR}:/data # -------------------------------------------------------------- subtitles # bazarr: image: lscr.io/linuxserver/bazarr:latest container_name: bazarr restart: unless-stopped networks: [arr-net] ports: - 6767:6767 environment: *arr-env volumes: - ${CONFIG_DIR}/bazarr:/config - ${DATA_DIR}:/data # ----------------------------------------------------------- media server # jellyfin: image: lscr.io/linuxserver/jellyfin:latest container_name: jellyfin restart: unless-stopped networks: [arr-net] ports: - 8096:8096/tcp # web UI / clients # - 8920:8920/tcp # optional HTTPS # - 7359:7359/udp # optional client autodiscovery environment: *arr-env volumes: - ${CONFIG_DIR}/jellyfin:/config - ${DATA_DIR}/media:/data/media:ro # read-only: Jellyfin never touches files # --- optional: Intel QuickSync hardware transcoding --- (Unused on this AMD machine) # devices: # - /dev/dri:/dev/dri # ------------------------------------------------------------------- music # lidarr: image: lscr.io/linuxserver/lidarr:latest container_name: lidarr restart: unless-stopped networks: [arr-net] ports: - 8686:8686 environment: *arr-env volumes: - ${CONFIG_DIR}/lidarr:/config - ${DATA_DIR}:/data # same /data tree as Sonarr/Radarr # ========================================================================== # Optional media extras - uncomment as needed # ========================================================================== # # jellyseerr: # request UI for Jellyfin # image: fallenbagel/jellyseerr:latest # container_name: jellyseerr # restart: unless-stopped # networks: [arr-net] # ports: ["5055:5055"] # environment: *arr-env # volumes: # - ${CONFIG_DIR}/jellyseerr:/app/config # ========================================================================== # Local AI — Ternary-Bonsai-27B + Open WebUI + RAG + coding helpers # Target: Beelink SER5 PRO (Ryzen 7 7735HS + Radeon 680M, no NVIDIA) # ========================================================================== # OpenAI-compatible API for Ternary-Bonsai-27B (PrismML Q2_0 GGUF) # First: ./scripts/download-bonsai-model.sh # Then: docker compose up -d --build bonsai open-webui searxng bonsai: build: context: ./ai dockerfile: Dockerfile args: # cpu = reliable default on SER5 PRO # vulkan = try Radeon 680M offload (also set NGL=99 and uncomment devices) BONSAI_BACKEND: ${BONSAI_BACKEND:-vulkan} image: arr-stack/bonsai:local container_name: bonsai restart: unless-stopped networks: [arr-net] ports: - "${BONSAI_PORT:-8081}:8080" # avoid clash with qBittorrent :8080 environment: TZ: ${TZ} MODEL_PATH: ${BONSAI_MODEL_PATH:-/models/Ternary-Bonsai-27B-Q2_0.gguf} # 0 = CPU (recommended start). Vulkan image: try 99 with /dev/dri mounted. NGL: ${BONSAI_NGL:-99} # 16k is a good default on 32–64 GB systems with media stack co-resident. # Long docs / full-repo: try 32768 or 65536; enable KV4=1 if RAM is tight. CTX_SIZE: ${BONSAI_CTX:-16384} TEMP: ${BONSAI_TEMP:-0.7} TOP_P: ${BONSAI_TOP_P:-0.95} TOP_K: ${BONSAI_TOP_K:-20} # 0 = auto; or set to physical cores (e.g. 8 on 7735HS) leaving some for Arrs THREADS: ${BONSAI_THREADS:-0} KV4: ${BONSAI_KV4:-0} EXTRA_ARGS: ${BONSAI_EXTRA_ARGS:-} volumes: - ${BONSAI_MODELS_DIR:-./models/bonsai}:/models:ro # Shared memory helps large context / mmap shm_size: "4gb" # Vulkan / AMD iGPU: devices + group_add live ONLY in # docker-compose.override.yml (Compose merges lists and rejects duplicates). healthcheck: test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8080/health"] interval: 30s timeout: 10s start_period: 180s retries: 5 # ChatGPT-style UI with built-in RAG, tools, and code interpreter open-webui: image: ghcr.io/open-webui/open-webui:main container_name: open-webui restart: unless-stopped networks: [arr-net] ports: - "${OPEN_WEBUI_PORT:-3000}:8080" depends_on: bonsai: condition: service_healthy searxng: condition: service_started environment: TZ: ${TZ} # Point at local llama-server (OpenAI-compatible) OPENAI_API_BASE_URL: http://bonsai:8080/v1 OPENAI_API_KEY: ${OPENAI_API_KEY:-sk-local-bonsai} # Disable Ollama unless you add it later ENABLE_OLLAMA_API: "false" # First account becomes admin; set WEBUI_AUTH=false only on trusted LAN WEBUI_AUTH: ${WEBUI_AUTH:-true} # RAG: local embeddings run inside open-webui (downloads on first use) RAG_EMBEDDING_ENGINE: ${RAG_EMBEDDING_ENGINE:-} CHUNK_SIZE: ${RAG_CHUNK_SIZE:-1000} CHUNK_OVERLAP: ${RAG_CHUNK_OVERLAP:-100} # Web search via local SearXNG ENABLE_RAG_WEB_SEARCH: "true" ENABLE_SEARCH_QUERY: "true" RAG_WEB_SEARCH_ENGINE: searxng SEARXNG_QUERY_URL: http://searxng:8080/search?q= # Allow fetching internal / LAN URLs for RAG (homelab docs) ENABLE_RAG_LOCAL_WEB_FETCH: "true" # Code interpreter (runs in open-webui sandbox) ENABLE_CODE_INTERPRETER: "true" # Reduce background LLM spam on a single local model ENABLE_TITLE_GENERATION: "false" ENABLE_FOLLOW_UP_GENERATION: "false" ENABLE_TAGS_GENERATION: "false" # Default model id as reported by llama-server /v1/models DEFAULT_MODELS: ${OPENWEBUI_DEFAULT_MODEL:-} volumes: - ${CONFIG_DIR}/open-webui:/app/backend/data # Your local files for RAG / "read my project" workflows - ${AI_WORKSPACE_DIR:-./workspace}:/workspace:ro # Optional: expose media library read-only for Q&A about your library tree # - ${DATA_DIR}/media:/media:ro extra_hosts: - "host.docker.internal:host-gateway" # Private metasearch (no API keys) for Open WebUI web RAG searxng: image: docker.io/searxng/searxng:latest container_name: searxng restart: unless-stopped networks: [arr-net] # Bind to localhost only — Open WebUI reaches it on arr-net ports: - "127.0.0.1:${SEARXNG_PORT:-8888}:8080" environment: SEARXNG_BASE_URL: http://localhost:${SEARXNG_PORT:-8888}/ SEARXNG_SECRET: ${SEARXNG_SECRET:-change-me-to-a-long-random-string} volumes: - ./searxng:/etc/searxng:rw cap_drop: - ALL cap_add: - CHOWN - SETGID - SETUID # ------------------------------------------------------------------------- # Forgejo — self-hosted Git (Gitea soft-fork). SQLite is fine for a homelab. # Web: http://host:3002 Git SSH: host:2222 (git@host:user/repo.git with port 2222) # First visit: create admin account, then create repos / push code. # ------------------------------------------------------------------------- forgejo: image: codeberg.org/forgejo/forgejo:11 container_name: forgejo restart: unless-stopped networks: [arr-net] ports: - "${FORGEJO_HTTP_PORT:-3002}:3000" - "${FORGEJO_SSH_PORT:-2222}:22" environment: USER_UID: ${PUID:-1000} USER_GID: ${PGID:-1000} TZ: ${TZ} # Image already runs OpenSSH on :22 (mapped to host FORGEJO_SSH_PORT). # Do NOT enable Forgejo's built-in SSH too — both bind :22 and the # process fatals with "address already in use" (connection refused on :3002). FORGEJO__server__START_SSH_SERVER: "false" FORGEJO__server__DISABLE_SSH: "false" FORGEJO__server__SSH_PORT: ${FORGEJO_SSH_PORT:-2222} FORGEJO__server__SSH_DOMAIN: ${FORGEJO_DOMAIN:-192.168.8.123} FORGEJO__server__DOMAIN: ${FORGEJO_DOMAIN:-192.168.8.123} FORGEJO__server__ROOT_URL: ${FORGEJO_ROOT_URL:-http://192.168.8.123:3002/} FORGEJO__server__HTTP_PORT: "3000" # SQLite by default (no extra DB container) FORGEJO__database__DB_TYPE: sqlite3 FORGEJO__service__DISABLE_REGISTRATION: ${FORGEJO_DISABLE_REGISTRATION:-false} FORGEJO__actions__ENABLED: "true" volumes: - ${CONFIG_DIR}/forgejo:/data - /etc/localtime:/etc/localtime:ro # ------------------------------------------------------------------------- # OpenHands — autonomous coding agent (opt-in) # Start with: docker compose --profile coding up -d openhands # UI: http://host:3001 → Settings → custom model openai/ # Base URL: http://bonsai:8080/v1 API key: sk-local-bonsai # Needs Docker socket to spawn sandboxed runtimes. # ------------------------------------------------------------------------- openhands: profiles: ["coding"] # Prefer GHCR — docker.all-hands.dev often fails DNS / is decommissioned image: ghcr.io/all-hands-ai/openhands:0.54 container_name: openhands restart: unless-stopped networks: [arr-net] ports: - "${OPENHANDS_PORT:-3001}:3000" extra_hosts: - "host.docker.internal:host-gateway" environment: TZ: ${TZ} SANDBOX_RUNTIME_CONTAINER_IMAGE: ghcr.io/all-hands-ai/runtime:0.54-nikolaik LOG_ALL_EVENTS: "true" # Pre-wire local Bonsai (override in UI if model id differs) LLM_BASE_URL: http://bonsai:8080/v1 LLM_API_KEY: ${OPENAI_API_KEY:-sk-local-bonsai} LLM_MODEL: ${OPENHANDS_MODEL:-openai/local} WORKSPACE_MOUNT_PATH: ${AI_WORKSPACE_DIR:-./workspace} SANDBOX_VOLUMES: ${AI_WORKSPACE_DIR:-./workspace}:/workspace:rw volumes: - /var/run/docker.sock:/var/run/docker.sock - ${CONFIG_DIR}/openhands:/.openhands-state - ${AI_WORKSPACE_DIR:-./workspace}:/opt/workspace_base depends_on: bonsai: condition: service_healthy