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.
This commit is contained in:
commit
3645d1314c
33 changed files with 2673 additions and 0 deletions
372
docker-compose.yml
Normal file
372
docker-compose.yml
Normal file
|
|
@ -0,0 +1,372 @@
|
|||
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=<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
|
||||
Loading…
Add table
Add a link
Reference in a new issue