stack/docker-compose.yml

471 lines
17 KiB
YAML
Raw Normal View History

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
# landing Caddy — dark LAN homepage (port 80)
#
# 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
# -------------------------------- Caddy: landing + HTTPS reverse proxy --- #
# LAN map: http://192.168.8.123/
# Public: https://forgejo.devopshomelab.com https://jellyfin.devopshomelab.com
# https://rustdesk.devopshomelab.com (info page) https://devopshomelab.com
# Let's Encrypt HTTP-01 needs WAN :80/:443 → this host and grey-cloud DNS.
landing:
image: caddy:2-alpine
container_name: landing
restart: unless-stopped
networks: [arr-net]
ports:
- "${LANDING_PORT:-80}:80"
- "${LANDING_HTTPS_PORT:-443}:443"
environment:
CADDY_EMAIL: ${CADDY_EMAIL:-admin@devopshomelab.com}
volumes:
- ./landing/Caddyfile:/etc/caddy/Caddyfile:ro
- ./landing/site:/srv:ro
- ${CONFIG_DIR}/caddy-data:/data
- ${CONFIG_DIR}/caddy-config:/config
depends_on:
- forgejo
- jellyfin
# ---------------------------------------------- Cloudflare dynamic DNS (A) #
# Updates apex + subdomains to the host's public IPv4. Secrets from secrets/.env
# Render env first: ./scripts/render-cloudflare-ddns-env.sh
cloudflare-ddns:
image: timothyjmiller/cloudflare-ddns:latest
container_name: cloudflare-ddns
restart: unless-stopped
# Host networking so we see the real WAN IPv4 (not a Docker NAT address)
network_mode: host
security_opt:
- no-new-privileges:true
# Generated by: ./scripts/render-cloudflare-ddns-env.sh (from secrets/.env)
# Path is relative to the compose project dir (/opt/stack).
env_file:
- ./config/cloudflare-ddns/ddns.env
# ------------------------------------------------------------------- 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 llama-server (Prism binary — Bonsai Q2_0 + standard GGUF)
# Profiles (./scripts/apply-profile.sh mini|mid|gaming) set model + CPU/Vulkan.
# mini=CPU · mid/gaming=Vulkan · download: ./scripts/download-models.sh
bonsai:
build:
context: ./ai
dockerfile: Dockerfile
args:
# cpu | vulkan — from STACK_PROFILE via .env (mini defaults cpu)
BONSAI_BACKEND: ${BONSAI_BACKEND:-cpu}
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}
NGL: ${BONSAI_NGL:-0}
CTX_SIZE: ${BONSAI_CTX:-16384}
TEMP: ${BONSAI_TEMP:-0.7}
TOP_P: ${BONSAI_TOP_P:-0.95}
TOP_K: ${BONSAI_TOP_K:-20}
THREADS: ${BONSAI_THREADS:-0}
KV4: ${BONSAI_KV4:-0}
EXTRA_ARGS: ${BONSAI_EXTRA_ARGS:-}
volumes:
- ${BONSAI_MODELS_DIR:-./models/bonsai}:/models:ro
shm_size: "4gb"
# Vulkan devices: docker-compose.override.yml (and mid/gaming profiles)
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:-forgejo.devopshomelab.com}
FORGEJO__server__DOMAIN: ${FORGEJO_DOMAIN:-forgejo.devopshomelab.com}
# Public HTTPS via Caddy (see landing/Caddyfile). LAN still works on :3002.
FORGEJO__server__ROOT_URL: ${FORGEJO_ROOT_URL:-https://forgejo.devopshomelab.com/}
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
# -------------------------------------------------------------------------
# RustDesk server (OSS) — rustdesk.devopshomelab.com
# Clients use TCP/UDP 2111521119 (not HTTP). Open those ports on the router.
# Public key: docker exec rustdesk-hbbs cat /root/id_ed25519.pub
# -------------------------------------------------------------------------
rustdesk-hbbr:
image: rustdesk/rustdesk-server:latest
container_name: rustdesk-hbbr
restart: unless-stopped
networks: [arr-net]
command: hbbr
volumes:
- ${CONFIG_DIR}/rustdesk:/root
ports:
- "21117:21117"
- "21119:21119"
rustdesk-hbbs:
image: rustdesk/rustdesk-server:latest
container_name: rustdesk-hbbs
restart: unless-stopped
networks: [arr-net]
# -r = relay host:port advertised to clients
command: hbbs -r ${RUSTDESK_DOMAIN:-rustdesk.devopshomelab.com}:21117
volumes:
- ${CONFIG_DIR}/rustdesk:/root
ports:
- "21115:21115"
- "21116:21116"
- "21116:21116/udp"
- "21118:21118"
depends_on:
- rustdesk-hbbr
# -------------------------------------------------------------------------
# OpenHands — always deployed (this stack always brings up every service)
# UI: http://host:3001 · LLM via bonsai OpenAI API
# -------------------------------------------------------------------------
openhands:
# 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"
LLM_BASE_URL: http://bonsai:8080/v1
LLM_API_KEY: ${OPENAI_API_KEY:-sk-local-bonsai}
LLM_MODEL: ${OPENHANDS_MODEL:-openai/Ternary-Bonsai-27B-Q2_0.gguf}
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