feat: add media server
This commit is contained in:
34
modules/20-services-apps/arr/.env.example
Normal file
34
modules/20-services-apps/arr/.env.example
Normal file
@@ -0,0 +1,34 @@
|
||||
# Example secrets for the *arr stack (used via terraform-provider-dotenv)
|
||||
# Copy to .env in this same directory and fill in values.
|
||||
|
||||
# Required keys for helpers
|
||||
SONARR_API_KEY=
|
||||
RADARR_API_KEY=
|
||||
LIDARR_API_KEY=
|
||||
QBITTORRENT_USERNAME=
|
||||
QBITTORRENT_PASSWORD=
|
||||
|
||||
# Optional keys for Flaresolverr
|
||||
LOG_LEVEL=info
|
||||
LOG_HTML=false
|
||||
CAPTCHA_SOLVER=none
|
||||
|
||||
# Optional keys for Decluttarr
|
||||
# Uncomment and adjust as needed. Defaults shown here align with the README.
|
||||
#DECLUTTARR_LOG_LEVEL=INFO
|
||||
#DECLUTTARR_TEST_RUN=False
|
||||
#DECLUTTARR_REMOVE_TIMER=10
|
||||
#DECLUTTARR_REMOVE_FAILED=True
|
||||
#DECLUTTARR_REMOVE_FAILED_IMPORTS=True
|
||||
#DECLUTTARR_REMOVE_METADATA_MISSING=True
|
||||
#DECLUTTARR_REMOVE_MISSING_FILES=True
|
||||
#DECLUTTARR_REMOVE_ORPHANS=True
|
||||
#DECLUTTARR_REMOVE_SLOW=True
|
||||
#DECLUTTARR_REMOVE_STALLED=True
|
||||
#DECLUTTARR_REMOVE_UNMONITORED=True
|
||||
#DECLUTTARR_RUN_PERIODIC_RESCANS=
|
||||
#DECLUTTARR_PERMITTED_ATTEMPTS=3
|
||||
#DECLUTTARR_REMOVAL_QBIT_TAG=stalled
|
||||
#DECLUTTARR_MIN_DOWNLOAD_SPEED=100
|
||||
#DECLUTTARR_FAILED_IMPORT_MESSAGE_PATTERNS=
|
||||
#DECLUTTARR_IGNORED_DOWNLOAD_CLIENTS=
|
||||
140
modules/20-services-apps/arr/README.md
Normal file
140
modules/20-services-apps/arr/README.md
Normal file
@@ -0,0 +1,140 @@
|
||||
# *arr Stack Module
|
||||
|
||||
This module deploys the *arr media stack components as Docker containers and provides a service definition for Jellyseerr to be published via your reverse proxy.
|
||||
|
||||
## Overview
|
||||
|
||||
The module includes the following containers:
|
||||
|
||||
- Sonarr (TV)
|
||||
- Radarr (Movies)
|
||||
- Lidarr (Music)
|
||||
- Bazarr (Subtitles)
|
||||
- Prowlarr (Indexers)
|
||||
- Jellyseerr (Requests) — published via reverse proxy
|
||||
- Flaresolverr (optional helper)
|
||||
- Unpackerr (post-processing)
|
||||
- Cleanuparr (cleanup helpers)
|
||||
- Decluttarr (torrent queue cleanup helper)
|
||||
|
||||
All containers share a common `/data` mount (media root) and are attached to the media Docker network. Only Jellyseerr is also attached to the proxy network for reachability by Caddy.
|
||||
|
||||
## Usage
|
||||
|
||||
```hcl
|
||||
module "arr" {
|
||||
source = "./modules/20-services-apps/arr"
|
||||
volume_path = "/srv/appdata/arr" # host path for config directories
|
||||
data_path = "/srv/data" # host media root, mounted as /data in containers
|
||||
downloads_path = "/srv/data/torrents" # host downloads dir, mounted for unpackerr
|
||||
networks = [module.media_docker_network.name]
|
||||
proxy_networks = [module.homelab_docker_network.name] # so Jellyseerr is reachable by Caddy
|
||||
}
|
||||
```
|
||||
|
||||
## Variables
|
||||
|
||||
| Variable | Description | Type | Default |
|
||||
| ------------------ | ------------------------------------------------------------------------------- | -------------- | --------------- |
|
||||
| `volume_path` | Base directory for config volumes for the *arr stack | `string` | - |
|
||||
| `data_path` | Base directory for media/data mounted at `/data` | `string` | - |
|
||||
| `downloads_path` | Directory for downloads mounted at `/data/torrents` (Unpackerr) | `string` | - |
|
||||
| `networks` | Networks to attach all containers to | `list(string)` | `[]` |
|
||||
| `proxy_networks` | Extra networks attached only to published services (Jellyseerr) | `list(string)` | `[]` |
|
||||
| `qbittorrent_host` | Hostname to reach qBittorrent (use `gluetun` when qBittorrent shares Gluetun) | `string` | `"qbittorrent"` |
|
||||
|
||||
## Outputs
|
||||
|
||||
| Output | Description |
|
||||
| -------------------- | --------------------------------------------------------------------- |
|
||||
| `service_definition` | Service definition for integration with networking modules |
|
||||
|
||||
## Service Definition
|
||||
|
||||
This module outputs a service definition that is used by the networking modules to expose the service.
|
||||
|
||||
```hcl
|
||||
{
|
||||
name = "jellyseerr"
|
||||
primary_port = 5055
|
||||
endpoint = "http://jellyseerr:5055"
|
||||
subdomains = ["req"]
|
||||
publish_via = "reverse_proxy"
|
||||
proxied = false
|
||||
}
|
||||
```
|
||||
|
||||
## Environment Variables (.env)
|
||||
|
||||
This module reads secrets via the `dotenv` provider from a `.env` file located in this module directory. Use `.env.example` as a template.
|
||||
|
||||
Required/used keys:
|
||||
|
||||
- `SONARR_API_KEY` — for Unpackerr, Cleanuparr, Decluttarr
|
||||
- `RADARR_API_KEY` — for Unpackerr, Cleanuparr, Decluttarr
|
||||
- `LIDARR_API_KEY` — for Decluttarr
|
||||
- `QBITTORRENT_USERNAME` — for Decluttarr
|
||||
- `QBITTORRENT_PASSWORD` — for Cleanuparr and Decluttarr
|
||||
- `LOG_LEVEL` — for Flaresolverr (optional)
|
||||
- `LOG_HTML` — for Flaresolverr (optional)
|
||||
- `CAPTCHA_SOLVER` — for Flaresolverr (optional)
|
||||
|
||||
Optional Decluttarr keys (override defaults as needed):
|
||||
|
||||
- `DECLUTTARR_LOG_LEVEL` (default: `INFO`)
|
||||
- `DECLUTTARR_TEST_RUN` (default: `False`)
|
||||
- `DECLUTTARR_REMOVE_TIMER` (default: `10`)
|
||||
- `DECLUTTARR_REMOVE_FAILED` (default: `True`)
|
||||
- `DECLUTTARR_REMOVE_FAILED_IMPORTS` (default: `True`)
|
||||
- `DECLUTTARR_REMOVE_METADATA_MISSING` (default: `True`)
|
||||
- `DECLUTTARR_REMOVE_MISSING_FILES` (default: `True`)
|
||||
- `DECLUTTARR_REMOVE_ORPHANS` (default: `True`)
|
||||
- `DECLUTTARR_REMOVE_SLOW` (default: `True`)
|
||||
- `DECLUTTARR_REMOVE_STALLED` (default: `True`)
|
||||
- `DECLUTTARR_REMOVE_UNMONITORED` (default: `True`)
|
||||
- `DECLUTTARR_RUN_PERIODIC_RESCANS` (default: empty)
|
||||
- `DECLUTTARR_PERMITTED_ATTEMPTS` (default: `3`)
|
||||
- `DECLUTTARR_REMOVAL_QBIT_TAG` (default: `stalled`)
|
||||
- `DECLUTTARR_MIN_DOWNLOAD_SPEED` (default: `100`)
|
||||
- `DECLUTTARR_FAILED_IMPORT_MESSAGE_PATTERNS` (default: empty)
|
||||
- `DECLUTTARR_IGNORED_DOWNLOAD_CLIENTS` (default: empty)
|
||||
|
||||
Note: `TZ`, `PUID`, `PGID` are injected automatically by the generic docker-service module from `modules/00-globals/system` and do not need to be in this `.env`.
|
||||
|
||||
## Data Persistence
|
||||
|
||||
- Each app stores configuration under `${volume_path}/<app>/...` mounted to `/config` (or as noted by the specific app).
|
||||
- Media library and downloads are accessed under `/data` inside containers, pointing to `data_path` on the host.
|
||||
- Unpackerr mounts `downloads_path` to `/data/torrents`.
|
||||
|
||||
## Networking
|
||||
|
||||
- All containers join `networks` (media network).
|
||||
- Jellyseerr additionally joins `proxy_networks` for reverse proxy reachability.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- No explicit inter-container dependencies are defined. Healthchecks are provided for stable orchestration.
|
||||
- Decluttarr expects to reach `sonarr`, `radarr`, and `lidarr` via internal DNS and qBittorrent at `http://<qbittorrent_host>:8080`. When qBittorrent is routed via Gluetun using `network_mode=container:gluetun`, set `qbittorrent_host = "gluetun"`. Ensure they share the same Docker network.
|
||||
|
||||
## Integration with Networking Modules
|
||||
|
||||
This service is configured to be exposed through the Caddy reverse proxy, set by `publish_via = "reverse_proxy"`.
|
||||
|
||||
## Example Integration in Main Configuration
|
||||
|
||||
```hcl
|
||||
# In services/main.tf
|
||||
module "arr" {
|
||||
source = "${local.module_dir}/20-services-apps/arr"
|
||||
volume_path = "${local.volume_host}/arr"
|
||||
data_path = local.data_host
|
||||
downloads_path = "${local.data_host}/torrents"
|
||||
networks = [module.media_docker_network.name]
|
||||
proxy_networks = [module.homelab_docker_network.name]
|
||||
# If qBittorrent shares Gluetun's network namespace, arr should reach it via 'gluetun'
|
||||
qbittorrent_host = "gluetun"
|
||||
}
|
||||
```
|
||||
|
||||
The service definition is exported by the `services` module as `module.services.service_definitions` and consumed by networking modules in the root `main.tf`.
|
||||
280
modules/20-services-apps/arr/main.tf
Normal file
280
modules/20-services-apps/arr/main.tf
Normal file
@@ -0,0 +1,280 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
dotenv = { source = "germanbrew/dotenv" }
|
||||
}
|
||||
}
|
||||
|
||||
variable "volume_path" {
|
||||
description = "Base directory for config volumes for *arr stack"
|
||||
type = string
|
||||
}
|
||||
variable "data_path" {
|
||||
description = "Base directory for media/data mounted at /data"
|
||||
type = string
|
||||
}
|
||||
variable "downloads_path" {
|
||||
description = "Directory for downloads mounted at /data/torrents"
|
||||
type = string
|
||||
}
|
||||
variable "networks" {
|
||||
description = "Networks to attach all containers to"
|
||||
type = list(string)
|
||||
default = []
|
||||
}
|
||||
variable "proxy_networks" {
|
||||
description = "Extra networks to attach only to published services (e.g., Jellyseerr)"
|
||||
type = list(string)
|
||||
default = []
|
||||
}
|
||||
|
||||
variable "qbittorrent_host" {
|
||||
description = "Hostname to reach qBittorrent (use 'qbittorrent' normally, 'gluetun' when qBittorrent shares Gluetun network)"
|
||||
type = string
|
||||
default = "qbittorrent"
|
||||
}
|
||||
|
||||
locals {
|
||||
env_file = "${path.module}/.env"
|
||||
monitoring = true
|
||||
|
||||
sonarr_name = "sonarr"
|
||||
radarr_name = "radarr"
|
||||
lidarr_name = "lidarr"
|
||||
bazarr_name = "bazarr"
|
||||
prowlarr_name = "prowlarr"
|
||||
jellyseerr_name = "jellyseerr"
|
||||
flaresolverr_name = "flaresolverr"
|
||||
unpackerr_name = "unpackerr"
|
||||
cleanuparr_name = "cleanuparr"
|
||||
decluttarr_name = "decluttarr"
|
||||
|
||||
sonarr_image = "lscr.io/linuxserver/sonarr"
|
||||
radarr_image = "lscr.io/linuxserver/radarr"
|
||||
lidarr_image = "lscr.io/linuxserver/lidarr"
|
||||
bazarr_image = "lscr.io/linuxserver/bazarr"
|
||||
prowlarr_image = "lscr.io/linuxserver/prowlarr"
|
||||
jellyseerr_image = "ghcr.io/fallenbagel/jellyseerr"
|
||||
flaresolverr_image = "21hsmw/flaresolverr"
|
||||
unpackerr_image = "ghcr.io/unpackerr/unpackerr"
|
||||
cleanuparr_image = "ghcr.io/cleanuparr/cleanuparr"
|
||||
decluttarr_image = "ghcr.io/manimatter/decluttarr"
|
||||
|
||||
sonarr_port = 8989
|
||||
radarr_port = 7878
|
||||
lidarr_port = 8686
|
||||
bazarr_port = 6767
|
||||
prowlarr_port = 9696
|
||||
jellyseerr_port = 5055
|
||||
flaresolverr_port = 8191
|
||||
|
||||
lidarr_healthcheck = { test = ["CMD", "curl", "--fail", "http://127.0.0.1:${local.lidarr_port}/lidarr/ping"], interval = "60s", timeout = "5s", retries = 10 }
|
||||
bazarr_healthcheck = { test = ["CMD", "curl", "--fail", "http://127.0.0.1:${local.bazarr_port}/bazarr/ping"], interval = "60s", timeout = "5s", retries = 10 }
|
||||
jellyseerr_healthcheck = { test = ["CMD", "wget", "http://127.0.0.1:${local.jellyseerr_port}/api/v1/status", "-qO", "/dev/null"], interval = "60s", timeout = "5s", retries = 10 }
|
||||
|
||||
jellyseerr_env = { LOG_LEVEL = "debug" }
|
||||
flaresolverr_env = {
|
||||
LOG_LEVEL = try(provider::dotenv::get_by_key("LOG_LEVEL", local.env_file), "")
|
||||
LOG_HTML = try(provider::dotenv::get_by_key("LOG_HTML", local.env_file), "")
|
||||
CAPTCHA_SOLVER = try(provider::dotenv::get_by_key("CAPTCHA_SOLVER", local.env_file), "")
|
||||
}
|
||||
unpackerr_env = {
|
||||
UN_SONARR_0_URL = "http://${local.sonarr_name}:${local.sonarr_port}/sonarr"
|
||||
UN_SONARR_0_API_KEY = provider::dotenv::get_by_key("SONARR_API_KEY", local.env_file)
|
||||
UN_RADARR_0_URL = "http://${local.radarr_name}:${local.radarr_port}/radarr"
|
||||
UN_RADARR_0_API_KEY = provider::dotenv::get_by_key("RADARR_API_KEY", local.env_file)
|
||||
}
|
||||
cleanuparr_env = {
|
||||
QUEUECLEANER__ENABLED = true
|
||||
QUEUECLEANER__IMPORT_FAILED_MAX_STRIKES = 3
|
||||
QUEUECLEANER__STALLED_MAX_STRIKES = 3
|
||||
QUEUECLEANER__DOWNLOADING_METADATA_MAX_STRIKES = 3
|
||||
QUEUECLEANER__STALLED_RESET_STRIKES_ON_PROGRESS = true
|
||||
TRIGGERS__QUEUECLEANER = "0 0 0/1 * * ?"
|
||||
CONTENTBLOCKER__ENABLED = true
|
||||
CONTENTBLOCKER__IGNORED_DOWNLOADS_PATH = "/usr/ignored.txt"
|
||||
TRIGGERS__CONTENTBLOCKER = "0 0 0/1 * * ?"
|
||||
DOWNLOAD_CLIENT = "qBittorrent"
|
||||
QBITTORRENT__URL = "http://${var.qbittorrent_host}:8080"
|
||||
QBITTORRENT__PASSWORD = provider::dotenv::get_by_key("QBITTORRENT_PASSWORD", local.env_file)
|
||||
SONARR__ENABLED = true
|
||||
SONARR__BLOCK__PATH = "/usr/blacklist.json"
|
||||
SONARR__INSTANCES__0__URL = "http://${local.sonarr_name}:${local.sonarr_port}/sonarr"
|
||||
SONARR__INSTANCES__0__APIKEY = provider::dotenv::get_by_key("SONARR_API_KEY", local.env_file)
|
||||
RADARR__ENABLED = true
|
||||
RADARR__BLOCK__PATH = "/usr/blacklist.json"
|
||||
RADARR__INSTANCES__0__URL = "http://${local.radarr_name}:${local.radarr_port}/radarr"
|
||||
RADARR__INSTANCES__0__APIKEY = provider::dotenv::get_by_key("RADARR_API_KEY", local.env_file)
|
||||
}
|
||||
|
||||
decluttarr_env = {
|
||||
RADARR_URL = "http://${local.radarr_name}:${local.radarr_port}/radarr"
|
||||
RADARR_KEY = provider::dotenv::get_by_key("RADARR_API_KEY", local.env_file)
|
||||
SONARR_URL = "http://${local.sonarr_name}:${local.sonarr_port}/sonarr"
|
||||
SONARR_KEY = provider::dotenv::get_by_key("SONARR_API_KEY", local.env_file)
|
||||
LIDARR_URL = "http://${local.lidarr_name}:${local.lidarr_port}/lidarr"
|
||||
LIDARR_KEY = provider::dotenv::get_by_key("LIDARR_API_KEY", local.env_file)
|
||||
QBITTORRENT_URL = "http://${var.qbittorrent_host}:8080"
|
||||
QBITTORRENT_USERNAME = provider::dotenv::get_by_key("QBITTORRENT_USERNAME", local.env_file)
|
||||
QBITTORRENT_PASSWORD = provider::dotenv::get_by_key("QBITTORRENT_PASSWORD", local.env_file)
|
||||
LOG_LEVEL = try(provider::dotenv::get_by_key("DECLUTTARR_LOG_LEVEL", local.env_file), "")
|
||||
TEST_RUN = try(provider::dotenv::get_by_key("DECLUTTARR_TEST_RUN", local.env_file), "")
|
||||
REMOVE_TIMER = try(provider::dotenv::get_by_key("DECLUTTARR_REMOVE_TIMER", local.env_file), "")
|
||||
REMOVE_FAILED = try(provider::dotenv::get_by_key("DECLUTTARR_REMOVE_FAILED", local.env_file), "")
|
||||
REMOVE_FAILED_IMPORTS = try(provider::dotenv::get_by_key("DECLUTTARR_REMOVE_FAILED_IMPORTS", local.env_file), "")
|
||||
REMOVE_METADATA_MISSING = try(provider::dotenv::get_by_key("DECLUTTARR_REMOVE_METADATA_MISSING", local.env_file), "")
|
||||
REMOVE_MISSING_FILES = try(provider::dotenv::get_by_key("DECLUTTARR_REMOVE_MISSING_FILES", local.env_file), "")
|
||||
REMOVE_ORPHANS = try(provider::dotenv::get_by_key("DECLUTTARR_REMOVE_ORPHANS", local.env_file), "")
|
||||
REMOVE_SLOW = try(provider::dotenv::get_by_key("DECLUTTARR_REMOVE_SLOW", local.env_file), "")
|
||||
REMOVE_STALLED = try(provider::dotenv::get_by_key("DECLUTTARR_REMOVE_STALLED", local.env_file), "")
|
||||
REMOVE_UNMONITORED = try(provider::dotenv::get_by_key("DECLUTTARR_REMOVE_UNMONITORED", local.env_file), "")
|
||||
RUN_PERIODIC_RESCANS = try(provider::dotenv::get_by_key("DECLUTTARR_RUN_PERIODIC_RESCANS", local.env_file), "")
|
||||
PERMITTED_ATTEMPTS = try(provider::dotenv::get_by_key("DECLUTTARR_PERMITTED_ATTEMPTS", local.env_file), "")
|
||||
NO_STALLED_REMOVAL_QBIT_TAG = try(provider::dotenv::get_by_key("DECLUTTARR_REMOVAL_QBIT_TAG", local.env_file), "")
|
||||
MIN_DOWNLOAD_SPEED = try(provider::dotenv::get_by_key("DECLUTTARR_MIN_DOWNLOAD_SPEED", local.env_file), "")
|
||||
FAILED_IMPORT_MESSAGE_PATTERNS = try(provider::dotenv::get_by_key("DECLUTTARR_FAILED_IMPORT_MESSAGE_PATTERNS", local.env_file), "")
|
||||
IGNORED_DOWNLOAD_CLIENTS = try(provider::dotenv::get_by_key("DECLUTTARR_IGNORED_DOWNLOAD_CLIENTS", local.env_file), "")
|
||||
}
|
||||
}
|
||||
|
||||
# Sonarr
|
||||
module "sonarr" {
|
||||
source = "../../10-services-generic/docker-service"
|
||||
container_name = local.sonarr_name
|
||||
image = local.sonarr_image
|
||||
volumes = [
|
||||
{ host_path = "${var.volume_path}/sonarr", container_path = "/config", read_only = false },
|
||||
{ host_path = var.data_path, container_path = "/data", read_only = false }
|
||||
]
|
||||
networks = var.networks
|
||||
monitoring = local.monitoring
|
||||
ports = [{ internal = local.sonarr_port, external = local.sonarr_port, protocol = "tcp" }]
|
||||
}
|
||||
|
||||
# Radarr
|
||||
module "radarr" {
|
||||
source = "../../10-services-generic/docker-service"
|
||||
container_name = local.radarr_name
|
||||
image = local.radarr_image
|
||||
volumes = [
|
||||
{ host_path = "${var.volume_path}/radarr", container_path = "/config", read_only = false },
|
||||
{ host_path = var.data_path, container_path = "/data", read_only = false }
|
||||
]
|
||||
networks = var.networks
|
||||
monitoring = local.monitoring
|
||||
ports = [{ internal = local.radarr_port, external = local.radarr_port, protocol = "tcp" }]
|
||||
}
|
||||
|
||||
# Lidarr
|
||||
module "lidarr" {
|
||||
source = "../../10-services-generic/docker-service"
|
||||
container_name = local.lidarr_name
|
||||
image = local.lidarr_image
|
||||
volumes = [
|
||||
{ host_path = "${var.volume_path}/lidarr", container_path = "/config", read_only = false },
|
||||
{ host_path = var.data_path, container_path = "/data", read_only = false }
|
||||
]
|
||||
networks = var.networks
|
||||
monitoring = local.monitoring
|
||||
healthcheck = local.lidarr_healthcheck
|
||||
ports = [{ internal = local.lidarr_port, external = local.lidarr_port, protocol = "tcp" }]
|
||||
}
|
||||
|
||||
# Bazarr
|
||||
module "bazarr" {
|
||||
source = "../../10-services-generic/docker-service"
|
||||
container_name = local.bazarr_name
|
||||
image = local.bazarr_image
|
||||
volumes = [
|
||||
{ host_path = "${var.volume_path}/bazarr/config", container_path = "/config", read_only = false },
|
||||
{ host_path = var.data_path, container_path = "/data", read_only = false }
|
||||
]
|
||||
networks = var.networks
|
||||
monitoring = local.monitoring
|
||||
healthcheck = local.bazarr_healthcheck
|
||||
ports = [{ internal = local.bazarr_port, external = local.bazarr_port, protocol = "tcp" }]
|
||||
}
|
||||
|
||||
# Prowlarr
|
||||
module "prowlarr" {
|
||||
source = "../../10-services-generic/docker-service"
|
||||
container_name = local.prowlarr_name
|
||||
image = local.prowlarr_image
|
||||
volumes = [
|
||||
{ host_path = "${var.volume_path}/prowlarr", container_path = "/config", read_only = false }
|
||||
]
|
||||
networks = var.networks
|
||||
monitoring = local.monitoring
|
||||
ports = [{ internal = local.prowlarr_port, external = local.prowlarr_port, protocol = "tcp" }]
|
||||
}
|
||||
|
||||
# Jellyseerr (published via reverse proxy)
|
||||
module "jellyseerr" {
|
||||
source = "../../10-services-generic/docker-service"
|
||||
container_name = local.jellyseerr_name
|
||||
image = local.jellyseerr_image
|
||||
volumes = [{ host_path = "${var.volume_path}/jellyseerr", container_path = "/app/config", read_only = false }]
|
||||
env_vars = local.jellyseerr_env
|
||||
networks = concat(var.networks, var.proxy_networks)
|
||||
monitoring = local.monitoring
|
||||
healthcheck = local.jellyseerr_healthcheck
|
||||
ports = [{ internal = local.jellyseerr_port, external = local.jellyseerr_port, protocol = "tcp" }]
|
||||
}
|
||||
|
||||
# Flaresolverr
|
||||
module "flaresolverr" {
|
||||
source = "../../10-services-generic/docker-service"
|
||||
container_name = local.flaresolverr_name
|
||||
image = local.flaresolverr_image
|
||||
tag = "nodriver"
|
||||
env_vars = local.flaresolverr_env
|
||||
networks = var.networks
|
||||
monitoring = local.monitoring
|
||||
ports = [{ internal = local.flaresolverr_port, external = local.flaresolverr_port, protocol = "tcp" }]
|
||||
}
|
||||
|
||||
# Unpackerr
|
||||
module "unpackerr" {
|
||||
source = "../../10-services-generic/docker-service"
|
||||
container_name = local.unpackerr_name
|
||||
image = local.unpackerr_image
|
||||
env_vars = local.unpackerr_env
|
||||
volumes = [{ host_path = var.downloads_path, container_path = "/data/torrents", read_only = false }]
|
||||
networks = var.networks
|
||||
monitoring = local.monitoring
|
||||
}
|
||||
|
||||
# Cleanuparr
|
||||
module "cleanuparr" {
|
||||
source = "../../10-services-generic/docker-service"
|
||||
container_name = local.cleanuparr_name
|
||||
image = local.cleanuparr_image
|
||||
env_vars = local.cleanuparr_env
|
||||
volumes = [
|
||||
{ host_path = "${var.volume_path}/cleanuparr/logs", container_path = "/var/logs", read_only = false },
|
||||
{ host_path = "${var.volume_path}/cleanuparr/ignored.txt", container_path = "/usr/ignored.txt", read_only = false },
|
||||
{ host_path = "${var.volume_path}/cleanuparr/blacklist.json", container_path = "/usr/blacklist.json", read_only = false }
|
||||
]
|
||||
networks = var.networks
|
||||
monitoring = local.monitoring
|
||||
}
|
||||
|
||||
module "decluttarr" {
|
||||
source = "../../10-services-generic/docker-service"
|
||||
container_name = local.decluttarr_name
|
||||
image = local.decluttarr_image
|
||||
env_vars = local.decluttarr_env
|
||||
networks = var.networks
|
||||
monitoring = local.monitoring
|
||||
}
|
||||
|
||||
output "service_definition" {
|
||||
description = "Service definition for Jellyseerr (reverse proxy)"
|
||||
value = {
|
||||
name = local.jellyseerr_name
|
||||
primary_port = local.jellyseerr_port
|
||||
endpoint = "http://${local.jellyseerr_name}:${local.jellyseerr_port}"
|
||||
subdomains = ["req"]
|
||||
publish_via = "reverse_proxy"
|
||||
proxied = true
|
||||
}
|
||||
}
|
||||
33
modules/20-services-apps/gluetun/.env.example
Normal file
33
modules/20-services-apps/gluetun/.env.example
Normal file
@@ -0,0 +1,33 @@
|
||||
# Gluetun VPN (.env example)
|
||||
# Copy to modules/20-services-apps/gluetun/.env and fill in values
|
||||
|
||||
# Provider and VPN type
|
||||
VPN_SERVICE_PROVIDER=mullvad
|
||||
VPN_TYPE=wireguard
|
||||
|
||||
# Wireguard credentials (required)
|
||||
# Generate from Mullvad account: private key and tunnel IP address
|
||||
WIREGUARD_PRIVATE_KEY=
|
||||
# Example: 10.64.0.2/32
|
||||
WIREGUARD_ADDRESSES=
|
||||
|
||||
# Server selection (one of the following is recommended)
|
||||
#SERVER_CITIES="Los Angeles"
|
||||
#SERVER_COUNTRIES="United States"
|
||||
|
||||
# Exact server pinning (optional; supports comma-separated list)
|
||||
# For Jakarta example:
|
||||
#SERVER_HOSTNAMES=id-jpu-wg-001
|
||||
#SERVER_HOSTNAME=id-jpu-wg-001
|
||||
|
||||
# Updater period (optional)
|
||||
#UPDATER_PERIOD=24h
|
||||
|
||||
# Firewall rules (optional)
|
||||
# Allow outbound traffic to RFC1918 subnets so LAN services can be reached
|
||||
# Example: 10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
|
||||
#FIREWALL_OUTBOUND_SUBNETS=
|
||||
|
||||
# If you need to accept inbound connections through Gluetun (e.g., expose qBittorrent UI),
|
||||
# publish the port in Terraform and optionally set FIREWALL_INPUT_PORTS as well
|
||||
#FIREWALL_INPUT_PORTS=8080
|
||||
65
modules/20-services-apps/gluetun/README.md
Normal file
65
modules/20-services-apps/gluetun/README.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# Gluetun (Mullvad Wireguard)
|
||||
|
||||
This module runs Gluetun to provide a VPN network stack for other containers.
|
||||
You can route qBittorrent through Gluetun by setting its `network_mode` to `container:gluetun` using the provided toggle in the qBittorrent module.
|
||||
|
||||
- Image: `qmcgaw/gluetun:v3.39.0`
|
||||
- Requires: NET_ADMIN capability and `/dev/net/tun` device
|
||||
- Default: No ports exposed on host. Publish only if you need host access.
|
||||
- Attach Gluetun to the same Docker network as services that should reach apps running through it (e.g., `media-network`).
|
||||
|
||||
## Usage
|
||||
|
||||
Example in `services/main.tf`:
|
||||
|
||||
```hcl
|
||||
module "gluetun" {
|
||||
source = "${local.module_dir}/20-services-apps/gluetun"
|
||||
volume_path = "${local.volume_host}/gluetun"
|
||||
networks = [module.media_docker_network.name]
|
||||
# Optionally expose qBittorrent's Web UI to the host via Gluetun:
|
||||
# ports = [{ internal = 8080, external = 8080, protocol = "tcp" }]
|
||||
}
|
||||
|
||||
module "qbittorrent" {
|
||||
source = "${local.module_dir}/20-services-apps/qbittorrent"
|
||||
volume_path = "${local.volume_host}/qbittorrent"
|
||||
downloads_path = "${local.data_host}/torrents"
|
||||
networks = [module.media_docker_network.name]
|
||||
connect_via_gluetun = true
|
||||
gluetun_container_name = "gluetun"
|
||||
}
|
||||
|
||||
module "arr" {
|
||||
source = "${local.module_dir}/20-services-apps/arr"
|
||||
volume_path = "${local.volume_host}/arr"
|
||||
data_path = local.data_host
|
||||
downloads_path = "${local.data_host}/torrents"
|
||||
networks = [module.media_docker_network.name]
|
||||
proxy_networks = [module.homelab_docker_network.name]
|
||||
qbittorrent_host = "gluetun" # arr containers will reach qBt at http://gluetun:8080
|
||||
}
|
||||
```
|
||||
|
||||
## Environment variables
|
||||
|
||||
Place a `.env` file in this module directory (`modules/20-services-apps/gluetun/.env`). See `.env.example` for all options. Key variables:
|
||||
|
||||
- VPN_SERVICE_PROVIDER=mullvad
|
||||
- VPN_TYPE=wireguard
|
||||
- WIREGUARD_PRIVATE_KEY=... (required)
|
||||
- WIREGUARD_ADDRESSES=10.64.0.2/32 (example)
|
||||
- SERVER_CITIES=... or SERVER_COUNTRIES=...
|
||||
- SERVER_HOSTNAMES=id-jpu-wg-001 (optional exact server pin; supports comma-separated list)
|
||||
- UPDATER_PERIOD=24h (optional)
|
||||
- FIREWALL_OUTBOUND_SUBNETS=10.0.0.0/8,192.168.0.0/16 (optional; allow containers to reach LAN subnets)
|
||||
- Optional: `FIREWALL_INPUT_PORTS=8080` if you need other containers/LAN to initiate connections to services through Gluetun.
|
||||
|
||||
Notes:
|
||||
- When qBittorrent shares Gluetun's network, other containers should use `http://gluetun:8080`.
|
||||
- To access qBittorrent UI from the host, publish `8080/tcp` on Gluetun via this module's `ports` input or set `FIREWALL_INPUT_PORTS` accordingly.
|
||||
- Do not publish ports on qBittorrent when using Gluetun network mode; publish on Gluetun instead.
|
||||
|
||||
Pinning a specific server:
|
||||
- Set `SERVER_HOSTNAMES=id-jpu-wg-001` to pin to Mullvad Jakarta `id-jpu-wg-001`.
|
||||
- The module also accepts `SERVER_HOSTNAME` for compatibility (falls back to it if `SERVER_HOSTNAMES` is not set).
|
||||
88
modules/20-services-apps/gluetun/main.tf
Normal file
88
modules/20-services-apps/gluetun/main.tf
Normal file
@@ -0,0 +1,88 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
dotenv = { source = "germanbrew/dotenv" }
|
||||
}
|
||||
}
|
||||
|
||||
variable "volume_path" {
|
||||
description = "Base directory for Gluetun state/config mounted at /gluetun"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "networks" {
|
||||
description = "Networks to attach Gluetun to"
|
||||
type = list(string)
|
||||
default = []
|
||||
}
|
||||
|
||||
variable "ports" {
|
||||
description = "Ports to publish on the Gluetun container (used to reach services connected via network_mode: container:gluetun)"
|
||||
type = list(object({
|
||||
internal = number
|
||||
external = number
|
||||
protocol = string
|
||||
}))
|
||||
// Default to no published ports. Publish only if you need host access.
|
||||
default = []
|
||||
}
|
||||
|
||||
variable "image_tag" {
|
||||
description = "Gluetun image tag"
|
||||
type = string
|
||||
default = "v3.39.0"
|
||||
}
|
||||
|
||||
locals {
|
||||
env_file = "${path.module}/.env"
|
||||
container_name = "gluetun"
|
||||
image = "qmcgaw/gluetun"
|
||||
tag = var.image_tag
|
||||
monitoring = true
|
||||
|
||||
// Gluetun environment
|
||||
env_vars = {
|
||||
VPN_SERVICE_PROVIDER = try(provider::dotenv::get_by_key("VPN_SERVICE_PROVIDER", local.env_file), "mullvad")
|
||||
VPN_TYPE = try(provider::dotenv::get_by_key("VPN_TYPE", local.env_file), "wireguard")
|
||||
WIREGUARD_PRIVATE_KEY = provider::dotenv::get_by_key("WIREGUARD_PRIVATE_KEY", local.env_file)
|
||||
WIREGUARD_ADDRESSES = provider::dotenv::get_by_key("WIREGUARD_ADDRESSES", local.env_file)
|
||||
SERVER_CITIES = try(provider::dotenv::get_by_key("SERVER_CITIES", local.env_file), "")
|
||||
SERVER_COUNTRIES = try(provider::dotenv::get_by_key("SERVER_COUNTRIES", local.env_file), "")
|
||||
SERVER_HOSTNAMES = try(
|
||||
provider::dotenv::get_by_key("SERVER_HOSTNAMES", local.env_file),
|
||||
try(provider::dotenv::get_by_key("SERVER_HOSTNAME", local.env_file), "")
|
||||
)
|
||||
UPDATER_PERIOD = try(provider::dotenv::get_by_key("UPDATER_PERIOD", local.env_file), "")
|
||||
FIREWALL_OUTBOUND_SUBNETS = try(provider::dotenv::get_by_key("FIREWALL_OUTBOUND_SUBNETS", local.env_file), "")
|
||||
}
|
||||
|
||||
volumes = [
|
||||
{
|
||||
host_path = var.volume_path,
|
||||
container_path = "/gluetun",
|
||||
read_only = false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
module "gluetun" {
|
||||
source = "../../10-services-generic/docker-service"
|
||||
container_name = local.container_name
|
||||
image = local.image
|
||||
tag = local.tag
|
||||
env_vars = local.env_vars
|
||||
volumes = local.volumes
|
||||
networks = var.networks
|
||||
monitoring = local.monitoring
|
||||
|
||||
// Grant minimal privileges required by Gluetun
|
||||
capabilities_add = ["NET_ADMIN"]
|
||||
devices = [
|
||||
{
|
||||
host_path = "/dev/net/tun"
|
||||
container_path = "/dev/net/tun"
|
||||
permissions = "rwm"
|
||||
}
|
||||
]
|
||||
|
||||
ports = var.ports
|
||||
}
|
||||
4
modules/20-services-apps/jellyfin/.env.example
Normal file
4
modules/20-services-apps/jellyfin/.env.example
Normal file
@@ -0,0 +1,4 @@
|
||||
# Optional values for Jellyfin module
|
||||
# Only needed if you enable JELLYFIN_PublishedServerUrl in main.tf
|
||||
|
||||
HOSTNAME=example.com
|
||||
87
modules/20-services-apps/jellyfin/README.md
Normal file
87
modules/20-services-apps/jellyfin/README.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# Jellyfin Module
|
||||
|
||||
This module deploys Jellyfin as a Docker container and outputs a service definition to be published via your reverse proxy.
|
||||
|
||||
## Overview
|
||||
|
||||
- Container: `jellyfin` (LinuxServer.io)
|
||||
- TCP 8096 for HTTP UI; UDP 7359/1900 for discovery/DLNA
|
||||
- Config and media volumes mapped via variables
|
||||
|
||||
## Usage
|
||||
|
||||
```hcl
|
||||
module "jellyfin" {
|
||||
source = "./modules/20-services-apps/jellyfin"
|
||||
volume_path = "/srv/appdata/jellyfin" # host path for Jellyfin config
|
||||
data_path = "/srv/data" # host media root, mounted as /data
|
||||
networks = [module.media_docker_network.name, module.homelab_docker_network.name]
|
||||
}
|
||||
```
|
||||
|
||||
## Variables
|
||||
|
||||
| Variable | Description | Type | Default |
|
||||
| ------------- | --------------------------------------------- | -------------- | ------- |
|
||||
| `volume_path` | Base directory for Jellyfin config | `string` | - |
|
||||
| `data_path` | Base directory for media data mounted at /data | `string` | - |
|
||||
| `networks` | List of networks to attach | `list(string)` | `[]` |
|
||||
|
||||
## Outputs
|
||||
|
||||
| Output | Description |
|
||||
| -------------------- | -------------------------------------------- |
|
||||
| `service_definition` | Service definition for integration with networking modules |
|
||||
|
||||
## Service Definition
|
||||
|
||||
This module outputs a service definition that is used by the networking modules to expose the service.
|
||||
|
||||
```hcl
|
||||
{
|
||||
name = "jellyfin"
|
||||
primary_port = 8096
|
||||
endpoint = "http://jellyfin:8096"
|
||||
subdomains = ["stream"]
|
||||
publish_via = "reverse_proxy"
|
||||
proxied = false
|
||||
}
|
||||
```
|
||||
|
||||
## Environment Variables (.env)
|
||||
|
||||
This module optionally reads `HOSTNAME` from `.env` if you choose to publish a fixed external URL (see commented example in `main.tf`).
|
||||
|
||||
- `HOSTNAME` — your public domain (e.g., example.com). Used only if you enable `JELLYFIN_PublishedServerUrl`.
|
||||
|
||||
Note: `TZ`, `PUID`, and `PGID` are provided automatically by the generic docker-service module via system globals.
|
||||
|
||||
## Data Persistence
|
||||
|
||||
- `/config` -> `${volume_path}`
|
||||
- `/data` -> `${data_path}`
|
||||
|
||||
Ensure the host paths exist and are writable by the container user.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- No explicit inter-container dependencies. Healthcheck ensures readiness.
|
||||
- UDP ports are exposed for discovery/DLNA.
|
||||
|
||||
## Integration with Networking Modules
|
||||
|
||||
This service is configured to be exposed through the Caddy reverse proxy, set by `publish_via = "reverse_proxy"`.
|
||||
|
||||
## Example Integration in Main Configuration
|
||||
|
||||
```hcl
|
||||
# In services/main.tf
|
||||
module "jellyfin" {
|
||||
source = "${local.module_dir}/20-services-apps/jellyfin"
|
||||
volume_path = "${local.volume_host}/jellyfin"
|
||||
data_path = "${local.data_host}/media"
|
||||
networks = [module.media_docker_network.name, module.homelab_docker_network.name]
|
||||
}
|
||||
```
|
||||
|
||||
The service definition is exported by the `services` module as `module.services.service_definitions` and consumed by networking modules in the root `main.tf`.
|
||||
96
modules/20-services-apps/jellyfin/main.tf
Normal file
96
modules/20-services-apps/jellyfin/main.tf
Normal file
@@ -0,0 +1,96 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
dotenv = { source = "germanbrew/dotenv" }
|
||||
}
|
||||
}
|
||||
|
||||
variable "volume_path" {
|
||||
description = "Base directory for Jellyfin config"
|
||||
type = string
|
||||
}
|
||||
variable "data_path" {
|
||||
description = "Base directory for media data mounted at /data"
|
||||
type = string
|
||||
}
|
||||
variable "networks" {
|
||||
description = "List of networks to attach"
|
||||
type = list(string)
|
||||
default = []
|
||||
}
|
||||
|
||||
locals {
|
||||
env_file = "${path.module}/.env"
|
||||
monitoring = true
|
||||
container_name = "jellyfin"
|
||||
image = "lscr.io/linuxserver/jellyfin"
|
||||
tag = "latest"
|
||||
internal_port = 8096
|
||||
|
||||
# UDP ports for DLNA/auto-discovery
|
||||
udp_ports = [
|
||||
{ internal = 7359, external = 7359, protocol = "udp" },
|
||||
{ internal = 1900, external = 1900, protocol = "udp" }
|
||||
]
|
||||
|
||||
volumes = [
|
||||
{
|
||||
host_path = var.volume_path,
|
||||
container_path = "/config",
|
||||
read_only = false
|
||||
},
|
||||
{
|
||||
host_path = var.volume_path,
|
||||
container_path = "/cache",
|
||||
read_only = false
|
||||
},
|
||||
{
|
||||
host_path = var.data_path,
|
||||
container_path = "/data",
|
||||
read_only = false
|
||||
}
|
||||
]
|
||||
|
||||
env_vars = {
|
||||
# If you want to publish external URL, uncomment the following and set HOSTNAME in .env
|
||||
JELLYFIN_PublishedServerUrl = "${provider::dotenv::get_by_key("HOSTNAME", local.env_file)}/jellyfin"
|
||||
}
|
||||
|
||||
# Intel VAAPI/QSV: map the entire /dev/dri directory (per linuxserver/jellyfin docs)
|
||||
devices = [
|
||||
{
|
||||
host_path = "/dev/dri/renderD128",
|
||||
container_path = "/dev/dri/renderD128",
|
||||
permissions = "rwm"
|
||||
},
|
||||
{
|
||||
host_path = "/dev/dri/card0",
|
||||
container_path = "/dev/dri/card0",
|
||||
permissions = "rwm"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
module "jellyfin" {
|
||||
source = "../../10-services-generic/docker-service"
|
||||
container_name = local.container_name
|
||||
image = local.image
|
||||
tag = local.tag
|
||||
volumes = local.volumes
|
||||
env_vars = local.env_vars
|
||||
networks = var.networks
|
||||
monitoring = local.monitoring
|
||||
ports = local.udp_ports
|
||||
devices = local.devices
|
||||
}
|
||||
|
||||
output "service_definition" {
|
||||
description = "Service definition for Jellyfin (reverse proxy)"
|
||||
value = {
|
||||
name = local.container_name
|
||||
primary_port = local.internal_port
|
||||
endpoint = "http://${local.container_name}:${local.internal_port}"
|
||||
subdomains = ["stream"]
|
||||
publish_via = "reverse_proxy"
|
||||
proxied = true
|
||||
}
|
||||
}
|
||||
117
modules/20-services-apps/qbittorrent/README.md
Normal file
117
modules/20-services-apps/qbittorrent/README.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# qBittorrent Module
|
||||
|
||||
This module deploys qBittorrent as a Docker container with the Vuetorrent UI mod.
|
||||
|
||||
## Overview
|
||||
|
||||
- Container: `qbittorrent` (LinuxServer.io) with `vuetorrent` mod
|
||||
- Web UI on TCP 8080
|
||||
- Mounts `/config` and `/data/torrents`
|
||||
|
||||
## Usage
|
||||
|
||||
Without Gluetun:
|
||||
|
||||
```hcl
|
||||
module "qbittorrent" {
|
||||
source = "./modules/20-services-apps/qbittorrent"
|
||||
volume_path = "/srv/appdata/qbittorrent"
|
||||
downloads_path = "/srv/data/torrents"
|
||||
networks = [module.media_docker_network.name]
|
||||
}
|
||||
```
|
||||
|
||||
With Gluetun (recommended for privacy):
|
||||
|
||||
```hcl
|
||||
module "gluetun" {
|
||||
source = "./modules/20-services-apps/gluetun"
|
||||
volume_path = "/srv/appdata/gluetun"
|
||||
networks = [module.media_docker_network.name]
|
||||
# Optional: expose qBittorrent UI to the host via Gluetun
|
||||
# ports = [{ internal = 8080, external = 8080, protocol = "tcp" }]
|
||||
}
|
||||
|
||||
module "qbittorrent" {
|
||||
source = "./modules/20-services-apps/qbittorrent"
|
||||
volume_path = "/srv/appdata/qbittorrent"
|
||||
downloads_path = "/srv/data/torrents"
|
||||
networks = [module.media_docker_network.name]
|
||||
connect_via_gluetun = true
|
||||
gluetun_container_name = "gluetun"
|
||||
}
|
||||
```
|
||||
|
||||
## Variables
|
||||
|
||||
| Variable | Description | Type | Default |
|
||||
| ----------------------- | --------------------------------------------------------------- | -------------- | ----------- |
|
||||
| `volume_path` | Base directory for qBittorrent config | `string` | - |
|
||||
| `downloads_path` | Directory for downloads mounted at /data/torrents | `string` | - |
|
||||
| `networks` | Networks to attach (ignored when `connect_via_gluetun` is true) | `list(string)` | `[]` |
|
||||
| `connect_via_gluetun` | Route qBittorrent through Gluetun (network_mode=container:gluetun) | `bool` | `false` |
|
||||
| `gluetun_container_name`| Container name of the Gluetun instance | `string` | `"gluetun"` |
|
||||
|
||||
## Outputs
|
||||
|
||||
| Output | Description |
|
||||
| -------------------- | ----------------------------------- |
|
||||
| `service_definition` | Service definition for integration with networking modules |
|
||||
|
||||
## Service Definition
|
||||
|
||||
This module outputs a service definition that is used by the networking modules. qBittorrent is not published externally.
|
||||
|
||||
```hcl
|
||||
{
|
||||
name = "qbittorrent"
|
||||
primary_port = 8080
|
||||
endpoint = "http://qbittorrent:8080"
|
||||
}
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
- Defaults:
|
||||
- `WEBUI_PORT=8080`
|
||||
- `DOCKER_MODS=ghcr.io/gabe565/linuxserver-mod-vuetorrent`
|
||||
- `TZ`, `PUID`, and `PGID` are injected by the generic docker-service module from system globals.
|
||||
|
||||
## Data Persistence
|
||||
|
||||
- `/config` -> `${volume_path}`
|
||||
- `/data/torrents` -> `${downloads_path}`
|
||||
- Ensure host paths exist and permissions align with the container user.
|
||||
|
||||
## Networking
|
||||
|
||||
- When `connect_via_gluetun = false`:
|
||||
- Container attaches to `networks` and exposes its Web UI internally at `http://qbittorrent:8080`.
|
||||
- When `connect_via_gluetun = true`:
|
||||
- Container runs with `network_mode = container:<gluetun_container_name>`.
|
||||
- Do not publish ports on qBittorrent. If you need host access, publish `8080/tcp` on the Gluetun module instead.
|
||||
- Other containers should reach the Web UI at `http://gluetun:8080` when on the same Docker network as Gluetun.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- No explicit inter-container dependencies. Healthcheck ensures readiness.
|
||||
|
||||
## Integration with Networking Modules
|
||||
|
||||
This service is not published externally. Its service definition is included in the aggregated `module.services.service_definitions` for internal discovery and potential future use by networking modules.
|
||||
|
||||
## Example Integration in Main Configuration
|
||||
|
||||
```hcl
|
||||
# In services/main.tf
|
||||
module "qbittorrent" {
|
||||
source = "${local.module_dir}/20-services-apps/qbittorrent"
|
||||
volume_path = "${local.volume_host}/qbittorrent"
|
||||
downloads_path = "${local.data_host}/torrents"
|
||||
networks = [module.media_docker_network.name]
|
||||
connect_via_gluetun = true
|
||||
gluetun_container_name = "gluetun"
|
||||
}
|
||||
```
|
||||
|
||||
The service definition is exported by the `services` module as `module.services.service_definitions` and consumed by networking modules in the root `main.tf`.
|
||||
86
modules/20-services-apps/qbittorrent/main.tf
Normal file
86
modules/20-services-apps/qbittorrent/main.tf
Normal file
@@ -0,0 +1,86 @@
|
||||
variable "volume_path" {
|
||||
description = "Base directory for qBittorrent config"
|
||||
type = string
|
||||
}
|
||||
variable "downloads_path" {
|
||||
description = "Directory for downloads mounted at /data/torrents"
|
||||
type = string
|
||||
}
|
||||
variable "networks" {
|
||||
description = "List of networks to attach"
|
||||
type = list(string)
|
||||
default = []
|
||||
}
|
||||
|
||||
// When true, run qBittorrent through Gluetun by sharing its network namespace
|
||||
variable "connect_via_gluetun" {
|
||||
description = "Route qBittorrent through Gluetun (network_mode=container:gluetun)"
|
||||
type = bool
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "gluetun_container_name" {
|
||||
description = "Container name of the Gluetun instance to share network with"
|
||||
type = string
|
||||
default = "gluetun"
|
||||
}
|
||||
|
||||
locals {
|
||||
container_name = "qbittorrent"
|
||||
image = "lscr.io/linuxserver/qbittorrent"
|
||||
tag = "libtorrentv1"
|
||||
monitoring = true
|
||||
internal_port = 8080
|
||||
|
||||
use_gluetun = var.connect_via_gluetun
|
||||
gluetun_name = var.gluetun_container_name
|
||||
network_mode = local.use_gluetun ? "container:${local.gluetun_name}" : "bridge"
|
||||
|
||||
env_vars = {
|
||||
WEBUI_PORT = "8080"
|
||||
DOCKER_MODS = "ghcr.io/gabe565/linuxserver-mod-vuetorrent"
|
||||
}
|
||||
|
||||
volumes = [
|
||||
{
|
||||
host_path = var.volume_path,
|
||||
container_path = "/config",
|
||||
read_only = false
|
||||
},
|
||||
{
|
||||
host_path = var.downloads_path,
|
||||
container_path = "/data/torrents",
|
||||
read_only = false
|
||||
}
|
||||
]
|
||||
|
||||
healthcheck = {
|
||||
test = ["CMD", "curl", "--fail", "http://127.0.0.1:8080", "https://google.com"]
|
||||
interval = "60s"
|
||||
timeout = "5s"
|
||||
retries = 10
|
||||
}
|
||||
}
|
||||
|
||||
module "qbittorrent" {
|
||||
source = "../../10-services-generic/docker-service"
|
||||
container_name = local.container_name
|
||||
image = local.image
|
||||
tag = local.tag
|
||||
env_vars = local.env_vars
|
||||
volumes = local.volumes
|
||||
network_mode = local.network_mode
|
||||
networks = local.use_gluetun ? [] : var.networks
|
||||
monitoring = local.monitoring
|
||||
healthcheck = local.healthcheck
|
||||
ports = local.use_gluetun ? [] : [{ internal = local.internal_port, external = local.internal_port, protocol = "tcp" }]
|
||||
}
|
||||
|
||||
output "service_definition" {
|
||||
description = "Service definition for qBittorrent (not published)"
|
||||
value = {
|
||||
name = local.container_name
|
||||
primary_port = local.internal_port
|
||||
endpoint = "http://${local.container_name}:${local.internal_port}"
|
||||
}
|
||||
}
|
||||
78
modules/20-services-apps/sabnzbd/README.md
Normal file
78
modules/20-services-apps/sabnzbd/README.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# Sabnzbd Module
|
||||
|
||||
This module deploys Sabnzbd as a Docker container and outputs a non-published service definition.
|
||||
|
||||
## Overview
|
||||
|
||||
- Container: `sabnzbd` (LinuxServer.io)
|
||||
- Web UI on TCP 8080
|
||||
- Mounts `/config` and `/downloads`
|
||||
|
||||
## Usage
|
||||
|
||||
```hcl
|
||||
module "sabnzbd" {
|
||||
source = "./modules/20-services-apps/sabnzbd"
|
||||
volume_path = "/srv/appdata/sabnzbd" # host path for app config
|
||||
downloads_path = "/srv/data/usenet" # host path for usenet downloads
|
||||
networks = [module.media_docker_network.name, module.homelab_docker_network.name]
|
||||
}
|
||||
```
|
||||
|
||||
## Variables
|
||||
|
||||
| Variable | Description | Type | Default |
|
||||
| ---------------- | ------------------------------------------- | -------------- | ------- |
|
||||
| `volume_path` | Base directory for Sabnzbd config | `string` | - |
|
||||
| `downloads_path` | Directory for downloads mounted at /downloads | `string` | - |
|
||||
| `networks` | List of networks to attach | `list(string)` | `[]` |
|
||||
|
||||
## Outputs
|
||||
|
||||
| Output | Description |
|
||||
| -------------------- | ------------------------------ |
|
||||
| `service_definition` | Service definition for integration with networking modules |
|
||||
|
||||
## Service Definition
|
||||
|
||||
This module outputs a service definition that is used by the networking modules. Sabnzbd is not published externally.
|
||||
|
||||
```hcl
|
||||
{
|
||||
name = "sabnzbd"
|
||||
primary_port = 8080
|
||||
endpoint = "http://sabnzbd:8080"
|
||||
}
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
- `TZ`, `PUID`, and `PGID` are injected automatically via system globals in the generic docker-service module.
|
||||
|
||||
## Data Persistence
|
||||
|
||||
- `/config` -> `${volume_path}`
|
||||
- `/downloads` -> `${downloads_path}`
|
||||
- Ensure host paths exist and permissions align with the container user.
|
||||
|
||||
## Networking
|
||||
|
||||
- Attaches to `networks` (typically media and homelab). Not published externally; accessible internally.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- No explicit inter-container dependencies. Healthcheck ensures readiness.
|
||||
|
||||
## Example Integration in Main Configuration
|
||||
|
||||
```hcl
|
||||
# In services/main.tf
|
||||
module "sabnzbd" {
|
||||
source = "${local.module_dir}/20-services-apps/sabnzbd"
|
||||
volume_path = "${local.volume_host}/sabnzbd"
|
||||
downloads_path = "${local.data_host}/usenet"
|
||||
networks = [module.media_docker_network.name, module.homelab_docker_network.name]
|
||||
}
|
||||
```
|
||||
|
||||
The service definition is exported by the `services` module as `module.services.service_definitions` and consumed by networking modules in the root `main.tf`.
|
||||
69
modules/20-services-apps/sabnzbd/main.tf
Normal file
69
modules/20-services-apps/sabnzbd/main.tf
Normal file
@@ -0,0 +1,69 @@
|
||||
variable "volume_path" {
|
||||
description = "Base directory for Sabnzbd config"
|
||||
type = string
|
||||
}
|
||||
variable "downloads_path" {
|
||||
description = "Directory for downloads mounted at /downloads"
|
||||
type = string
|
||||
}
|
||||
variable "networks" {
|
||||
description = "List of networks to attach"
|
||||
type = list(string)
|
||||
default = []
|
||||
}
|
||||
|
||||
locals {
|
||||
container_name = "sabnzbd"
|
||||
image = "lscr.io/linuxserver/sabnzbd"
|
||||
tag = "latest"
|
||||
monitoring = true
|
||||
internal_port = 8080
|
||||
|
||||
env_vars = {
|
||||
# Add typical env like PUID/PGID/TZ if desired via the generic module interface
|
||||
}
|
||||
|
||||
volumes = [
|
||||
{
|
||||
host_path = var.volume_path,
|
||||
container_path = "/config",
|
||||
read_only = false
|
||||
},
|
||||
{
|
||||
host_path = var.downloads_path,
|
||||
container_path = "/data/usenet/downloads",
|
||||
read_only = false
|
||||
}
|
||||
]
|
||||
|
||||
healthcheck = {
|
||||
test = ["CMD", "curl", "--fail", "http://127.0.0.1:8080"]
|
||||
interval = "60s"
|
||||
timeout = "5s"
|
||||
retries = 10
|
||||
}
|
||||
}
|
||||
|
||||
module "sabnzbd" {
|
||||
source = "../../10-services-generic/docker-service"
|
||||
container_name = local.container_name
|
||||
image = local.image
|
||||
tag = local.tag
|
||||
env_vars = local.env_vars
|
||||
volumes = local.volumes
|
||||
networks = var.networks
|
||||
monitoring = local.monitoring
|
||||
healthcheck = local.healthcheck
|
||||
}
|
||||
|
||||
output "service_definition" {
|
||||
description = "Service definition for Sabnzbd (not published)"
|
||||
value = {
|
||||
name = local.container_name
|
||||
primary_port = local.internal_port
|
||||
endpoint = "http://${local.container_name}:${local.internal_port}"
|
||||
subdomains = ["sabnzbd"]
|
||||
publish_via = "tunnel"
|
||||
proxied = true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user