406 lines
15 KiB
YAML
406 lines
15 KiB
YAML
name: arr-stack
|
||
|
||
################################################################################
|
||
# Arr stack
|
||
# gluetun WireGuard VPN client + firewall killswitch
|
||
# qbittorrent torrent client, lives INSIDE gluetun's network namespace
|
||
# prowlarr indexer manager (torrents + NZB/Usenet indexers)
|
||
# sabnzbd Usenet NZB download client (Newshosting, Tweaknews, …)
|
||
# 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
|
||
depends_on:
|
||
- flaresolverr
|
||
|
||
# Cloudflare / JS challenge proxy for Prowlarr indexers (not a general captcha solver)
|
||
# Wire in Prowlarr: Settings → Indexers → FlareSolverr → http://flaresolverr:8191
|
||
flaresolverr:
|
||
image: ghcr.io/flaresolverr/flaresolverr:latest
|
||
container_name: flaresolverr
|
||
restart: unless-stopped
|
||
networks: [arr-net]
|
||
environment:
|
||
LOG_LEVEL: ${FLARESOLVERR_LOG_LEVEL:-info}
|
||
LOG_HTML: "false"
|
||
CAPTCHA_SOLVER: ${FLARESOLVERR_CAPTCHA_SOLVER:-none}
|
||
TZ: ${TZ}
|
||
# Optional LAN debug UI/API — leave commented to keep it internal-only
|
||
# ports:
|
||
# - "8191:8191"
|
||
|
||
# ---------------------------------------------------------- usenet / NZB #
|
||
# SABnzbd talks to Usenet providers over SSL (no VPN required).
|
||
# Incomplete + complete under /data/usenet so Sonarr/Radarr can hardlink.
|
||
sabnzbd:
|
||
image: lscr.io/linuxserver/sabnzbd:latest
|
||
container_name: sabnzbd
|
||
restart: unless-stopped
|
||
networks: [arr-net]
|
||
ports:
|
||
- "${SABNZBD_PORT:-8085}:8080"
|
||
environment: *arr-env
|
||
volumes:
|
||
- ${CONFIG_DIR}/sabnzbd:/config
|
||
- ${DATA_DIR}:/data
|
||
|
||
# -------------------------------------------------------------------- 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=<query>
|
||
# 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/<id>
|
||
# 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
|