stack/docker-compose.yml
tim 3645d1314c Initial commit: arr-stack homelab + local AI
Docker Compose media stack (gluetun, *arr, Jellyfin), Ternary-Bonsai AI
(Open WebUI, Forgejo, optional OpenHands), and Ansible bootstrap for the
SER5 Ubuntu host.
2026-07-22 06:28:49 -07:00

372 lines
14 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 3264 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