diff --git a/modules/20-services-apps/arr/.env.example b/modules/20-services-apps/arr/.env.example new file mode 100644 index 0000000..05d45f5 --- /dev/null +++ b/modules/20-services-apps/arr/.env.example @@ -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= diff --git a/modules/20-services-apps/arr/README.md b/modules/20-services-apps/arr/README.md new file mode 100644 index 0000000..0eec3ca --- /dev/null +++ b/modules/20-services-apps/arr/README.md @@ -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}//...` 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://: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`. diff --git a/modules/20-services-apps/arr/main.tf b/modules/20-services-apps/arr/main.tf new file mode 100644 index 0000000..9277087 --- /dev/null +++ b/modules/20-services-apps/arr/main.tf @@ -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 + } +} diff --git a/modules/20-services-apps/gluetun/.env.example b/modules/20-services-apps/gluetun/.env.example new file mode 100644 index 0000000..31b4fab --- /dev/null +++ b/modules/20-services-apps/gluetun/.env.example @@ -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 diff --git a/modules/20-services-apps/gluetun/README.md b/modules/20-services-apps/gluetun/README.md new file mode 100644 index 0000000..9e52f64 --- /dev/null +++ b/modules/20-services-apps/gluetun/README.md @@ -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). diff --git a/modules/20-services-apps/gluetun/main.tf b/modules/20-services-apps/gluetun/main.tf new file mode 100644 index 0000000..4cce1fd --- /dev/null +++ b/modules/20-services-apps/gluetun/main.tf @@ -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 +} diff --git a/modules/20-services-apps/jellyfin/.env.example b/modules/20-services-apps/jellyfin/.env.example new file mode 100644 index 0000000..28e8e7a --- /dev/null +++ b/modules/20-services-apps/jellyfin/.env.example @@ -0,0 +1,4 @@ +# Optional values for Jellyfin module +# Only needed if you enable JELLYFIN_PublishedServerUrl in main.tf + +HOSTNAME=example.com diff --git a/modules/20-services-apps/jellyfin/README.md b/modules/20-services-apps/jellyfin/README.md new file mode 100644 index 0000000..da33b31 --- /dev/null +++ b/modules/20-services-apps/jellyfin/README.md @@ -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`. diff --git a/modules/20-services-apps/jellyfin/main.tf b/modules/20-services-apps/jellyfin/main.tf new file mode 100644 index 0000000..adb986f --- /dev/null +++ b/modules/20-services-apps/jellyfin/main.tf @@ -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 + } +} diff --git a/modules/20-services-apps/qbittorrent/README.md b/modules/20-services-apps/qbittorrent/README.md new file mode 100644 index 0000000..fed69c2 --- /dev/null +++ b/modules/20-services-apps/qbittorrent/README.md @@ -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:`. + - 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`. diff --git a/modules/20-services-apps/qbittorrent/main.tf b/modules/20-services-apps/qbittorrent/main.tf new file mode 100644 index 0000000..9177cdd --- /dev/null +++ b/modules/20-services-apps/qbittorrent/main.tf @@ -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}" + } +} diff --git a/modules/20-services-apps/sabnzbd/README.md b/modules/20-services-apps/sabnzbd/README.md new file mode 100644 index 0000000..f3e7f27 --- /dev/null +++ b/modules/20-services-apps/sabnzbd/README.md @@ -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`. diff --git a/modules/20-services-apps/sabnzbd/main.tf b/modules/20-services-apps/sabnzbd/main.tf new file mode 100644 index 0000000..2eb79f8 --- /dev/null +++ b/modules/20-services-apps/sabnzbd/main.tf @@ -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 + } +} diff --git a/services/main.tf b/services/main.tf index fa316b0..268c8d3 100755 --- a/services/main.tf +++ b/services/main.tf @@ -20,6 +20,16 @@ module "homelab_docker_network" { subnet = "10.100.0.0/16" } +// Docker network used for media services +module "media_docker_network" { + source = "${local.module_dir}/01-networking/docker-network" + + name = "media-network" + driver = "bridge" + attachable = true + subnet = "10.110.0.0/16" +} + module "actualbudget" { source = "${local.module_dir}/20-services-apps/actualbudget" volume_path = "${local.volume_host}/actual" @@ -32,6 +42,16 @@ module "affine" { networks = [module.homelab_docker_network.name] } +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" +} + module "calibre" { source = "${local.module_dir}/20-services-apps/calibre" volume_path = "${local.volume_host}/calibre" @@ -63,6 +83,20 @@ module "glance" { networks = [module.homelab_docker_network.name] } +module "gluetun" { + source = "${local.module_dir}/20-services-apps/gluetun" + volume_path = "${local.volume_host}/gluetun" + networks = [module.media_docker_network.name] + ports = [ + # Expose qBittorrent UI to the host + { + internal = 8080 + external = 8080 + protocol = "tcp" + } + ] +} + module "immich" { source = "${local.module_dir}/20-services-apps/immich" appdata_path = "${local.volume_host}/immich" @@ -70,6 +104,13 @@ module "immich" { networks = [module.homelab_docker_network.name] } +module "jellyfin" { + source = "${local.module_dir}/20-services-apps/jellyfin" + volume_path = "${local.volume_host}/jellyfin" + data_path = "${local.data_host}" + networks = [module.media_docker_network.name, module.homelab_docker_network.name] +} + module "linkwarden" { source = "${local.module_dir}/20-services-apps/linkwarden" volume_path = "${local.volume_host}/linkwarden" @@ -112,6 +153,23 @@ module "pterodactyl_wings" { networks = [module.homelab_docker_network.name] } +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" + depends_on = [module.gluetun] +} + +module "sabnzbd" { + source = "${local.module_dir}/20-services-apps/sabnzbd" + volume_path = "${local.volume_host}/sabnzbd" + downloads_path = "${local.data_host}/usenet/downloads" + networks = [module.media_docker_network.name, module.homelab_docker_network.name] +} + module "searxng" { source = "${local.module_dir}/20-services-apps/searxng" volume_path = "${local.volume_host}/searxng" diff --git a/services/outputs.tf b/services/outputs.tf index 579e037..44c2271 100755 --- a/services/outputs.tf +++ b/services/outputs.tf @@ -6,12 +6,14 @@ output "service_definitions" { value = [ module.actualbudget.service_definition, module.affine.service_definition, + module.arr.service_definition, module.calibre.service_definition, module.copyparty.service_definition, module.crawl4ai.service_definition, module.emulatorjs.service_definition, module.glance.service_definition, module.immich.service_definition, + module.jellyfin.service_definition, module.linkwarden.service_definition, module.n8n.service_definition, module.n8n.n8n_mcp_service_definition, @@ -20,6 +22,8 @@ output "service_definitions" { module.portainer.service_definition, module.pterodactyl_wings.service_definition, module.pterodactyl_panel.service_definition, + module.qbittorrent.service_definition, + module.sabnzbd.service_definition, module.searxng.service_definition ] }